rename folder extensions -> services

Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
Christian Richter
2022-06-20 16:46:09 +02:00
parent 1aea93d8cd
commit 78064e6bab
926 changed files with 0 additions and 0 deletions
@@ -0,0 +1,32 @@
package middleware
import (
"net/http"
"time"
"github.com/go-chi/chi/v5/middleware"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// AccessLog is a middleware to log http requests at info level logging.
func AccessLog(logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrap := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
next.ServeHTTP(wrap, r)
logger.Info().
Str("proto", r.Proto).
Str("request", chimiddleware.GetReqID(r.Context())).
Str("remote-addr", r.RemoteAddr).
Str("method", r.Method).
Int("status", wrap.Status()).
Str("path", r.URL.Path).
Dur("duration", time.Since(start)).
Int("bytes", wrap.BytesWritten()).
Msg("access-log")
})
}
}
@@ -0,0 +1,118 @@
package middleware
import (
"errors"
"net/http"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/user/backend"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
)
// AccountResolver provides a middleware which mints a jwt and adds it to the proxied request based
// on the oidc-claims
func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handler {
options := newOptions(optionSetters...)
logger := options.Logger
return func(next http.Handler) http.Handler {
return &accountResolver{
next: next,
logger: logger,
userProvider: options.UserProvider,
userOIDCClaim: options.UserOIDCClaim,
userCS3Claim: options.UserCS3Claim,
autoProvisionAccounts: options.AutoprovisionAccounts,
}
}
}
type accountResolver struct {
next http.Handler
logger log.Logger
userProvider backend.UserBackend
autoProvisionAccounts bool
userOIDCClaim string
userCS3Claim string
}
// TODO do not use the context to store values: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39
func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims := oidc.FromContext(ctx)
user, ok := revactx.ContextGetUser(ctx)
token := ""
// TODO what if an X-Access-Token is set? happens eg for download requests to the /data endpoint in the reva frontend
if claims == nil && !ok {
m.next.ServeHTTP(w, req)
return
}
if user == nil && claims != nil {
var err error
var value string
var ok bool
if value, ok = claims[m.userOIDCClaim].(string); !ok || value == "" {
m.logger.Error().Str("claim", m.userOIDCClaim).Interface("claims", claims).Msg("claim not set or empty")
w.WriteHeader(http.StatusInternalServerError) // admin needs to make the idp send the right claim
return
}
user, token, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value, true)
if errors.Is(err, backend.ErrAccountNotFound) {
m.logger.Debug().Str("claim", m.userOIDCClaim).Str("value", value).Msg("User by claim not found")
if !m.autoProvisionAccounts {
m.logger.Debug().Interface("claims", claims).Msg("Autoprovisioning disabled")
w.WriteHeader(http.StatusUnauthorized)
return
}
m.logger.Debug().Interface("claims", claims).Msg("Autoprovisioning user")
user, err = m.userProvider.CreateUserFromClaims(req.Context(), claims)
if err != nil {
m.logger.Error().Err(err).Msg("Autoprovisioning user failed")
}
}
if errors.Is(err, backend.ErrAccountDisabled) {
m.logger.Debug().Interface("claims", claims).Msg("Disabled")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err != nil {
m.logger.Error().Err(err).Msg("Could not get user by claim")
w.WriteHeader(http.StatusInternalServerError)
return
}
// add user to context for selectors
ctx = revactx.ContextSetUser(ctx, user)
req = req.WithContext(ctx)
m.logger.Debug().Interface("claims", claims).Interface("user", user).Msg("associated claims with user")
} else if user != nil {
var err error
_, token, err = m.userProvider.GetUserByClaims(req.Context(), "username", user.Username, true)
if errors.Is(err, backend.ErrAccountDisabled) {
m.logger.Debug().Interface("user", user).Msg("Disabled")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err != nil {
m.logger.Error().Err(err).Msg("Could not get user by claim")
w.WriteHeader(http.StatusInternalServerError)
return
}
}
req.Header.Set(revactx.TokenHeader, token)
m.next.ServeHTTP(w, req)
}
@@ -0,0 +1,152 @@
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/v2/pkg/auth/scope"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/user/backend/test"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
"github.com/stretchr/testify/assert"
)
func TestTokenIsAddedWithMailClaim(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, oidc.Email, "mail")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.Email: "foo@example.com",
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.NotEmpty(t, token)
assert.Contains(t, token, "eyJ")
}
func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil, oidc.PreferredUsername, "username")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.NotEmpty(t, token)
assert.Contains(t, token, "eyJ")
}
func TestNSkipOnNoClaims(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.Email, "mail")
req, rw := mockRequest(nil)
sut.ServeHTTP(rw, req)
token := req.Header.Get("x-access-token")
assert.Empty(t, token)
assert.Equal(t, http.StatusOK, rw.Code)
}
func TestUnauthorizedOnUserNotFound(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.PreferredUsername, "username")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.Empty(t, token)
assert.Equal(t, http.StatusUnauthorized, rw.Code)
}
func TestUnauthorizedOnUserDisabled(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.PreferredUsername, "username")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.Empty(t, token)
assert.Equal(t, http.StatusUnauthorized, rw.Code)
}
func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.Email, "mail")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
})
sut.ServeHTTP(rw, req)
token := req.Header.Get(revactx.TokenHeader)
assert.Empty(t, token)
assert.Equal(t, http.StatusInternalServerError, rw.Code)
}
func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr error, oidcclaim, cs3claim string) http.Handler {
tokenManager, _ := jwt.New(map[string]interface{}{
"secret": "change-me",
"expires": int64(60),
})
token := ""
if userBackendResult != nil {
s, _ := scope.AddOwnerScope(nil)
token, _ = tokenManager.MintToken(context.Background(), userBackendResult, s)
}
mock := &test.UserBackendMock{
GetUserByClaimsFunc: func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, string, error) {
return userBackendResult, token, userBackendErr
},
}
return AccountResolver(
Logger(log.NewLogger()),
UserProvider(mock),
TokenManagerConfig(config.TokenManager{JWTSecret: "secret"}),
UserOIDCClaim(oidcclaim),
UserCS3Claim(cs3claim),
AutoprovisionAccounts(false),
)(mockHandler{})
}
func mockRequest(claims map[string]interface{}) (*http.Request, *httptest.ResponseRecorder) {
if claims == nil {
return httptest.NewRequest("GET", "http://example.com/foo", nil), httptest.NewRecorder()
}
ctx := oidc.NewContext(context.Background(), claims)
req := httptest.NewRequest("GET", "http://example.com/foo", nil).WithContext(ctx)
rw := httptest.NewRecorder()
return req, rw
}
type mockHandler struct{}
func (m mockHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {}
@@ -0,0 +1,132 @@
package middleware
import (
"fmt"
"net/http"
"regexp"
"strings"
)
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(options.UserinfoCacheTTL),
CredentialsByUserAgent(options.CredentialsByUserAgent),
)
}
// newBasicAuth returns a configured basic middleware
func newBasicAuth(options Options) func(http.Handler) http.Handler {
return BasicAuth(
UserProvider(options.UserProvider),
Logger(options.Logger),
EnableBasicAuth(options.EnableBasicAuth),
OIDCIss(options.OIDCIss),
UserOIDCClaim(options.UserOIDCClaim),
UserCS3Claim(options.UserCS3Claim),
CredentialsByUserAgent(options.CredentialsByUserAgent),
)
}
+145
View File
@@ -0,0 +1,145 @@
package middleware
import (
"fmt"
"net/http"
"strings"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/webdav"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
)
// BasicAuth provides a middleware to check if BasicAuth is provided
func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
options := newOptions(optionSetters...)
logger := options.Logger
if options.EnableBasicAuth {
options.Logger.Warn().Msg("basic auth enabled, use only for testing or development")
}
h := basicAuth{
logger: logger,
enabled: options.EnableBasicAuth,
userProvider: options.UserProvider,
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, req *http.Request) {
if h.isPublicLink(req) || !h.isBasicAuth(req) || h.isOIDCTokenAuth(req) {
if !h.isPublicLink(req) {
userAgentAuthenticateLockIn(w, req, options.CredentialsByUserAgent, "basic")
}
next.ServeHTTP(w, req)
return
}
removeSuperfluousAuthenticate(w)
login, password, _ := req.BasicAuth()
user, _, err := h.userProvider.Authenticate(req.Context(), login, password)
// 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 err != nil {
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)
}
// if the request is a PROPFIND return a WebDAV error code.
// TODO: The proxy has to be smart enough to detect when a request is directed towards a webdav server
// and react accordingly.
w.WriteHeader(http.StatusUnauthorized)
if webdav.IsWebdavRequest(req) {
b, err := webdav.Marshal(webdav.Exception{
Code: webdav.SabredavPermissionDenied,
Message: "Authentication error",
})
webdav.HandleWebdavError(w, b, err)
return
}
return
}
// fake oidc claims
claims := map[string]interface{}{
oidc.Iss: user.Id.Idp,
oidc.PreferredUsername: user.Username,
oidc.Email: user.Mail,
oidc.OwncloudUUID: user.Id.OpaqueId,
}
if options.UserCS3Claim == "userid" {
// set the custom user claim only if users will be looked up by the userid on the CS3api
// OpaqueId contains the userid configured in STORAGE_LDAP_USER_SCHEMA_UID
claims[options.UserOIDCClaim] = user.Id.OpaqueId
}
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims)))
},
)
}
}
type basicAuth struct {
logger log.Logger
enabled bool
userProvider backend.UserBackend
}
func (m basicAuth) isPublicLink(req *http.Request) bool {
login, _, ok := req.BasicAuth()
if !ok || login != "public" {
return false
}
publicPaths := []string{
"/dav/public-files/",
"/remote.php/dav/public-files/",
"/remote.php/ocs/apps/files_sharing/api/v1/tokeninfo/unprotected",
"/ocs/v1.php/cloud/capabilities",
"/data",
}
isPublic := false
for _, p := range publicPaths {
if strings.HasPrefix(req.URL.Path, p) {
isPublic = true
break
}
}
return isPublic
}
// The token auth endpoint uses basic auth for clients, see https://openid.net/specs/openid-connect-basic-1_0.html#TokenRequest
// > The Client MUST authenticate to the Token Endpoint using the HTTP Basic method, as described in 2.3.1 of OAuth 2.0.
func (m basicAuth) isOIDCTokenAuth(req *http.Request) bool {
return req.URL.Path == "/konnect/v1/token"
}
func (m basicAuth) isBasicAuth(req *http.Request) bool {
_, _, ok := req.BasicAuth()
return m.enabled && ok
}
@@ -0,0 +1,40 @@
package middleware
import (
"net/http/httptest"
"testing"
)
/**/
func TestBasicAuth__isPublicLink(t *testing.T) {
tests := []struct {
url string
username string
expected bool
}{
{url: "/remote.php/dav/public-files/", username: "", expected: false},
{url: "/remote.php/dav/public-files/", username: "abc", expected: false},
{url: "/remote.php/dav/public-files/", username: "private", expected: false},
{url: "/remote.php/dav/public-files/", username: "public", expected: true},
{url: "/ocs/v1.php/cloud/capabilities", username: "", expected: false},
{url: "/ocs/v1.php/cloud/capabilities", username: "abc", expected: false},
{url: "/ocs/v1.php/cloud/capabilities", username: "private", expected: false},
{url: "/ocs/v1.php/cloud/capabilities", username: "public", expected: true},
{url: "/ocs/v1.php/cloud/users/admin", username: "public", expected: false},
}
ba := basicAuth{}
for _, tt := range tests {
req := httptest.NewRequest("", tt.url, nil)
if tt.username != "" {
req.SetBasicAuth(tt.username, "")
}
result := ba.isPublicLink(req)
if result != tt.expected {
t.Errorf("with %s expected %t got %t", tt.url, tt.expected, result)
}
}
}
@@ -0,0 +1,64 @@
package middleware
import (
"net/http"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"google.golang.org/grpc/metadata"
)
// CreateHome provides a middleware which sends a CreateHome request to the reva gateway
func CreateHome(optionSetters ...Option) func(next http.Handler) http.Handler {
options := newOptions(optionSetters...)
logger := options.Logger
return func(next http.Handler) http.Handler {
return &createHome{
next: next,
logger: logger,
revaGatewayClient: options.RevaGatewayClient,
}
}
}
type createHome struct {
next http.Handler
logger log.Logger
revaGatewayClient gateway.GatewayAPIClient
}
func (m createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if !m.shouldServe(req) {
m.next.ServeHTTP(w, req)
return
}
token := req.Header.Get("x-access-token")
// we need to pass the token to authenticate the CreateHome request.
//ctx := tokenpkg.ContextSetToken(r.Context(), token)
ctx := metadata.AppendToOutgoingContext(req.Context(), revactx.TokenHeader, token)
createHomeReq := &provider.CreateHomeRequest{}
createHomeRes, err := m.revaGatewayClient.CreateHome(ctx, createHomeReq)
if err != nil {
m.logger.Err(err).Msg("error calling CreateHome")
} else if createHomeRes.Status.Code != rpc.Code_CODE_OK {
err := status.NewErrorFromCode(createHomeRes.Status.Code, "gateway")
if createHomeRes.Status.Code != rpc.Code_CODE_ALREADY_EXISTS {
m.logger.Err(err).Msg("error when calling Createhome")
}
}
m.next.ServeHTTP(w, req)
}
func (m createHome) shouldServe(req *http.Request) bool {
return req.Header.Get("x-access-token") != ""
}
@@ -0,0 +1,19 @@
package middleware
import (
"fmt"
"net/http"
)
// HTTPSRedirect redirects insecure requests to https
func HTTPSRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
proto := req.Header.Get("x-forwarded-proto")
if proto == "http" || proto == "HTTP" {
http.Redirect(res, req, fmt.Sprintf("https://%s%s", req.Host, req.URL), http.StatusPermanentRedirect)
return
}
next.ServeHTTP(res, req)
})
}
+178
View File
@@ -0,0 +1,178 @@
package middleware
import (
"context"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
gOidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
"github.com/owncloud/ocis/v2/ocis-pkg/sync"
"golang.org/x/oauth2"
)
// OIDCProvider used to mock the oidc provider during tests
type OIDCProvider interface {
UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error)
}
// OIDCAuth provides a middleware to check access secured by a static token.
func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
options := newOptions(optionSetters...)
tokenCache := sync.NewCache(options.UserinfoCacheSize)
h := oidcAuth{
logger: options.Logger,
providerFunc: options.OIDCProviderFunc,
httpClient: options.HTTPClient,
oidcIss: options.OIDCIss,
TokenManagerConfig: options.TokenManagerConfig,
tokenCache: &tokenCache,
tokenCacheTTL: options.UserinfoCacheTTL,
}
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
}
if h.getProvider() == nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
token := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ")
claims, status := h.getClaims(token, req)
if status != 0 {
w.WriteHeader(status)
return
}
// inject claims to the request context for the account_uuid middleware.
req = req.WithContext(oidc.NewContext(req.Context(), claims))
// store claims in context
// uses the original context, not the one with probably reduced security
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims)))
})
}
}
type oidcAuth struct {
logger log.Logger
provider OIDCProvider
providerFunc func() (OIDCProvider, error)
httpClient *http.Client
oidcIss string
tokenCache *sync.Cache
tokenCacheTTL time.Duration
TokenManagerConfig config.TokenManager
}
func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string]interface{}, status int) {
hit := m.tokenCache.Load(token)
if hit == nil {
// TODO cache userinfo for access token if we can determine the expiry (which works in case it is a jwt based access token)
oauth2Token := &oauth2.Token{
AccessToken: token,
}
userInfo, err := m.getProvider().UserInfo(
context.WithValue(req.Context(), oauth2.HTTPClient, m.httpClient),
oauth2.StaticTokenSource(oauth2Token),
)
if err != nil {
m.logger.Error().Err(err).Msg("Failed to get userinfo")
status = http.StatusUnauthorized
return
}
if err := userInfo.Claims(&claims); err != nil {
m.logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims")
status = http.StatusInternalServerError
return
}
expiration := m.extractExpiration(token)
m.tokenCache.Store(token, claims, expiration)
m.logger.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Time("expiration", expiration.UTC()).Msg("unmarshalled and cached userinfo")
return
}
var ok bool
if claims, ok = hit.V.(map[string]interface{}); !ok {
status = http.StatusInternalServerError
return
}
m.logger.Debug().Interface("claims", claims).Msg("cache hit for userinfo")
return
}
// extractExpiration tries to parse and extract the expiration from the provided token. It might not even be a jwt.
// defaults to the configured fallback TTL.
// TODO: use introspection endpoint if available in the oidc configuration. Still needs a fallback to configured TTL.
func (m oidcAuth) extractExpiration(token string) time.Time {
defaultExpiration := time.Now().Add(m.tokenCacheTTL)
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return []byte(m.TokenManagerConfig.JWTSecret), nil
})
if err != nil {
return defaultExpiration
}
at, ok := t.Claims.(jwt.StandardClaims)
if !ok || at.ExpiresAt == 0 {
return defaultExpiration
}
return time.Unix(at.ExpiresAt, 0)
}
func (m oidcAuth) shouldServe(req *http.Request) bool {
header := req.Header.Get("Authorization")
if m.oidcIss == "" {
return false
}
// todo: looks dirty, check later
// TODO: make a PR to coreos/go-oidc for exposing userinfo endpoint on provider, see https://github.com/coreos/go-oidc/issues/248
for _, ignoringPath := range []string{"/konnect/v1/userinfo", "/status.php"} {
if req.URL.Path == ignoringPath {
return false
}
}
return strings.HasPrefix(header, "Bearer ")
}
func (m *oidcAuth) getProvider() OIDCProvider {
if m.provider == nil {
// Lazily initialize a provider
// provider needs to be cached as when it is created
// it will fetch the keys from the issuer using the .well-known
// endpoint
provider, err := m.providerFunc()
if err != nil {
m.logger.Error().Err(err).Msg("could not initialize oidcAuth provider")
return nil
}
m.provider = provider
}
return m.provider
}
@@ -0,0 +1,67 @@
package middleware
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"golang.org/x/oauth2"
)
func TestOIDCAuthMiddleware(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
m := OIDCAuth(
Logger(log.NewLogger()),
OIDCProviderFunc(func() (OIDCProvider, error) {
return mockOP(false), nil
}),
OIDCIss("https://localhost:9200"),
)(next)
r := httptest.NewRequest(http.MethodGet, "https://idp.example.com", nil)
r.Header.Set("Authorization", "Bearer sometoken")
w := httptest.NewRecorder()
m.ServeHTTP(w, r)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected an internal server error")
}
}
type mockOIDCProvider struct {
UserInfoFunc func(ctx context.Context, ts oauth2.TokenSource) (*oidc.UserInfo, error)
}
// UserInfo will panic if the function has been called, but not mocked
func (m mockOIDCProvider) UserInfo(ctx context.Context, ts oauth2.TokenSource) (*oidc.UserInfo, error) {
if m.UserInfoFunc != nil {
return m.UserInfoFunc(ctx, ts)
}
panic("UserInfo was called in test but not mocked")
}
func mockOP(retErr bool) OIDCProvider {
if retErr {
return &mockOIDCProvider{
UserInfoFunc: func(ctx context.Context, ts oauth2.TokenSource) (*oidc.UserInfo, error) {
return nil, fmt.Errorf("error returned by mockOIDCProvider UserInfo")
},
}
}
return &mockOIDCProvider{
UserInfoFunc: func(ctx context.Context, ts oauth2.TokenSource) (*oidc.UserInfo, error) {
ui := &oidc.UserInfo{
// claims: private ...
}
return ui, nil
},
}
}
+195
View File
@@ -0,0 +1,195 @@
package middleware
import (
"net/http"
"time"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/user/backend"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
// Logger to use for logging, must be set
Logger log.Logger
// TokenManagerConfig for communicating with the reva token manager
TokenManagerConfig config.TokenManager
// PolicySelectorConfig for using the policy selector
PolicySelector config.PolicySelector
// HTTPClient to use for communication with the oidcAuth provider
HTTPClient *http.Client
// UP
UserProvider backend.UserBackend
// SettingsRoleService for the roles API in settings
SettingsRoleService settingssvc.RoleService
// OIDCProviderFunc to lazily initialize an oidc provider, must be set for the oidc_auth middleware
OIDCProviderFunc func() (OIDCProvider, error)
// OIDCIss is the oidcAuth-issuer
OIDCIss string
// RevaGatewayClient to send requests to the reva gateway
RevaGatewayClient gateway.GatewayAPIClient
// Store for persisting data
Store storesvc.StoreService
// PreSignedURLConfig to configure the middleware
PreSignedURLConfig config.PreSignedURL
// UserOIDCClaim to read from the oidc claims
UserOIDCClaim string
// UserCS3Claim to use when looking up a user in the CS3 API
UserCS3Claim string
// AutoprovisionAccounts when an accountResolver does not exist.
AutoprovisionAccounts bool
// EnableBasicAuth to allow basic auth
EnableBasicAuth bool
// UserinfoCacheSize defines the max number of entries in the userinfo cache, intended for the oidc_auth middleware
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.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(l log.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// TokenManagerConfig provides a function to set the token manger config option.
func TokenManagerConfig(cfg config.TokenManager) Option {
return func(o *Options) {
o.TokenManagerConfig = cfg
}
}
// PolicySelectorConfig provides a function to set the policy selector config option.
func PolicySelectorConfig(cfg config.PolicySelector) Option {
return func(o *Options) {
o.PolicySelector = cfg
}
}
// HTTPClient provides a function to set the http client config option.
func HTTPClient(c *http.Client) Option {
return func(o *Options) {
o.HTTPClient = c
}
}
// SettingsRoleService provides a function to set the role service option.
func SettingsRoleService(rc settingssvc.RoleService) Option {
return func(o *Options) {
o.SettingsRoleService = rc
}
}
// OIDCProviderFunc provides a function to set the the oidc provider function option.
func OIDCProviderFunc(f func() (OIDCProvider, error)) Option {
return func(o *Options) {
o.OIDCProviderFunc = f
}
}
// OIDCIss sets the oidcAuth issuer url
func OIDCIss(iss string) Option {
return func(o *Options) {
o.OIDCIss = iss
}
}
// 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) {
o.RevaGatewayClient = gc
}
}
// Store provides a function to set the store option.
func Store(sc storesvc.StoreService) Option {
return func(o *Options) {
o.Store = sc
}
}
// PreSignedURLConfig provides a function to set the PreSignedURL config
func PreSignedURLConfig(cfg config.PreSignedURL) Option {
return func(o *Options) {
o.PreSignedURLConfig = cfg
}
}
// UserOIDCClaim provides a function to set the UserClaim config
func UserOIDCClaim(val string) Option {
return func(o *Options) {
o.UserOIDCClaim = val
}
}
// UserCS3Claim provides a function to set the UserClaimType config
func UserCS3Claim(val string) Option {
return func(o *Options) {
o.UserCS3Claim = val
}
}
// AutoprovisionAccounts provides a function to set the AutoprovisionAccounts config
func AutoprovisionAccounts(val bool) Option {
return func(o *Options) {
o.AutoprovisionAccounts = val
}
}
// EnableBasicAuth provides a function to set the EnableBasicAuth config
func EnableBasicAuth(enableBasicAuth bool) Option {
return func(o *Options) {
o.EnableBasicAuth = enableBasicAuth
}
}
// TokenCacheSize provides a function to set the TokenCacheSize
func TokenCacheSize(size int) Option {
return func(o *Options) {
o.UserinfoCacheSize = size
}
}
// TokenCacheTTL provides a function to set the TokenCacheTTL
func TokenCacheTTL(ttl time.Duration) Option {
return func(o *Options) {
o.UserinfoCacheTTL = ttl
}
}
// UserProvider sets the accounts user provider
func UserProvider(up backend.UserBackend) Option {
return func(o *Options) {
o.UserProvider = up
}
}
@@ -0,0 +1,72 @@
package middleware
import (
"net/http"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
)
const (
headerRevaAccessToken = "x-access-token"
headerShareToken = "public-token"
basicAuthPasswordPrefix = "password|"
authenticationType = "publicshares"
)
// PublicShareAuth ...
func PublicShareAuth(opts ...Option) func(next http.Handler) http.Handler {
options := newOptions(opts...)
logger := options.Logger
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
shareToken := r.Header.Get(headerShareToken)
if shareToken == "" {
shareToken = r.URL.Query().Get(headerShareToken)
}
// Currently we only want to authenticate app open request coming from public shares.
if shareToken == "" {
// Don't authenticate
next.ServeHTTP(w, r)
return
}
var sharePassword string
if signature := r.URL.Query().Get("signature"); signature != "" {
expiration := r.URL.Query().Get("expiration")
if expiration == "" {
logger.Warn().Str("signature", signature).Msg("cannot do signature auth without the expiration")
next.ServeHTTP(w, r)
return
}
sharePassword = strings.Join([]string{"signature", signature, expiration}, "|")
} else {
// We can ignore the username since it is always set to "public" in public shares.
_, password, ok := r.BasicAuth()
sharePassword = basicAuthPasswordPrefix
if ok {
sharePassword += password
}
}
authResp, err := options.RevaGatewayClient.Authenticate(r.Context(), &gateway.AuthenticateRequest{
Type: authenticationType,
ClientId: shareToken,
ClientSecret: sharePassword,
})
if err != nil {
logger.Debug().Err(err).Str("public_share_token", shareToken).Msg("could not authenticate public share")
// try another middleware
next.ServeHTTP(w, r)
return
}
r.Header.Add(headerRevaAccessToken, authResp.Token)
next.ServeHTTP(w, r)
})
}
}
@@ -0,0 +1,69 @@
package middleware
import (
"net/http"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/proxy/policy"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
)
// SelectorCookie provides a middleware which
func SelectorCookie(optionSetters ...Option) func(next http.Handler) http.Handler {
options := newOptions(optionSetters...)
logger := options.Logger
policySelector := options.PolicySelector
return func(next http.Handler) http.Handler {
return &selectorCookie{
next: next,
logger: logger,
policySelector: policySelector,
}
}
}
type selectorCookie struct {
next http.Handler
logger log.Logger
policySelector config.PolicySelector
}
func (m selectorCookie) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if m.policySelector.Regex == nil && m.policySelector.Claims == nil {
// only set selector cookie for regex and claim selectors
m.next.ServeHTTP(w, req)
return
}
selectorCookieName := ""
if m.policySelector.Regex != nil {
selectorCookieName = m.policySelector.Regex.SelectorCookieName
} else if m.policySelector.Claims != nil {
selectorCookieName = m.policySelector.Claims.SelectorCookieName
}
// update cookie
if oidc.FromContext(req.Context()) != nil {
selectorFunc, err := policy.LoadSelector(&m.policySelector)
if err != nil {
m.logger.Err(err)
}
selector, err := selectorFunc(req)
if err != nil {
m.logger.Err(err)
}
cookie := http.Cookie{
Name: selectorCookieName,
Value: selector,
Path: "/",
}
http.SetCookie(w, &cookie)
}
m.next.ServeHTTP(w, req)
}
@@ -0,0 +1,213 @@
package middleware
import (
"context"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
storemsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/store/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
"golang.org/x/crypto/pbkdf2"
)
// SignedURLAuth provides a middleware to check access secured by a signed URL.
func SignedURLAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
options := newOptions(optionSetters...)
return func(next http.Handler) http.Handler {
return &signedURLAuth{
next: next,
logger: options.Logger,
preSignedURLConfig: options.PreSignedURLConfig,
store: options.Store,
userProvider: options.UserProvider,
}
}
}
type signedURLAuth struct {
next http.Handler
logger log.Logger
preSignedURLConfig config.PreSignedURL
userProvider backend.UserBackend
store storesvc.StoreService
}
func (m signedURLAuth) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if !m.shouldServe(req) {
m.next.ServeHTTP(w, req)
return
}
user, _, err := m.userProvider.GetUserByClaims(req.Context(), "username", req.URL.Query().Get("OC-Credential"), true)
if err != nil {
m.logger.Error().Err(err).Msg("Could not get user by claim")
w.WriteHeader(http.StatusInternalServerError)
}
ctx := revactx.ContextSetUser(req.Context(), user)
req = req.WithContext(ctx)
if err := m.validate(req); err != nil {
http.Error(w, "Invalid url signature", http.StatusUnauthorized)
return
}
m.next.ServeHTTP(w, req)
}
func (m signedURLAuth) shouldServe(req *http.Request) bool {
if !m.preSignedURLConfig.Enabled {
return false
}
return req.URL.Query().Get("OC-Signature") != ""
}
func (m signedURLAuth) validate(req *http.Request) (err error) {
query := req.URL.Query()
if ok, err := m.allRequiredParametersArePresent(query); !ok {
return err
}
if ok, err := m.requestMethodMatches(req.Method, query); !ok {
return err
}
if ok, err := m.requestMethodIsAllowed(req.Method); !ok {
return err
}
if expired, err := m.urlIsExpired(query, time.Now); expired {
return err
}
if ok, err := m.signatureIsValid(req); !ok {
return err
}
return nil
}
func (m signedURLAuth) allRequiredParametersArePresent(query url.Values) (ok bool, err error) {
// check if required query parameters exist in given request query parameters
// OC-Signature - the computed signature - server will verify the request upon this REQUIRED
// OC-Credential - defines the user scope (shall we use the owncloud user id here - this might leak internal data ....) REQUIRED
// OC-Date - defined the date the url was signed (ISO 8601 UTC) REQUIRED
// OC-Expires - defines the expiry interval in seconds (between 1 and 604800 = 7 days) REQUIRED
// TODO OC-Verb - defines for which http verb the request is valid - defaults to GET OPTIONAL
for _, p := range []string{
"OC-Signature",
"OC-Credential",
"OC-Date",
"OC-Expires",
"OC-Verb",
} {
if query.Get(p) == "" {
return false, fmt.Errorf("required %s parameter not found", p)
}
}
return true, nil
}
func (m signedURLAuth) requestMethodMatches(meth string, query url.Values) (ok bool, err error) {
// check if given url query parameter OC-Verb matches given request method
if !strings.EqualFold(meth, query.Get("OC-Verb")) {
return false, errors.New("required OC-Verb parameter did not match request method")
}
return true, nil
}
func (m signedURLAuth) requestMethodIsAllowed(meth string) (ok bool, err error) {
// check if given request method is allowed
methodIsAllowed := false
for _, am := range m.preSignedURLConfig.AllowedHTTPMethods {
if strings.EqualFold(meth, am) {
methodIsAllowed = true
break
}
}
if !methodIsAllowed {
return false, errors.New("request method is not listed in PreSignedURLConfig AllowedHTTPMethods")
}
return true, nil
}
func (m signedURLAuth) urlIsExpired(query url.Values, now func() time.Time) (expired bool, err error) {
// check if url is expired by checking if given date (OC-Date) + expires in seconds (OC-Expires) is after now
validFrom, err := time.Parse(time.RFC3339, query.Get("OC-Date"))
if err != nil {
return true, err
}
requestExpiry, err := time.ParseDuration(query.Get("OC-Expires") + "s")
if err != nil {
return true, err
}
validTo := validFrom.Add(requestExpiry)
return !(now().After(validFrom) && now().Before(validTo)), nil
}
func (m signedURLAuth) signatureIsValid(req *http.Request) (ok bool, err error) {
u := revactx.ContextMustGetUser(req.Context())
signingKey, err := m.getSigningKey(req.Context(), u.Id.OpaqueId)
if err != nil {
m.logger.Error().Err(err).Msg("could not retrieve signing key")
return false, err
}
if len(signingKey) == 0 {
m.logger.Error().Err(err).Msg("signing key empty")
return false, err
}
q := req.URL.Query()
signature := q.Get("OC-Signature")
q.Del("OC-Signature")
req.URL.RawQuery = q.Encode()
url := req.URL.String()
if !req.URL.IsAbs() {
url = "https://" + req.Host + url // TODO where do we get the scheme from
}
return m.createSignature(url, signingKey) == signature, nil
}
func (m signedURLAuth) createSignature(url string, signingKey []byte) string {
// the oc10 signature check: $hash = \hash_pbkdf2("sha512", $url, $signingKey, 10000, 64, false);
// - sets the length of the output string to 64
// - sets raw output to false -> if raw_output is FALSE length corresponds to twice the byte-length of the derived key (as every byte of the key is returned as two hexits).
// TODO change to length 128 in oc10?
// fo golangs pbkdf2.Key we need to use 32 because it will be encoded into 64 hexits later
hash := pbkdf2.Key([]byte(url), signingKey, 10000, 32, sha512.New)
return hex.EncodeToString(hash)
}
func (m signedURLAuth) getSigningKey(ctx context.Context, ocisID string) ([]byte, error) {
res, err := m.store.Read(ctx, &storesvc.ReadRequest{
Options: &storemsg.ReadOptions{
Database: "proxy",
Table: "signing-keys",
},
Key: ocisID,
})
if err != nil || len(res.Records) < 1 {
return []byte{}, err
}
return res.Records[0].Value, nil
}
@@ -0,0 +1,136 @@
package middleware
import (
"net/http/httptest"
"testing"
"time"
)
func TestSignedURLAuth_shouldServe(t *testing.T) {
pua := signedURLAuth{}
tests := []struct {
url string
enabled bool
expected bool
}{
{"https://example.com/example.jpg", true, false},
{"https://example.com/example.jpg?OC-Signature=something", true, true},
{"https://example.com/example.jpg", false, false},
{"https://example.com/example.jpg?OC-Signature=something", false, false},
}
for _, tt := range tests {
pua.preSignedURLConfig.Enabled = tt.enabled
r := httptest.NewRequest("", tt.url, nil)
result := pua.shouldServe(r)
if result != tt.expected {
t.Errorf("with %s expected %t got %t", tt.url, tt.expected, result)
}
}
}
func TestSignedURLAuth_allRequiredParametersPresent(t *testing.T) {
pua := signedURLAuth{}
baseURL := "https://example.com/example.jpg?"
tests := []struct {
params string
expected bool
}{
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Expires=something&OC-Verb=something", true},
{"OC-Credential=something&OC-Date=something&OC-Expires=something&OC-Verb=something", false},
{"OC-Signature=something&OC-Date=something&OC-Expires=something&OC-Verb=something", false},
{"OC-Signature=something&OC-Credential=something&OC-Expires=something&OC-Verb=something", false},
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Verb=something", false},
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Expires=something", false},
}
for _, tt := range tests {
r := httptest.NewRequest("", baseURL+tt.params, nil)
ok, _ := pua.allRequiredParametersArePresent(r.URL.Query())
if ok != tt.expected {
t.Errorf("with %s expected %t got %t", tt.params, tt.expected, ok)
}
}
}
func TestSignedURLAuth_requestMethodMatches(t *testing.T) {
pua := signedURLAuth{}
tests := []struct {
method string
url string
expected bool
}{
{"GET", "https://example.com/example.jpg?OC-Verb=GET", true},
{"GET", "https://example.com/example.jpg?OC-Verb=get", true},
{"POST", "https://example.com/example.jpg?OC-Verb=GET", false},
}
for _, tt := range tests {
r := httptest.NewRequest(tt.method, tt.url, nil)
ok, _ := pua.requestMethodMatches(r.Method, r.URL.Query())
if ok != tt.expected {
t.Errorf("with method %s and url %s expected %t got %t", tt.method, tt.url, tt.expected, ok)
}
}
}
func TestSignedURLAuth_requestMethodIsAllowed(t *testing.T) {
pua := signedURLAuth{}
tests := []struct {
method string
allowed []string
expected bool
}{
{"GET", []string{}, false},
{"GET", []string{"POST"}, false},
{"GET", []string{"GET"}, true},
{"GET", []string{"get"}, true},
{"GET", []string{"POST", "GET"}, true},
}
for _, tt := range tests {
pua.preSignedURLConfig.AllowedHTTPMethods = tt.allowed
ok, _ := pua.requestMethodIsAllowed(tt.method)
if ok != tt.expected {
t.Errorf("with method %s and allowed methods %s expected %t got %t", tt.method, tt.allowed, tt.expected, ok)
}
}
}
func TestSignedURLAuth_urlIsExpired(t *testing.T) {
pua := signedURLAuth{}
nowFunc := func() time.Time {
t, _ := time.Parse(time.RFC3339, "2020-02-02T12:30:00.000Z")
return t
}
tests := []struct {
url string
isExpired bool
}{
{"http://example.com/example.jpg?OC-Date=2020-02-02T12:29:00.000Z&OC-Expires=61", false},
{"http://example.com/example.jpg?OC-Date=2020-02-02T12:29:00.000Z&OC-Expires=invalid", true},
{"http://example.com/example.jpg?OC-Date=2020-02-02T12:29:00.000Z&OC-Expires=59", true},
{"http://example.com/example.jpg?OC-Date=2020-02-03T12:29:00.000Z&OC-Expires=59", true},
{"http://example.com/example.jpg?OC-Date=2020-02-01T12:29:00.000Z&OC-Expires=59", true},
}
for _, tt := range tests {
r := httptest.NewRequest("", tt.url, nil)
expired, _ := pua.urlIsExpired(r.URL.Query(), nowFunc)
if expired != tt.isExpired {
t.Errorf("with %s expected %t got %t", tt.url, tt.isExpired, expired)
}
}
}
func TestSignedURLAuth_createSignature(t *testing.T) {
pua := signedURLAuth{}
expected := "27d2ebea381384af3179235114801dcd00f91e46f99fca72575301cf3948101d"
s := pua.createSignature("something", []byte("somerandomkey"))
if s != expected {
t.Fail()
}
}