diff --git a/backend/pkg/analytics/builder.go b/backend/pkg/analytics/builder.go index 12f68513f..5c746dcf7 100644 --- a/backend/pkg/analytics/builder.go +++ b/backend/pkg/analytics/builder.go @@ -2,6 +2,7 @@ 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" @@ -12,6 +13,7 @@ import ( 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) { @@ -23,5 +25,6 @@ func NewServiceBuilder(log logger.Logger, cfg *analytics.Config, pgconn pool.Poo return &ServicesBuilder{ Flaker: flaker, ObjStorage: objStore, + Auth: auth.NewAuth(log, cfg.JWTSecret, pgconn), }, nil } diff --git a/backend/pkg/analytics/service/timeseries.go b/backend/pkg/analytics/service/timeseries.go new file mode 100644 index 000000000..6d43c3366 --- /dev/null +++ b/backend/pkg/analytics/service/timeseries.go @@ -0,0 +1 @@ +package service diff --git a/backend/pkg/common/api/auth/auth.go b/backend/pkg/common/api/auth/auth.go new file mode 100644 index 000000000..4f4fd62c5 --- /dev/null +++ b/backend/pkg/common/api/auth/auth.go @@ -0,0 +1,51 @@ +package auth + +import ( + "fmt" + "strings" + + "github.com/golang-jwt/jwt/v5" + + "openreplay/backend/pkg/db/postgres/pool" + "openreplay/backend/pkg/logger" +) + +type Auth interface { + IsAuthorized(authHeader string, permissions []string, isExtension bool) (*User, error) +} + +type authImpl struct { + log logger.Logger + secret string + pgconn pool.Pool +} + +func NewAuth(log logger.Logger, jwtSecret string, conn pool.Pool) Auth { + return &authImpl{ + log: log, + secret: jwtSecret, + pgconn: conn, + } +} + +func parseJWT(authHeader, secret string) (*JWTClaims, error) { + if authHeader == "" { + return nil, fmt.Errorf("authorization header missing") + } + tokenParts := strings.Split(authHeader, "Bearer ") + if len(tokenParts) != 2 { + return nil, fmt.Errorf("invalid authorization header") + } + tokenString := tokenParts[1] + + claims := &JWTClaims{} + token, err := jwt.ParseWithClaims(tokenString, claims, + func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + if err != nil || !token.Valid { + fmt.Printf("token err: %v\n", err) + return nil, fmt.Errorf("invalid token") + } + return claims, nil +} diff --git a/backend/pkg/common/api/auth/authorizer.go b/backend/pkg/common/api/auth/authorizer.go new file mode 100644 index 000000000..b36ce81bb --- /dev/null +++ b/backend/pkg/common/api/auth/authorizer.go @@ -0,0 +1,10 @@ +package auth + +func (a *authImpl) IsAuthorized(authHeader string, permissions []string, isExtension bool) (*User, error) { + secret := a.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) +} diff --git a/backend/pkg/common/api/auth/model.go b/backend/pkg/common/api/auth/model.go new file mode 100644 index 000000000..ef2f09d75 --- /dev/null +++ b/backend/pkg/common/api/auth/model.go @@ -0,0 +1,34 @@ +package auth + +import "github.com/golang-jwt/jwt/v5" + +type JWTClaims struct { + UserId int `json:"userId"` + TenantID int `json:"tenantId"` + jwt.RegisteredClaims +} + +type User struct { + ID uint64 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + TenantID uint64 `json:"tenantId"` + JwtIat int `json:"jwtIat"` + Permissions map[string]bool `json:"permissions"` + AuthMethod string +} + +func (u *User) HasPermission(perm string) bool { + if u.Permissions == nil { + return true // no permissions + } + _, ok := u.Permissions[perm] + return ok +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/backend/pkg/common/api/auth/storage.go b/backend/pkg/common/api/auth/storage.go new file mode 100644 index 000000000..3830f94f9 --- /dev/null +++ b/backend/pkg/common/api/auth/storage.go @@ -0,0 +1,22 @@ +package auth + +import ( + "fmt" + "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 + FROM public.users + WHERE user_id = $1 AND deleted_at IS NULL + LIMIT 1;` + 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") + } + if user.JwtIat == 0 || abs(jwtIAT-user.JwtIat) > 1 { + return nil, fmt.Errorf("token has been updated") + } + return user, nil +} diff --git a/backend/pkg/common/api/service/user.go b/backend/pkg/common/api/service/user.go new file mode 100644 index 000000000..1f2b16c33 --- /dev/null +++ b/backend/pkg/common/api/service/user.go @@ -0,0 +1,3 @@ +package service + +var getUserSQL = `SELECT 1, name, email FROM public.users WHERE user_id = $1 AND deleted_at IS NULL LIMIT 1` diff --git a/backend/pkg/common/builder.go b/backend/pkg/common/builder.go new file mode 100644 index 000000000..805d0c79a --- /dev/null +++ b/backend/pkg/common/builder.go @@ -0,0 +1 @@ +package common