fix: harden identity, live access, drafts and setup (review only)
Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Modified in the independent hardening fork; see docs/hardening.
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
@@ -50,21 +52,27 @@ func (s *authService) GetAuthPrincipal(ctx *gin.Context) *dto.AuthPrincipal {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) setAuthPrincipal(ctx *gin.Context, user *models.User, roles, permissions []string) *dto.AuthPrincipal {
|
||||
principal := &dto.AuthPrincipal{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Avatar: user.Avatar,
|
||||
UserType: user.UserType,
|
||||
Status: user.Status,
|
||||
Roles: roles,
|
||||
Permissions: permissions,
|
||||
}
|
||||
func (s *authService) setAuthPrincipal(ctx *gin.Context, user *models.User, session *models.LoginSession, roles, permissions []string) *dto.AuthPrincipal {
|
||||
principal := buildAuthPrincipal(user, session, roles, permissions)
|
||||
ctx.Set(authPrincipalContextKey, principal)
|
||||
return principal
|
||||
}
|
||||
|
||||
func buildAuthPrincipal(user *models.User, session *models.LoginSession, roles, permissions []string) *dto.AuthPrincipal {
|
||||
return &dto.AuthPrincipal{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Avatar: user.Avatar,
|
||||
UserType: user.UserType,
|
||||
Status: user.Status,
|
||||
Roles: slices.Clone(roles),
|
||||
Permissions: slices.Clone(permissions),
|
||||
LoginSessionID: session.ID,
|
||||
ExpiresAt: session.ExpiredAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *authService) RequirePermission(ctx *gin.Context, permission constants.Permission) (principal *dto.AuthPrincipal, err error) {
|
||||
if principal = s.GetAuthPrincipal(ctx); principal == nil {
|
||||
if principal, err = s.Authenticate(ctx); err != nil {
|
||||
@@ -96,7 +104,7 @@ func (s *authService) Login(req request.LoginRequest, authCfg config.AuthConfig,
|
||||
}
|
||||
|
||||
user := UserService.GetByUsername(username)
|
||||
if user == nil || user.Status != enums.StatusOk {
|
||||
if user == nil || user.DeletedAt != nil || user.Status != enums.StatusOk {
|
||||
_ = s.createLoginCredentialLog(principal, 0, false, clientIP, userAgent, "user not found")
|
||||
return nil, errorsx.InvalidAccountI18n("error.e0260")
|
||||
}
|
||||
@@ -165,7 +173,7 @@ func (s *authService) Authenticate(ctx *gin.Context) (*dto.AuthPrincipal, error)
|
||||
}
|
||||
|
||||
user := UserService.Get(session.UserID)
|
||||
if user == nil || user.Status != enums.StatusOk {
|
||||
if user == nil || user.DeletedAt != nil || user.Status != enums.StatusOk {
|
||||
return nil, errorsx.UnauthorizedI18n("error.e0256")
|
||||
}
|
||||
|
||||
@@ -173,7 +181,7 @@ func (s *authService) Authenticate(ctx *gin.Context) (*dto.AuthPrincipal, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
principal := s.setAuthPrincipal(ctx, user, roles, permissions)
|
||||
principal := s.setAuthPrincipal(ctx, user, session, roles, permissions)
|
||||
|
||||
now := time.Now()
|
||||
_ = LoginSessionService.Updates(session.ID, map[string]any{
|
||||
@@ -184,6 +192,39 @@ func (s *authService) Authenticate(ctx *gin.Context) (*dto.AuthPrincipal, error)
|
||||
return principal, nil
|
||||
}
|
||||
|
||||
// RevalidatePrincipal refreshes a long-lived connection's authorization without
|
||||
// keeping the bearer token. The original snapshot is never mutated and its
|
||||
// absolute deadline cannot be extended by revalidation.
|
||||
func (s *authService) RevalidatePrincipal(principal *dto.AuthPrincipal) (*dto.AuthPrincipal, error) {
|
||||
return s.revalidatePrincipal(sqls.DB(), principal, time.Now())
|
||||
}
|
||||
|
||||
func (s *authService) revalidatePrincipal(db *gorm.DB, principal *dto.AuthPrincipal, now time.Time) (*dto.AuthPrincipal, error) {
|
||||
if principal == nil || principal.UserID <= 0 || principal.LoginSessionID <= 0 || !now.Before(principal.ExpiresAt) {
|
||||
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
|
||||
session, err := repositories.LoginSessionRepository.GetAuthState(db, principal.LoginSessionID, principal.UserID)
|
||||
if err != nil || session == nil || session.RevokedAt != nil || !now.Before(session.ExpiredAt) {
|
||||
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
user, err := repositories.UserRepository.GetAuthState(db, principal.UserID)
|
||||
if err != nil || user == nil || user.DeletedAt != nil || user.Status != enums.StatusOk {
|
||||
return nil, errorsx.UnauthorizedI18n("error.e0256")
|
||||
}
|
||||
roles, permissions, err := s.loadUserAuthScope(db, user.ID)
|
||||
if err != nil {
|
||||
// Database failures must close the connection, not retain its old scope or
|
||||
// expose persistence details through a transport error.
|
||||
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
fresh := buildAuthPrincipal(user, session, roles, permissions)
|
||||
if principal.ExpiresAt.Before(fresh.ExpiresAt) {
|
||||
fresh.ExpiresAt = principal.ExpiresAt
|
||||
}
|
||||
return fresh, nil
|
||||
}
|
||||
|
||||
func (s *authService) HasPermission(ctx *gin.Context, permissionCode string) bool {
|
||||
principal := s.GetAuthPrincipal(ctx)
|
||||
if principal == nil {
|
||||
@@ -341,7 +382,7 @@ func (s *authService) validateSessionToken(token string) (*models.LoginSession,
|
||||
if session.RevokedAt != nil {
|
||||
return nil, errorsx.InvalidTokenI18n("error.e0267")
|
||||
}
|
||||
if time.Now().After(session.ExpiredAt) {
|
||||
if !time.Now().Before(session.ExpiredAt) {
|
||||
return nil, errorsx.InvalidTokenI18n("error.e0268")
|
||||
}
|
||||
return session, nil
|
||||
@@ -372,49 +413,16 @@ func (s *authService) loadUserRoleCodes(tx *gorm.DB, userID int64) ([]string, er
|
||||
}
|
||||
|
||||
func (s *authService) loadUserRoles(tx *gorm.DB, userID int64) ([]models.Role, error) {
|
||||
roles := make([]models.Role, 0)
|
||||
if err := tx.
|
||||
Table("t_role AS r").
|
||||
Select("r.*").
|
||||
Joins("JOIN t_user_role AS ur ON ur.role_id = r.id").
|
||||
Where("ur.user_id = ? AND r.status = ?", userID, enums.StatusOk).
|
||||
Order("r.sort_no ASC, r.id ASC").
|
||||
Scan(&roles).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
return repositories.RoleRepository.FindEnabledByUser(tx, userID)
|
||||
}
|
||||
|
||||
func (s *authService) loadUserPermissionCodes(tx *gorm.DB, userID int64) ([]string, error) {
|
||||
permissionRows := make([]struct {
|
||||
Code string
|
||||
}, 0)
|
||||
db := tx.Table("t_permission AS p").
|
||||
Select("DISTINCT p.code").
|
||||
Joins("JOIN t_role_permission AS rp ON rp.permission_id = p.id").
|
||||
Joins("JOIN t_user_role AS ur ON ur.role_id = rp.role_id").
|
||||
Where("ur.user_id = ?", userID).
|
||||
Where("p.status = ?", enums.StatusOk)
|
||||
if err := db.Scan(&permissionRows).Error; err != nil {
|
||||
permissionCodes, err := repositories.PermissionRepository.FindEnabledCodesByUserRoles(tx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
permissionCodes := make([]string, 0, len(permissionRows))
|
||||
for _, permission := range permissionRows {
|
||||
permissionCodes = append(permissionCodes, permission.Code)
|
||||
}
|
||||
|
||||
overrideRows := make([]struct {
|
||||
Code string
|
||||
Effect int
|
||||
}, 0)
|
||||
if err := tx.
|
||||
Table("t_user_permission AS up").
|
||||
Select("p.code, up.effect").
|
||||
Joins("JOIN t_permission AS p ON p.id = up.permission_id").
|
||||
Where("up.user_id = ? AND (up.expired_at IS NULL OR up.expired_at > ?)", userID, time.Now()).
|
||||
Scan(&overrideRows).Error; err != nil {
|
||||
overrideRows, err := repositories.UserPermissionRepository.FindActiveEnabledOverrides(tx, userID, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user