feat: migrate workbench services
This commit is contained in:
+92
-92
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -24,22 +23,18 @@ var (
|
||||
)
|
||||
|
||||
type Principal struct {
|
||||
TenantID int64
|
||||
TenantCode string
|
||||
UserID int64
|
||||
DeviceID string
|
||||
AuthLevel string
|
||||
TenantID int64
|
||||
MemberID int64
|
||||
DeviceID string
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TenantCode string `json:"tenant_code"`
|
||||
DeviceID string `json:"device_id"`
|
||||
AuthLevel string `json:"auth_level"`
|
||||
State string `json:"state"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
IssuedAt string `json:"issued_at"`
|
||||
MemberID int64 `json:"member_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
State string `json:"state"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
IssuedAt string `json:"issued_at"`
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
@@ -51,23 +46,20 @@ func New(cfg config.AuthConfig, redisClient *redis.Client) *Authenticator {
|
||||
return &Authenticator{cfg: cfg, redis: redisClient}
|
||||
}
|
||||
|
||||
func (a *Authenticator) DefaultTenantID() int64 {
|
||||
return a.cfg.DefaultTenantID
|
||||
}
|
||||
|
||||
func (a *Authenticator) Authenticate(req *http.Request) (*Principal, error) {
|
||||
if err := a.validateAPIKey(req); err != nil {
|
||||
if err := a.ValidateAPIKey(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if a.cfg.AllowDevHeaderAuth {
|
||||
if principal := principalFromDevHeaders(req); principal != nil {
|
||||
return principal, nil
|
||||
if memberID := parseInt64Header(req, "X-Member-ID"); memberID > 0 {
|
||||
return &Principal{TenantID: a.cfg.DefaultTenantID, MemberID: memberID,
|
||||
DeviceID: strings.TrimSpace(req.Header.Get("X-Device-ID"))}, nil
|
||||
}
|
||||
}
|
||||
if a.cfg.SkipSessionInDev {
|
||||
return &Principal{
|
||||
TenantID: parseInt64Header(req, "X-Tenant-ID", 1),
|
||||
TenantCode: firstNonEmpty(req.Header.Get("X-Tenant-Code"), "default"),
|
||||
UserID: parseInt64Header(req, "X-User-ID", 1),
|
||||
AuthLevel: "auth",
|
||||
}, nil
|
||||
}
|
||||
if a.redis == nil {
|
||||
return nil, ErrAuthRequired
|
||||
}
|
||||
@@ -79,50 +71,60 @@ func (a *Authenticator) Authenticate(req *http.Request) (*Principal, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if session.UserID <= 0 || session.TenantID <= 0 {
|
||||
if session.MemberID <= 0 || session.TenantID < 0 || session.State != "active" {
|
||||
return nil, ErrAuthRequired
|
||||
}
|
||||
if state := strings.ToLower(strings.TrimSpace(session.State)); state != "" && state != "active" {
|
||||
expiresAt, err := time.Parse(time.RFC3339, session.ExpiresAt)
|
||||
if err != nil || time.Now().After(expiresAt) {
|
||||
return nil, ErrAuthRequired
|
||||
}
|
||||
if session.ExpiresAt != "" {
|
||||
expiresAt, err := time.Parse(time.RFC3339, session.ExpiresAt)
|
||||
if err == nil && time.Now().After(expiresAt) {
|
||||
return nil, ErrAuthRequired
|
||||
}
|
||||
if err := a.validateMemberSession(req.Context(), session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headerTenant := strings.TrimSpace(req.Header.Get("X-Tenant-Code"))
|
||||
if a.cfg.RequiredTenantCode && headerTenant == "" {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
if headerTenant != "" && session.TenantCode != "" && headerTenant != session.TenantCode {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
return &Principal{
|
||||
TenantID: session.TenantID,
|
||||
TenantCode: firstNonEmpty(session.TenantCode, headerTenant),
|
||||
UserID: session.UserID,
|
||||
DeviceID: session.DeviceID,
|
||||
AuthLevel: session.AuthLevel,
|
||||
}, nil
|
||||
return &Principal{TenantID: session.TenantID, MemberID: session.MemberID, DeviceID: session.DeviceID}, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) validateAPIKey(req *http.Request) error {
|
||||
func (a *Authenticator) Logout(req *http.Request) error {
|
||||
if _, err := a.Authenticate(req); err != nil {
|
||||
return err
|
||||
}
|
||||
token, err := bearerToken(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := sessionKeyFromToken(a.cfg.SessionKeyPattern, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.redis == nil {
|
||||
return ErrAuthRequired
|
||||
}
|
||||
return a.redis.Del(req.Context(), key).Err()
|
||||
}
|
||||
|
||||
func (a *Authenticator) RevokeMemberSessions(ctx context.Context, tenantID, memberID int64) error {
|
||||
if a == nil || a.redis == nil {
|
||||
return ErrAuthRequired
|
||||
}
|
||||
key := memberRevokedBeforeKey(tenantID, memberID)
|
||||
value := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
ttl := time.Duration(a.cfg.SessionTTLSeconds) * time.Second
|
||||
return a.redis.Set(ctx, key, value, ttl).Err()
|
||||
}
|
||||
|
||||
func (a *Authenticator) ValidateAPIKey(req *http.Request) error {
|
||||
if len(a.cfg.APIKeys) == 0 && !a.cfg.RequiredAPIKey {
|
||||
return nil
|
||||
}
|
||||
got := strings.TrimSpace(req.Header.Get("X-MeetingIM-APIKey"))
|
||||
got := strings.TrimSpace(req.Header.Get("X-API-Key"))
|
||||
if got == "" {
|
||||
got = strings.TrimSpace(req.URL.Query().Get("apikey"))
|
||||
}
|
||||
if got == "" {
|
||||
if a.cfg.RequiredAPIKey {
|
||||
return ErrForbidden
|
||||
}
|
||||
return nil
|
||||
return ErrForbidden
|
||||
}
|
||||
if len(a.cfg.APIKeys) == 0 {
|
||||
return nil
|
||||
return ErrForbidden
|
||||
}
|
||||
for _, key := range a.cfg.APIKeys {
|
||||
if subtle.ConstantTimeCompare([]byte(got), []byte(strings.TrimSpace(key))) == 1 {
|
||||
@@ -133,14 +135,12 @@ func (a *Authenticator) validateAPIKey(req *http.Request) error {
|
||||
}
|
||||
|
||||
func (a *Authenticator) sessionFromRedis(ctx context.Context, token string) (*Session, error) {
|
||||
secret := []byte(strings.TrimSpace(token))
|
||||
if decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(token)); err == nil {
|
||||
secret = decoded
|
||||
key, err := sessionKeyFromToken(a.cfg.SessionKeyPattern, token)
|
||||
if err != nil {
|
||||
return nil, ErrAuthRequired
|
||||
}
|
||||
digest := sha256.Sum256(secret)
|
||||
key := fmt.Sprintf(a.cfg.SessionKeyPattern, hex.EncodeToString(digest[:]))
|
||||
data, err := a.redis.Get(ctx, key).Bytes()
|
||||
if err != nil && errors.Is(err, redis.Nil) {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, ErrAuthRequired
|
||||
}
|
||||
if err != nil {
|
||||
@@ -153,46 +153,46 @@ func (a *Authenticator) sessionFromRedis(ctx context.Context, token string) (*Se
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) validateMemberSession(ctx context.Context, session *Session) error {
|
||||
revokedBefore, err := a.redis.Get(ctx, memberRevokedBeforeKey(session.TenantID, session.MemberID)).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issuedAt, issuedErr := time.Parse(time.RFC3339Nano, session.IssuedAt)
|
||||
revokedAt, revokedErr := time.Parse(time.RFC3339Nano, revokedBefore)
|
||||
if issuedErr != nil || revokedErr != nil || !issuedAt.After(revokedAt) {
|
||||
return ErrAuthRequired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func memberRevokedBeforeKey(tenantID, memberID int64) string {
|
||||
return "workbench:session:member-revoked-before:" + strconv.FormatInt(tenantID, 10) + ":" + strconv.FormatInt(memberID, 10)
|
||||
}
|
||||
|
||||
func sessionKeyFromToken(pattern, token string) (string, error) {
|
||||
raw, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(token))
|
||||
if err != nil || len(raw) != 32 {
|
||||
return "", ErrAuthRequired
|
||||
}
|
||||
digest := sha256.Sum256(raw)
|
||||
return strings.Replace(pattern, "%s", hex.EncodeToString(digest[:]), 1), nil
|
||||
}
|
||||
|
||||
func bearerToken(req *http.Request) (string, error) {
|
||||
authz := strings.TrimSpace(req.Header.Get("Authorization"))
|
||||
token, ok := strings.CutPrefix(authz, "Bearer ")
|
||||
authorization := strings.TrimSpace(req.Header.Get("Authorization"))
|
||||
token, ok := strings.CutPrefix(authorization, "Bearer ")
|
||||
if !ok || strings.TrimSpace(token) == "" {
|
||||
return "", ErrAuthRequired
|
||||
}
|
||||
return strings.TrimSpace(token), nil
|
||||
}
|
||||
|
||||
func principalFromDevHeaders(req *http.Request) *Principal {
|
||||
uid := parseInt64Header(req, "X-User-ID", 0)
|
||||
if uid <= 0 {
|
||||
return nil
|
||||
}
|
||||
return &Principal{
|
||||
TenantID: parseInt64Header(req, "X-Tenant-ID", 1),
|
||||
TenantCode: firstNonEmpty(req.Header.Get("X-Tenant-Code"), "default"),
|
||||
UserID: uid,
|
||||
DeviceID: strings.TrimSpace(req.Header.Get("X-Device-ID")),
|
||||
AuthLevel: "auth",
|
||||
}
|
||||
}
|
||||
|
||||
func parseInt64Header(req *http.Request, key string, fallback int64) int64 {
|
||||
func parseInt64Header(req *http.Request, key string) int64 {
|
||||
value := strings.TrimSpace(req.Header.Get(key))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
parsed, _ := strconv.ParseInt(value, 10, 64)
|
||||
return parsed
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user