mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-30 17:00:57 -06:00
Merge pull request #1009 from owncloud/ocis-1132
This commit is contained in:
@@ -36,6 +36,5 @@ require (
|
||||
replace (
|
||||
github.com/owncloud/ocis/ocis-pkg => ../ocis-pkg
|
||||
github.com/owncloud/ocis/settings => ../settings
|
||||
github.com/owncloud/ocis/storage => ../storage
|
||||
google.golang.org/grpc => google.golang.org/grpc v1.26.0
|
||||
)
|
||||
|
||||
20
changelog/unreleased/user-agent-challenge-lock-in.md
Normal file
20
changelog/unreleased/user-agent-challenge-lock-in.md
Normal file
@@ -0,0 +1,20 @@
|
||||
Enhancement: Add www-authenticate based on user agent
|
||||
|
||||
Tags: reva, proxy
|
||||
|
||||
We now comply with HTTP spec by adding Www-Authenticate headers on every `401` request. Furthermore, we not only take care of such a thing at the Proxy but also Reva will take care of it. In addition, we now are able to lock-in a set of User-Agent to specific challenges.
|
||||
|
||||
Admins can use this feature by configuring OCIS + Reva following this approach:
|
||||
|
||||
```
|
||||
STORAGE_FRONTEND_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT="mirall:basic, Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0:bearer" \
|
||||
PROXY_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT="mirall:basic, Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0:bearer" \
|
||||
PROXY_ENABLE_BASIC_AUTH=true \
|
||||
go run cmd/ocis/main.go server
|
||||
```
|
||||
|
||||
We introduced two new environment variables:
|
||||
|
||||
`STORAGE_FRONTEND_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT` as well as `PROXY_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT`, The reason they have the same value is not to rely on the os env on a distributed environment, so in redundancy we trust. They both configure the same on the backend storage and OCIS Proxy.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/1009
|
||||
@@ -1,6 +1,9 @@
|
||||
package conversions
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// StringToSliceString splits a string into a slice string according to separator
|
||||
func StringToSliceString(src string, sep string) []string {
|
||||
@@ -12,3 +15,15 @@ func StringToSliceString(src string, sep string) []string {
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
// Reverse reverses a string
|
||||
func Reverse(s string) string {
|
||||
size := len(s)
|
||||
buf := make([]byte, size)
|
||||
for start := 0; start < size; {
|
||||
r, n := utf8.DecodeRuneInString(s[start:])
|
||||
start += n
|
||||
utf8.EncodeRune(buf[size-start:], r)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ github.com/aws/aws-sdk-go v1.34.12 h1:7UbBEYDUa4uW0YmRnOd806MS1yoJMcaodBWDzvBShA
|
||||
github.com/aws/aws-sdk-go v1.34.12/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go v1.35.9 h1:b1HiUpdkFLJyoOQ7zas36YHzjNHH0ivHx/G5lWBeg+U=
|
||||
github.com/aws/aws-sdk-go v1.35.9/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
|
||||
github.com/aws/aws-sdk-go v1.35.23 h1:SCP0d0XvyJTDmfnHEQPvBaYi3kea1VNUo7uQmkVgFts=
|
||||
github.com/aws/aws-sdk-go v1.35.23/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
|
||||
github.com/aws/aws-sdk-go v1.35.27/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
|
||||
github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04=
|
||||
@@ -208,6 +209,7 @@ github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666 h1:E7VsSSN/2YZLS
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00 h1:LVl25JaflluOchVvaHWtoCynm5OaM+VNai0IYkcCSe0=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 h1:mZpylrgnCgSeaZ5EznvHIPIKuaQHMHZDi2wkJtk4M8Y=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/reva v1.1.0 h1:Gih6ECHvMMGSx523SFluFlDmNMuhYelXYShdWvjvW38=
|
||||
github.com/cs3org/reva v1.1.0/go.mod h1:fBzTrNuAKdQ62ybjpdu8nyhBin90/3/3s6DGQDCdBp4=
|
||||
@@ -323,6 +325,7 @@ github.com/go-ozzo/ozzo-validation/v4 v4.2.1 h1:XALUNshPYumA7UShB7iM3ZVlqIBn0jfw
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.2.1/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
@@ -953,6 +956,7 @@ github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCv
|
||||
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
|
||||
github.com/pkg/xattr v0.4.1 h1:dhclzL6EqOXNaPDWqoeb9tIxATfBSmjqL0b4DpSjwRw=
|
||||
github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs=
|
||||
github.com/pkg/xattr v0.4.2 h1:fbVxr9lvkToTGgPljVszvFsOdcbSv5BmGABneyxRgZM=
|
||||
github.com/pkg/xattr v0.4.2/go.mod h1:sBD3RAqlr8Q+RC3FutZcikpT8nyDrIEEBw2J744gVWs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -1410,6 +1414,7 @@ golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
|
||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck=
|
||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.7.0
|
||||
contrib.go.opencensus.io/exporter/zipkin v0.1.2
|
||||
github.com/UnnoTed/fileb0x v1.1.4
|
||||
github.com/cs3org/reva v1.4.1-0.20201203075131-783e35cbff51 // indirect
|
||||
github.com/go-test/deep v1.0.6 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
||||
github.com/micro/cli/v2 v2.1.2
|
||||
|
||||
@@ -21,6 +21,10 @@ func StorageFrontendCommand(cfg *config.Config) *cli.Command {
|
||||
Action: func(c *cli.Context) error {
|
||||
scfg := configureStorageFrontend(cfg)
|
||||
|
||||
if err := command.Frontend(scfg).Before(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cli.HandleAction(
|
||||
command.Frontend(scfg).Action,
|
||||
c,
|
||||
|
||||
@@ -192,6 +192,7 @@ github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666 h1:E7VsSSN/2YZLS
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00 h1:LVl25JaflluOchVvaHWtoCynm5OaM+VNai0IYkcCSe0=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 h1:mZpylrgnCgSeaZ5EznvHIPIKuaQHMHZDi2wkJtk4M8Y=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/reva v1.2.2-0.20200924071957-e6676516e61e/go.mod h1:DOV5SjpOBKN+aWfOHLdA4KiLQkpyC786PQaXEdRAZ0M=
|
||||
github.com/cs3org/reva v1.4.1-0.20201120104232-f5afafc04c3b h1:bDGaeyhTFrdLF3Pm8XdJ60ADrE4f+f/Mz2hkICvQHJM=
|
||||
|
||||
@@ -3,6 +3,7 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
openzipkin "github.com/openzipkin/zipkin-go"
|
||||
zipkinhttp "github.com/openzipkin/zipkin-go/reporter/http"
|
||||
acc "github.com/owncloud/ocis/accounts/pkg/proto/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/conversions"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
|
||||
"github.com/owncloud/ocis/proxy/pkg/config"
|
||||
@@ -48,8 +50,10 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
}
|
||||
cfg.PreSignedURL.AllowedHTTPMethods = ctx.StringSlice("presignedurl-allow-method")
|
||||
|
||||
// When running on single binary mode the before hook from the root command won't get called. We manually
|
||||
// call this before hook from ocis command, so the configuration can be loaded.
|
||||
if err := loadUserAgent(ctx, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ParseConfig(ctx, cfg)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
@@ -268,8 +272,8 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic
|
||||
|
||||
return alice.New(
|
||||
middleware.HTTPSRedirect,
|
||||
middleware.OIDCAuth(
|
||||
middleware.Logger(l),
|
||||
middleware.Authentication(
|
||||
// OIDC Options
|
||||
middleware.OIDCProviderFunc(func() (middleware.OIDCProvider, error) {
|
||||
// Initialize a provider by specifying the issuer URL.
|
||||
// it will fetch the keys from the issuer using the .well-known
|
||||
@@ -280,15 +284,15 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic
|
||||
)
|
||||
}),
|
||||
middleware.HTTPClient(oidcHTTPClient),
|
||||
middleware.OIDCIss(cfg.OIDC.Issuer),
|
||||
middleware.TokenCacheSize(cfg.OIDC.UserinfoCache.Size),
|
||||
middleware.TokenCacheTTL(time.Second*time.Duration(cfg.OIDC.UserinfoCache.TTL)),
|
||||
),
|
||||
middleware.BasicAuth(
|
||||
|
||||
// basic Options
|
||||
middleware.Logger(l),
|
||||
middleware.EnableBasicAuth(cfg.EnableBasicAuth),
|
||||
middleware.AccountsClient(accountsClient),
|
||||
middleware.OIDCIss(cfg.OIDC.Issuer),
|
||||
middleware.CredentialsByUserAgent(cfg.Reva.Middleware.Auth.CredentialsByUserAgent),
|
||||
),
|
||||
middleware.SignedURLAuth(
|
||||
middleware.Logger(l),
|
||||
@@ -312,3 +316,28 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// loadUserAgent reads the proxy-user-agent-lock-in, since it is a string flag, and attempts to construct a map of
|
||||
// "user-agent":"challenge" locks in for Reva.
|
||||
// Modifies cfg. Spaces don't need to be trimmed as urfavecli takes care of it. User agents with spaces are valid. i.e:
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0
|
||||
// This function works by relying in our format of specifying [user-agent:challenge] and the fact that the user agent
|
||||
// might contain ":" (colon), so the original string is reversed, split in two parts, by the time it is split we
|
||||
// have the indexes reversed and the tuple is in the format of [challenge:user-agent], then the same process is applied
|
||||
// in reverse for each individual part
|
||||
func loadUserAgent(c *cli.Context, cfg *config.Config) error {
|
||||
cfg.Reva.Middleware.Auth.CredentialsByUserAgent = make(map[string]string)
|
||||
locks := c.StringSlice("proxy-user-agent-lock-in")
|
||||
|
||||
for _, v := range locks {
|
||||
vv := conversions.Reverse(v)
|
||||
parts := strings.SplitN(vv, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("unexpected config value for user-agent lock-in: %v, expected format is user-agent:challenge", v)
|
||||
}
|
||||
|
||||
cfg.Reva.Middleware.Auth.CredentialsByUserAgent[conversions.Reverse(parts[1])] = conversions.Reverse(parts[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,7 +80,18 @@ var (
|
||||
|
||||
// Reva defines all available REVA configuration.
|
||||
type Reva struct {
|
||||
Address string
|
||||
Address string
|
||||
Middleware Middleware
|
||||
}
|
||||
|
||||
// Middleware configures reva middlewares.
|
||||
type Middleware struct {
|
||||
Auth Auth
|
||||
}
|
||||
|
||||
// Auth configures reva http auth middleware.
|
||||
type Auth struct {
|
||||
CredentialsByUserAgent map[string]string
|
||||
}
|
||||
|
||||
// Cache is a TTL cache configuration.
|
||||
|
||||
@@ -232,7 +232,7 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
|
||||
Destination: &cfg.AutoprovisionAccounts,
|
||||
},
|
||||
|
||||
// Presigned URLs
|
||||
// Pre Signed URLs
|
||||
&cli.StringSliceFlag{
|
||||
Name: "presignedurl-allow-method",
|
||||
Value: cli.NewStringSlice("GET"),
|
||||
@@ -255,8 +255,14 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
|
||||
EnvVars: []string{"PROXY_ENABLE_BASIC_AUTH"},
|
||||
Destination: &cfg.EnableBasicAuth,
|
||||
},
|
||||
}
|
||||
|
||||
// Reva Middlewares Config
|
||||
&cli.StringSliceFlag{
|
||||
Name: "proxy-user-agent-lock-in",
|
||||
Usage: "--user-agent-whitelist-lock-in=mirall:basic,foo:bearer Given a tuple of [UserAgent:challenge] it locks a given user agent to the authentication challenge. Particularly useful for old clients whose USer-Agent is known and only support one authentication challenge. When this flag is set in the proxy it configures the authentication middlewares.",
|
||||
EnvVars: []string{"PROXY_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ListProxyWithConfig applies the config to the list commands flags.
|
||||
|
||||
131
proxy/pkg/middleware/authentication.go
Normal file
131
proxy/pkg/middleware/authentication.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// SupportedAuthStrategies stores configured challenges.
|
||||
SupportedAuthStrategies []string
|
||||
|
||||
// ProxyWwwAuthenticate is a list of endpoints that do not rely on reva underlying authentication, such as ocs.
|
||||
// services that fallback to reva authentication are declared in the "frontend" command on OCIS. It is a list of strings
|
||||
// to be regexp compiled.
|
||||
ProxyWwwAuthenticate = []string{"/ocs/v[12].php/cloud/"}
|
||||
|
||||
// WWWAuthenticate captures the Www-Authenticate header string.
|
||||
WWWAuthenticate = "Www-Authenticate"
|
||||
)
|
||||
|
||||
// userAgentLocker aids in dependency injection for helper methods. The set of fields is arbitrary and the only relation
|
||||
// they share is to fulfill their duty and lock a User-Agent to its correct challenge if configured.
|
||||
type userAgentLocker struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
locks map[string]string // locks represents a reva user-agent:challenge mapping.
|
||||
fallback string
|
||||
}
|
||||
|
||||
// Authentication is a higher order authentication middleware.
|
||||
func Authentication(opts ...Option) func(next http.Handler) http.Handler {
|
||||
options := newOptions(opts...)
|
||||
|
||||
configureSupportedChallenges(options)
|
||||
oidc := newOIDCAuth(options)
|
||||
basic := newBasicAuth(options)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if options.OIDCIss != "" && options.EnableBasicAuth {
|
||||
oidc(basic(next)).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
if options.OIDCIss != "" && !options.EnableBasicAuth {
|
||||
oidc(next).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
if options.OIDCIss == "" && options.EnableBasicAuth {
|
||||
basic(next).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// configureSupportedChallenges adds known authentication challenges to the current session.
|
||||
func configureSupportedChallenges(options Options) {
|
||||
if options.OIDCIss != "" {
|
||||
SupportedAuthStrategies = append(SupportedAuthStrategies, "bearer")
|
||||
}
|
||||
|
||||
if options.EnableBasicAuth {
|
||||
SupportedAuthStrategies = append(SupportedAuthStrategies, "basic")
|
||||
}
|
||||
}
|
||||
|
||||
func writeSupportedAuthenticateHeader(w http.ResponseWriter, r *http.Request) {
|
||||
for i := 0; i < len(SupportedAuthStrategies); i++ {
|
||||
w.Header().Add(WWWAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(SupportedAuthStrategies[i]), r.Host))
|
||||
}
|
||||
}
|
||||
|
||||
func removeSuperfluousAuthenticate(w http.ResponseWriter) {
|
||||
w.Header().Del(WWWAuthenticate)
|
||||
}
|
||||
|
||||
// userAgentAuthenticateLockIn sets Www-Authenticate according to configured user agents. This is useful for the case of
|
||||
// legacy clients that do not support protocols like OIDC or OAuth and want to lock a given user agent to a challenge
|
||||
// such as basic. For more context check https://github.com/cs3org/reva/pull/1350
|
||||
func userAgentAuthenticateLockIn(w http.ResponseWriter, r *http.Request, locks map[string]string, fallback string) {
|
||||
u := userAgentLocker{
|
||||
w: w,
|
||||
r: r,
|
||||
locks: locks,
|
||||
fallback: fallback,
|
||||
}
|
||||
|
||||
for i := 0; i < len(ProxyWwwAuthenticate); i++ {
|
||||
evalRequestURI(&u, i)
|
||||
}
|
||||
}
|
||||
|
||||
func evalRequestURI(l *userAgentLocker, i int) {
|
||||
r := regexp.MustCompile(ProxyWwwAuthenticate[i])
|
||||
if r.Match([]byte(l.r.RequestURI)) {
|
||||
for k, v := range l.locks {
|
||||
if strings.Contains(k, l.r.UserAgent()) {
|
||||
removeSuperfluousAuthenticate(l.w)
|
||||
l.w.Header().Add(WWWAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), l.r.Host))
|
||||
return
|
||||
}
|
||||
}
|
||||
l.w.Header().Add(WWWAuthenticate, fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(l.fallback), l.r.Host))
|
||||
}
|
||||
}
|
||||
|
||||
// newOIDCAuth returns a configured oidc middleware
|
||||
func newOIDCAuth(options Options) func(http.Handler) http.Handler {
|
||||
return OIDCAuth(
|
||||
Logger(options.Logger),
|
||||
OIDCProviderFunc(options.OIDCProviderFunc),
|
||||
HTTPClient(options.HTTPClient),
|
||||
OIDCIss(options.OIDCIss),
|
||||
TokenCacheSize(options.UserinfoCacheSize),
|
||||
TokenCacheTTL(time.Second*time.Duration(options.UserinfoCacheTTL)),
|
||||
CredentialsByUserAgent(options.CredentialsByUserAgent),
|
||||
)
|
||||
}
|
||||
|
||||
// newBasicAuth returns a configured basic middleware
|
||||
func newBasicAuth(options Options) func(http.Handler) http.Handler {
|
||||
return BasicAuth(
|
||||
Logger(options.Logger),
|
||||
EnableBasicAuth(options.EnableBasicAuth),
|
||||
AccountsClient(options.AccountsClient),
|
||||
OIDCIss(options.OIDCIss),
|
||||
CredentialsByUserAgent(options.CredentialsByUserAgent),
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const publicFilesEndpoint = "/remote.php/dav/public-files/"
|
||||
@@ -31,13 +32,36 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, req *http.Request) {
|
||||
if h.isPublicLink(req) || !h.isBasicAuth(req) {
|
||||
if !h.isPublicLink(req) {
|
||||
userAgentAuthenticateLockIn(w, req, options.CredentialsByUserAgent, "basic")
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
removeSuperfluousAuthenticate(w)
|
||||
account, ok := h.getAccount(req)
|
||||
|
||||
// touch is a user agent locking guard, when touched changes to true it indicates the User-Agent on the
|
||||
// request is configured to support only one challenge, it it remains untouched, there are no considera-
|
||||
// tions and we should write all available authentication challenges to the response.
|
||||
touch := false
|
||||
|
||||
if !ok {
|
||||
for k, v := range options.CredentialsByUserAgent {
|
||||
if strings.Contains(k, req.UserAgent()) {
|
||||
removeSuperfluousAuthenticate(w)
|
||||
w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host))
|
||||
touch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// if the request is not bound to any user agent, write all available challenges
|
||||
if !touch {
|
||||
writeSupportedAuthenticateHeader(w, req)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// there is no bearer token on the request,
|
||||
if !h.shouldServe(req) {
|
||||
// oidc supported but token not present, add header and handover to the next middleware.
|
||||
userAgentAuthenticateLockIn(w, req, options.CredentialsByUserAgent, "bearer")
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ type Options struct {
|
||||
UserinfoCacheSize int
|
||||
// UserinfoCacheTTL sets the max cache duration for the userinfo cache, intended for the oidc_auth middleware
|
||||
UserinfoCacheTTL time.Duration
|
||||
// CredentialsByUserAgent sets the auth challenges on a per user-agent basis
|
||||
CredentialsByUserAgent map[string]string
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
@@ -108,6 +110,13 @@ func OIDCIss(iss string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// CredentialsByUserAgent sets UserAgentChallenges.
|
||||
func CredentialsByUserAgent(v map[string]string) Option {
|
||||
return func(o *Options) {
|
||||
o.CredentialsByUserAgent = v
|
||||
}
|
||||
}
|
||||
|
||||
// RevaGatewayClient provides a function to set the the reva gateway service client option.
|
||||
func RevaGatewayClient(gc gateway.GatewayAPIClient) Option {
|
||||
return func(o *Options) {
|
||||
|
||||
@@ -196,6 +196,7 @@ github.com/cs3org/go-cs3apis v0.0.0-20200730121022-c4f3d4f7ddfd/go.mod h1:UXha4T
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20200810113633-b00aca449666/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00 h1:LVl25JaflluOchVvaHWtoCynm5OaM+VNai0IYkcCSe0=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 h1:mZpylrgnCgSeaZ5EznvHIPIKuaQHMHZDi2wkJtk4M8Y=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/reva v1.1.0/go.mod h1:fBzTrNuAKdQ62ybjpdu8nyhBin90/3/3s6DGQDCdBp4=
|
||||
github.com/cs3org/reva v1.2.2-0.20200924071957-e6676516e61e/go.mod h1:DOV5SjpOBKN+aWfOHLdA4KiLQkpyC786PQaXEdRAZ0M=
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cs3org/reva/cmd/revad/runtime"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/micro/cli/v2"
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/ocis-pkg/conversions"
|
||||
"github.com/owncloud/ocis/storage/pkg/config"
|
||||
"github.com/owncloud/ocis/storage/pkg/flagset"
|
||||
"github.com/owncloud/ocis/storage/pkg/server/debug"
|
||||
@@ -25,8 +27,7 @@ func Frontend(cfg *config.Config) *cli.Command {
|
||||
Flags: flagset.FrontendWithConfig(cfg),
|
||||
Before: func(c *cli.Context) error {
|
||||
cfg.Reva.Frontend.Services = c.StringSlice("service")
|
||||
|
||||
return nil
|
||||
return loadUserAgent(c, cfg)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := NewLogger(cfg)
|
||||
@@ -115,6 +116,9 @@ func Frontend(cfg *config.Config) *cli.Command {
|
||||
"cors": map[string]interface{}{
|
||||
"allow_credentials": true,
|
||||
},
|
||||
"auth": map[string]interface{}{
|
||||
"credentials_by_user_agent": cfg.Reva.Frontend.Middleware.Auth.CredentialsByUserAgent,
|
||||
},
|
||||
},
|
||||
// TODO build services dynamically
|
||||
"services": map[string]interface{}{
|
||||
@@ -298,3 +302,28 @@ func Frontend(cfg *config.Config) *cli.Command {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// loadUserAgent reads the user-agent-whitelist-lock-in, since it is a string flag, and attempts to construct a map of
|
||||
// "user-agent":"challenge" locks in for Reva.
|
||||
// Modifies cfg. Spaces don't need to be trimmed as urfavecli takes care of it. User agents with spaces are valid. i.e:
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0
|
||||
// This function works by relying in our format of specifying [user-agent:challenge] and the fact that the user agent
|
||||
// might contain ":" (colon), so the original string is reversed, split in two parts, by the time it is split we
|
||||
// have the indexes reversed and the tuple is in the format of [challenge:user-agent], then the same process is applied
|
||||
// in reverse for each individual part
|
||||
func loadUserAgent(c *cli.Context, cfg *config.Config) error {
|
||||
cfg.Reva.Frontend.Middleware.Auth.CredentialsByUserAgent = make(map[string]string)
|
||||
locks := c.StringSlice("user-agent-whitelist-lock-in")
|
||||
|
||||
for _, v := range locks {
|
||||
vv := conversions.Reverse(v)
|
||||
parts := strings.SplitN(vv, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("unexpected config value for user-agent lock-in: %v, expected format is user-agent:challenge", v)
|
||||
}
|
||||
|
||||
cfg.Reva.Frontend.Middleware.Auth.CredentialsByUserAgent[conversions.Reverse(parts[1])] = conversions.Reverse(parts[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,6 +84,17 @@ type FrontendPort struct {
|
||||
OCSPrefix string
|
||||
OCSSharePrefix string
|
||||
PublicURL string
|
||||
Middleware Middleware
|
||||
}
|
||||
|
||||
// Middleware configures reva middlewares.
|
||||
type Middleware struct {
|
||||
Auth Auth
|
||||
}
|
||||
|
||||
// Auth configures reva http auth middleware.
|
||||
type Auth struct {
|
||||
CredentialsByUserAgent map[string]string
|
||||
}
|
||||
|
||||
// DataGatewayPort has a public url
|
||||
|
||||
@@ -139,6 +139,13 @@ func FrontendWithConfig(cfg *config.Config) []cli.Flag {
|
||||
EnvVars: []string{"STORAGE_FRONTEND_UPLOAD_HTTP_METHOD_OVERRIDE"},
|
||||
Destination: &cfg.Reva.UploadHTTPMethodOverride,
|
||||
},
|
||||
|
||||
// Reva Middlewares Config
|
||||
&cli.StringSliceFlag{
|
||||
Name: "user-agent-whitelist-lock-in",
|
||||
Usage: "--user-agent-whitelist-lock-in=mirall:basic,foo:bearer Given a tuple of comma separated [UserAgent:challenge] values, it locks a given user agent to the authentication challenge. Particularly useful for old clients whose USer-Agent is known and only support one authentication challenge. When this flag is set in the storage-frontend it configures Reva.",
|
||||
EnvVars: []string{"STORAGE_FRONTEND_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT"},
|
||||
},
|
||||
}
|
||||
|
||||
flags = append(flags, TracingWithConfig(cfg)...)
|
||||
|
||||
Reference in New Issue
Block a user