feat(backend): analytics - common auth middleware with extra secret to support spot like service
This commit is contained in:
parent
417b9e59a8
commit
1578b891bd
10 changed files with 118 additions and 71 deletions
|
|
@ -2,9 +2,12 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"openreplay/backend/internal/http/server"
|
||||
"openreplay/backend/pkg/analytics/api"
|
||||
"openreplay/backend/pkg/common"
|
||||
"openreplay/backend/pkg/common/api/auth"
|
||||
"openreplay/backend/pkg/common/middleware"
|
||||
"openreplay/backend/pkg/db/postgres/pool"
|
||||
"openreplay/backend/pkg/logger"
|
||||
|
|
@ -29,18 +32,13 @@ func main() {
|
|||
builder := common.NewServiceBuilder(log)
|
||||
services, err := builder.
|
||||
WithDatabase(pgConn).
|
||||
WithJWTSecret(cfg.JWTSecret).
|
||||
WithJWTSecret(cfg.JWTSecret, cfg.JWTSpotSecret).
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(ctx, "can't init services: %s", err)
|
||||
}
|
||||
|
||||
//services, err := analytics.NewServiceBuilder(log, cfg, pgConn)
|
||||
//if err != nil {
|
||||
// log.Fatal(ctx, "can't init services: %s", err)
|
||||
//}
|
||||
|
||||
// Define excluded paths for this service
|
||||
excludedPaths := map[string]map[string]bool{
|
||||
//"/v1/ping": {"GET": true},
|
||||
|
|
@ -56,9 +54,29 @@ func main() {
|
|||
return []string{"user"}
|
||||
}
|
||||
|
||||
authOptionsSelector := func(r *http.Request) *auth.Options {
|
||||
pathTemplate, err := mux.CurrentRoute(r).GetPathTemplate()
|
||||
if err != nil {
|
||||
log.Error(r.Context(), "failed to get path template: %s", err)
|
||||
return nil // Use default options if there’s an error
|
||||
}
|
||||
|
||||
// Customize based on route and method
|
||||
if pathTemplate == "/v1/spots/{id}/uploaded" && r.Method == "POST" {
|
||||
column := "spot_jwt_iat"
|
||||
secret := cfg.JWTSpotSecret
|
||||
return &auth.Options{JwtColumn: column, Secret: secret}
|
||||
}
|
||||
|
||||
// Return nil to signal default options in AuthMiddleware
|
||||
return nil
|
||||
}
|
||||
|
||||
authMiddleware := middleware.AuthMiddleware(services, log, excludedPaths, getPermissions, authOptionsSelector)
|
||||
|
||||
router, err := api.NewRouter(cfg, log, services)
|
||||
router.GetRouter().Use(middleware.CORS(cfg.UseAccessControlHeaders))
|
||||
router.GetRouter().Use(middleware.AuthMiddleware(services, log, excludedPaths, getPermissions))
|
||||
router.GetRouter().Use(authMiddleware)
|
||||
router.GetRouter().Use(middleware.RateLimit)
|
||||
router.GetRouter().Use(middleware.Action)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type Config struct {
|
|||
UseAccessControlHeaders bool `env:"USE_CORS,default=false"`
|
||||
ProjectExpiration time.Duration `env:"PROJECT_EXPIRATION,default=10m"`
|
||||
JWTSecret string `env:"JWT_SECRET,required"`
|
||||
JWTSpotSecret string `env:"JWT_SPOT_SECRET,required"` // TODO: remove this
|
||||
MinimumStreamDuration int `env:"MINIMUM_STREAM_DURATION,default=15000"` // 15s
|
||||
WorkerID uint16
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func (e *Router) spotTest(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Welcome to NSE Live API"))
|
||||
}
|
||||
|
||||
func (e *Router) createDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ func (e *Router) init() {
|
|||
e.router = mux.NewRouter()
|
||||
e.router.HandleFunc("/", e.ping)
|
||||
|
||||
e.router.HandleFunc("/{projectId}/dashboards", e.createDashboard).Methods("POST")
|
||||
e.router.HandleFunc("/{projectId}/dashboards", e.getDashboards).Methods("GET")
|
||||
e.router.HandleFunc("/{projectId}/dashboards", e.createDashboard).Methods("POST", "OPTIONS")
|
||||
e.router.HandleFunc("/v1/spots/{id}/uploaded", e.spotTest).Methods("POST", "OPTIONS")
|
||||
e.router.HandleFunc("/{projectId}/dashboards", e.getDashboards).Methods("GET", "OPTIONS")
|
||||
e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}", e.getDashboard).Methods("GET")
|
||||
e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}", e.updateDashboard).Methods("PUT")
|
||||
e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}", e.deleteDashboard).Methods("DELETE")
|
||||
|
|
|
|||
|
|
@ -1,30 +1 @@
|
|||
package analytics
|
||||
|
||||
//import (
|
||||
// "openreplay/backend/internal/config/analytics"
|
||||
// "openreplay/backend/pkg/common/api/auth"
|
||||
// "openreplay/backend/pkg/db/postgres/pool"
|
||||
// "openreplay/backend/pkg/flakeid"
|
||||
// "openreplay/backend/pkg/logger"
|
||||
// "openreplay/backend/pkg/objectstorage"
|
||||
// "openreplay/backend/pkg/objectstorage/store"
|
||||
//)
|
||||
//
|
||||
//type ServicesBuilder struct {
|
||||
// Flaker *flakeid.Flaker
|
||||
// ObjStorage objectstorage.ObjectStorage
|
||||
// Auth auth.Auth
|
||||
//}
|
||||
//
|
||||
//func NewServiceBuilder(log logger.Logger, cfg *analytics.Config, pgconn pool.Pool) (*ServicesBuilder, error) {
|
||||
// objStore, err := store.NewStore(&cfg.ObjectsConfig)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// flaker := flakeid.NewFlaker(cfg.WorkerID)
|
||||
// return &ServicesBuilder{
|
||||
// Flaker: flaker,
|
||||
// ObjStorage: objStore,
|
||||
// Auth: auth.NewAuth(log, cfg.JWTSecret, pgconn),
|
||||
// }, nil
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -10,21 +10,46 @@ import (
|
|||
"openreplay/backend/pkg/logger"
|
||||
)
|
||||
|
||||
// Options struct to hold optional JWT column and secret
|
||||
type Options struct {
|
||||
JwtColumn string // The JWT column to use (e.g., "jwt_iat" or "spot_jwt_iat")
|
||||
Secret string // An optional secret; if nil, default secret is used
|
||||
}
|
||||
|
||||
type Auth interface {
|
||||
IsAuthorized(authHeader string, permissions []string, isExtension bool) (*User, error)
|
||||
IsAuthorized(authHeader string, permissions []string, options Options) (*User, error)
|
||||
Secret() string
|
||||
JWTCol() string
|
||||
ExtraSecret() string
|
||||
}
|
||||
|
||||
type authImpl struct {
|
||||
log logger.Logger
|
||||
secret string
|
||||
pgconn pool.Pool
|
||||
log logger.Logger
|
||||
secret string
|
||||
extraSecret string
|
||||
pgconn pool.Pool
|
||||
jwtCol string
|
||||
}
|
||||
|
||||
func NewAuth(log logger.Logger, jwtSecret string, conn pool.Pool) Auth {
|
||||
func (a *authImpl) Secret() string {
|
||||
return a.secret
|
||||
}
|
||||
|
||||
func (a *authImpl) JWTCol() string {
|
||||
return a.jwtCol
|
||||
}
|
||||
|
||||
func (a *authImpl) ExtraSecret() string {
|
||||
return a.extraSecret
|
||||
}
|
||||
|
||||
func NewAuth(log logger.Logger, jwtCol string, jwtSecret string, extraSecret string, conn pool.Pool) Auth {
|
||||
return &authImpl{
|
||||
log: log,
|
||||
secret: jwtSecret,
|
||||
pgconn: conn,
|
||||
log: log,
|
||||
secret: jwtSecret,
|
||||
extraSecret: extraSecret,
|
||||
pgconn: conn,
|
||||
jwtCol: jwtCol,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package auth
|
||||
|
||||
func (a *authImpl) IsAuthorized(authHeader string, permissions []string, isExtension bool) (*User, error) {
|
||||
secret := a.secret
|
||||
func (a *authImpl) IsAuthorized(authHeader string, permissions []string, options Options) (*User, error) {
|
||||
jwtCol := options.JwtColumn
|
||||
secret := options.Secret
|
||||
|
||||
jwtInfo, err := parseJWT(authHeader, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authUser(a.pgconn, jwtInfo.UserId, jwtInfo.TenantID, int(jwtInfo.IssuedAt.Unix()), isExtension)
|
||||
return authUser(a.pgconn, jwtInfo.UserId, jwtInfo.TenantID, int(jwtInfo.IssuedAt.Unix()), jwtCol)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ import (
|
|||
"openreplay/backend/pkg/db/postgres/pool"
|
||||
)
|
||||
|
||||
func authUser(conn pool.Pool, userID, tenantID, jwtIAT int, isExtension bool) (*User, error) {
|
||||
sql := `
|
||||
SELECT user_id, name, email
|
||||
func authUser(conn pool.Pool, userID, tenantID, jwtIAT int, jwtCol string) (*User, error) {
|
||||
sql := fmt.Sprintf(`
|
||||
SELECT user_id, name, email, EXTRACT(epoch FROM %s)::BIGINT AS jwt_iat
|
||||
FROM public.users
|
||||
WHERE user_id = $1 AND deleted_at IS NULL
|
||||
LIMIT 1;`
|
||||
LIMIT 1;`, jwtCol)
|
||||
user := &User{TenantID: 1, AuthMethod: "jwt"}
|
||||
if err := conn.QueryRow(sql, userID).Scan(&user.ID, &user.Name, &user.Email, &user.JwtIat); err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
return nil, fmt.Errorf("user not found") // TODO should be a proper message with error message
|
||||
}
|
||||
if user.JwtIat == 0 || abs(jwtIAT-user.JwtIat) > 1 {
|
||||
return nil, fmt.Errorf("token has been updated")
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@ import (
|
|||
|
||||
// ServicesBuilder struct to hold service components
|
||||
type ServicesBuilder struct {
|
||||
flaker *flakeid.Flaker
|
||||
objStorage objectstorage.ObjectStorage
|
||||
Auth auth.Auth
|
||||
log logger.Logger
|
||||
pgconn pool.Pool
|
||||
workerID int
|
||||
jwtSecret string
|
||||
flaker *flakeid.Flaker
|
||||
objStorage objectstorage.ObjectStorage
|
||||
Auth auth.Auth
|
||||
log logger.Logger
|
||||
pgconn pool.Pool
|
||||
workerID int
|
||||
jwtSecret string
|
||||
extraSecret string
|
||||
}
|
||||
|
||||
// NewServiceBuilder initializes the ServicesBuilder with essential components (logger)
|
||||
|
|
@ -57,9 +58,12 @@ func (b *ServicesBuilder) WithWorkerID(workerID int) *ServicesBuilder {
|
|||
return b
|
||||
}
|
||||
|
||||
// WithJWTSecret sets the JWT secret for Auth
|
||||
func (b *ServicesBuilder) WithJWTSecret(jwtSecret string) *ServicesBuilder {
|
||||
// WithJWTSecret sets the JWT and optional extra secret for Auth
|
||||
func (b *ServicesBuilder) WithJWTSecret(jwtSecret string, extraSecret ...string) *ServicesBuilder {
|
||||
b.jwtSecret = jwtSecret
|
||||
if len(extraSecret) > 0 {
|
||||
b.extraSecret = extraSecret[0]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +86,7 @@ func (b *ServicesBuilder) Build() (*ServicesBuilder, error) {
|
|||
if b.jwtSecret == "" {
|
||||
return nil, errors.New("JWT secret is required")
|
||||
}
|
||||
b.Auth = auth.NewAuth(b.log, b.jwtSecret, b.pgconn)
|
||||
b.Auth = auth.NewAuth(b.log, "jwt_iat", b.jwtSecret, b.extraSecret, b.pgconn)
|
||||
}
|
||||
|
||||
// Return the fully constructed service
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package middleware
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"openreplay/backend/internal/http/util"
|
||||
"openreplay/backend/pkg/common"
|
||||
|
|
@ -41,7 +40,8 @@ func AuthMiddleware(
|
|||
services *common.ServicesBuilder, // Injected services (Auth, Keys, etc.)
|
||||
log logger.Logger, // Logger for logging events
|
||||
excludedPaths map[string]map[string]bool, // Map of excluded paths with methods
|
||||
getPermissions func(path string) []string, // Function to retrieve permissions for a path
|
||||
getPermissions func(path string) []string,
|
||||
authOptionsSelector func(r *http.Request) *auth.Options, // Function to retrieve permissions for a path
|
||||
) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -56,14 +56,34 @@ func AuthMiddleware(
|
|||
return
|
||||
}
|
||||
|
||||
// Check if the route is dynamic and get the path template
|
||||
pathTemplate, err := mux.CurrentRoute(r).GetPathTemplate()
|
||||
if err != nil {
|
||||
log.Error(r.Context(), "failed to get path template: %s", err)
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
log.Warn(r.Context(), "Authorization header missing")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Get AuthOptions for the request
|
||||
options := auth.Options{
|
||||
JwtColumn: services.Auth.JWTCol(), // Default JWT column from ServicesBuilder
|
||||
Secret: services.Auth.Secret(), // Default secret from ServicesBuilder
|
||||
}
|
||||
|
||||
if authOptionsSelector != nil {
|
||||
selectorOptions := authOptionsSelector(r)
|
||||
if selectorOptions != nil {
|
||||
// Override defaults with values from selectorOptions
|
||||
if selectorOptions.JwtColumn != "" {
|
||||
options.JwtColumn = selectorOptions.JwtColumn
|
||||
}
|
||||
if selectorOptions.Secret != "" {
|
||||
options.Secret = selectorOptions.Secret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this request is authorized
|
||||
user, err := services.Auth.IsAuthorized(r.Header.Get("Authorization"), getPermissions(r.URL.Path), pathTemplate != "")
|
||||
user, err := services.Auth.IsAuthorized(authHeader, getPermissions(r.URL.Path), options)
|
||||
if err != nil {
|
||||
log.Warn(r.Context(), "Unauthorized request: %s", err)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue