Initial QSfera import
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
|
||||
|
||||
"github.com/qsfera/server/pkg/account"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/token/manager/jwt"
|
||||
"go-micro.dev/v4/metadata"
|
||||
)
|
||||
|
||||
// newAccountOptions initializes the available default options.
|
||||
func newAccountOptions(opts ...account.Option) account.Options {
|
||||
opt := account.Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// AccountID serves as key for the account uuid in the context
|
||||
const AccountID string = "Account-Id"
|
||||
|
||||
// RoleIDs serves as key for the roles in the context
|
||||
const RoleIDs string = "Role-Ids"
|
||||
|
||||
// ExtractAccountUUID provides a middleware to extract the account uuid from the x-access-token header value
|
||||
// and write it to the context. If there is no x-access-token the middleware is omitted.
|
||||
func ExtractAccountUUID(opts ...account.Option) func(http.Handler) http.Handler {
|
||||
opt := newAccountOptions(opts...)
|
||||
tokenManager, err := jwt.New(map[string]any{
|
||||
"secret": opt.JWTSecret,
|
||||
"expires": int64(24 * 60 * 60),
|
||||
})
|
||||
if err != nil {
|
||||
opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager")
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get(revactx.TokenHeader)
|
||||
if len(token) == 0 {
|
||||
roleIDsJSON, _ := json.Marshal([]string{})
|
||||
ctx := metadata.Set(r.Context(), RoleIDs, string(roleIDsJSON))
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), token)
|
||||
if err != nil {
|
||||
opt.Logger.Error().Err(err)
|
||||
return
|
||||
}
|
||||
if ok, err := scope.VerifyScope(r.Context(), tokenScope, r); err != nil || !ok {
|
||||
opt.Logger.Error().Err(err).Msg("verifying scope failed")
|
||||
return
|
||||
}
|
||||
|
||||
// store user in context for request
|
||||
ctx := revactx.ContextSetUser(r.Context(), u)
|
||||
|
||||
// Important: user.Id.OpaqueId is the AccountUUID. Set this way in the account uuid middleware in КуСфера proxy.
|
||||
// https://github.com/qsfera/server-proxy/blob/ea254d6036592cf9469d757d1295e0c4309d1e63/pkg/middleware/account_uuid.go#L109
|
||||
// TODO: implement token manager in cs3org/reva that uses generic metadata instead of access token from header.
|
||||
ctx = metadata.Set(ctx, AccountID, u.Id.OpaqueId)
|
||||
if u.Opaque != nil {
|
||||
if roles, ok := u.Opaque.Map["roles"]; ok {
|
||||
ctx = metadata.Set(ctx, RoleIDs, string(roles.Value))
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qsfera/server/pkg/cors"
|
||||
|
||||
rscors "github.com/rs/cors"
|
||||
)
|
||||
|
||||
// NoCache writes required cache headers to all requests.
|
||||
func NoCache(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
|
||||
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Cors writes required cors headers to all requests.
|
||||
func Cors(opts ...cors.Option) func(http.Handler) http.Handler {
|
||||
options := cors.NewOptions(opts...)
|
||||
logger := options.Logger
|
||||
logger.Debug().
|
||||
Str("allowed_origins", strings.Join(options.AllowedOrigins, ", ")).
|
||||
Str("allowed_methods", strings.Join(options.AllowedMethods, ", ")).
|
||||
Str("allowed_headers", strings.Join(options.AllowedHeaders, ", ")).
|
||||
Bool("allow_credentials", options.AllowCredentials).
|
||||
Msg("setup cors middleware")
|
||||
c := rscors.New(rscors.Options{
|
||||
AllowedOrigins: options.AllowedOrigins,
|
||||
AllowedMethods: options.AllowedMethods,
|
||||
AllowedHeaders: options.AllowedHeaders,
|
||||
AllowCredentials: options.AllowCredentials,
|
||||
})
|
||||
return c.Handler
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
)
|
||||
|
||||
// Logger is a middleware to log http requests. It uses debug level logging and should be used by all services save the proxy (which uses info level logging).
|
||||
func Logger(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.Debug().
|
||||
Str(log.RequestIDString, r.Header.Get("X-Request-ID")).
|
||||
Str("proto", r.Proto).
|
||||
Str("method", r.Method).
|
||||
Int("status", wrap.Status()).
|
||||
Str("path", r.URL.Path).
|
||||
Dur("duration", time.Since(start)).
|
||||
Int("bytes", wrap.BytesWritten()).
|
||||
Msg("")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
goidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/qsfera/server/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// newOidcOptions initializes the available default options.
|
||||
func newOidcOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// 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 authenticate a bearer auth with an OpenID Connect identity provider
|
||||
// It will put all claims provided by the userinfo endpoint in the context
|
||||
func OidcAuth(opts ...Option) func(http.Handler) http.Handler {
|
||||
opt := newOidcOptions(opts...)
|
||||
|
||||
// TODO use a micro store cache option
|
||||
|
||||
providerFunc := func() (OIDCProvider, error) {
|
||||
// Initialize a provider by specifying the issuer URL.
|
||||
// it will fetch the keys from the issuer using the .well-known
|
||||
// endpoint
|
||||
return goidc.NewProvider(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, &opt.HttpClient),
|
||||
opt.OidcIssuer,
|
||||
)
|
||||
}
|
||||
var provider OIDCProvider
|
||||
initializeProviderLock := sync.Mutex{}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
switch {
|
||||
case strings.HasPrefix(authHeader, "Bearer "):
|
||||
if provider == nil {
|
||||
// lazy initialize provider
|
||||
initializeProviderLock.Lock()
|
||||
var err error
|
||||
// ensure no other request initialized the provider
|
||||
if provider == nil {
|
||||
provider, err = providerFunc()
|
||||
}
|
||||
initializeProviderLock.Unlock()
|
||||
if err != nil {
|
||||
opt.Logger.Error().Err(err).Msg("could not initialize OIDC provider")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
opt.Logger.Debug().Msg("initialized OIDC provider")
|
||||
}
|
||||
|
||||
oauth2Token := &oauth2.Token{
|
||||
AccessToken: strings.TrimPrefix(authHeader, "Bearer "),
|
||||
}
|
||||
|
||||
userInfo, err := provider.UserInfo(
|
||||
context.WithValue(ctx, oauth2.HTTPClient, &opt.HttpClient),
|
||||
oauth2.StaticTokenSource(oauth2Token),
|
||||
)
|
||||
if err != nil {
|
||||
w.Header().Add("WWW-Authenticate", `Bearer`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
claims := map[string]any{}
|
||||
err = userInfo.Claims(&claims)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
ctx = oidc.NewContext(ctx, claims)
|
||||
|
||||
default:
|
||||
// do nothing
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/qsfera/server/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
|
||||
// The OpenID Connect Issuer URL
|
||||
OidcIssuer string
|
||||
// GatewayAPIClient is a reva gateway client
|
||||
GatewayAPIClient gatewayv1beta1.GatewayAPIClient
|
||||
// HttpClient is a http client
|
||||
HttpClient http.Client
|
||||
}
|
||||
|
||||
// WithLogger provides a function to set the openid connect issuer option.
|
||||
func WithOidcIssuer(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.OidcIssuer = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger provides a function to set the logger option.
|
||||
func WithLogger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithGatewayAPIClient provides a function to set the reva gateway client option.
|
||||
func WithGatewayAPIClient(val gatewayv1beta1.GatewayAPIClient) Option {
|
||||
return func(o *Options) {
|
||||
o.GatewayAPIClient = val
|
||||
}
|
||||
}
|
||||
|
||||
// HttpClient provides a function to set the http client option.
|
||||
func WithHttpClient(val http.Client) Option {
|
||||
return func(o *Options) {
|
||||
o.HttpClient = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Static is a middleware that serves static assets.
|
||||
func Static(root string, fs http.FileSystem, ttl int) func(http.Handler) http.Handler {
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root = root + "/"
|
||||
}
|
||||
|
||||
static := http.StripPrefix(
|
||||
root,
|
||||
http.FileServer(
|
||||
fs,
|
||||
),
|
||||
)
|
||||
|
||||
// TODO: investigate broken caching - https://github.com/qsfera/server/issues/1094
|
||||
// we don't have a last modification date of the static assets, so we use the service start date
|
||||
//lastModified := time.Now().UTC().Format(http.TimeFormat)
|
||||
//expires := time.Now().Add(time.Second * time.Duration(ttl)).UTC().Format(http.TimeFormat)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, path.Join(root, "api")) {
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
// TODO: investigate broken caching - https://github.com/qsfera/server/issues/1094
|
||||
//w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s, must-revalidate", strconv.Itoa(ttl)))
|
||||
//w.Header().Set("Expires", expires)
|
||||
//w.Header().Set("Last-Modified", lastModified)
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
|
||||
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
static.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
// Throttle limits the number of concurrent requests.
|
||||
func Throttle(limit int) func(http.Handler) http.Handler {
|
||||
if limit > 0 {
|
||||
return middleware.Throttle(limit)
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
)
|
||||
|
||||
var propagator = propagation.NewCompositeTextMapPropagator(
|
||||
propagation.Baggage{},
|
||||
propagation.TraceContext{},
|
||||
)
|
||||
|
||||
// TraceContext unpacks the request context looking for an existing trace id.
|
||||
func TraceContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
|
||||
propagator.Inject(ctx, propagation.HeaderCarrier(r.Header))
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Version writes the current version to the headers.
|
||||
func Version(name, version string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set(
|
||||
fmt.Sprintf("X-%s-VERSION", strings.ToUpper(name)),
|
||||
version,
|
||||
)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user