Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+78
View File
@@ -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))
})
}
}
+41
View File
@@ -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
}
+30
View File
@@ -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("")
})
}
}
+102
View File
@@ -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))
})
}
}
+51
View File
@@ -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
}
}
+44
View File
@@ -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)
}
})
}
}
+19
View File
@@ -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)
})
}
}
+21
View File
@@ -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))
})
}
+21
View File
@@ -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)
})
}
}