168 lines
5.6 KiB
Go
168 lines
5.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
|
|
)
|
|
|
|
// PlatformRequirement is the route-level authorization policy evaluated after
|
|
// Identity has refreshed all account and organization claims from persistence.
|
|
type PlatformRequirement string
|
|
|
|
const (
|
|
PlatformApp PlatformRequirement = "app"
|
|
PlatformAdmin PlatformRequirement = "admin"
|
|
PlatformSuperAdmin PlatformRequirement = "super_admin"
|
|
)
|
|
|
|
// PlatformAuthErrorKind is deliberately transport-oriented. Domain handlers
|
|
// can map it to the current stable status without learning Cookie/parser detail.
|
|
type PlatformAuthErrorKind string
|
|
|
|
const (
|
|
PlatformUnauthenticated PlatformAuthErrorKind = "unauthenticated"
|
|
PlatformForbidden PlatformAuthErrorKind = "forbidden"
|
|
PlatformConfigurationError PlatformAuthErrorKind = "configuration_error"
|
|
)
|
|
|
|
// PlatformAuthError represents only expected authentication and RBAC denials.
|
|
// Resolver/database failures remain ordinary errors and must become generic 500s.
|
|
type PlatformAuthError struct {
|
|
Kind PlatformAuthErrorKind
|
|
Status int
|
|
Message string
|
|
}
|
|
|
|
func (err *PlatformAuthError) Error() string {
|
|
if err.Message != "" {
|
|
return err.Message
|
|
}
|
|
return string(err.Kind)
|
|
}
|
|
|
|
type PlatformAuthorizer struct {
|
|
state AuthState
|
|
resolver SessionResolver
|
|
now func() time.Time
|
|
localDevelopmentFallback bool
|
|
}
|
|
|
|
type PlatformAuthorizerOption func(*PlatformAuthorizer)
|
|
|
|
// WithLocalDevelopmentFallback controls the privileged demo identity used by
|
|
// the explicit local backend. Production composition disables it even when a
|
|
// legacy auth environment value says authentication is optional.
|
|
func WithLocalDevelopmentFallback(enabled bool) PlatformAuthorizerOption {
|
|
return func(authorizer *PlatformAuthorizer) { authorizer.localDevelopmentFallback = enabled }
|
|
}
|
|
|
|
// NewPlatformAuthorizer creates the single HTTP-side platform authentication
|
|
// seam shared by protected route Modules.
|
|
func NewPlatformAuthorizer(state AuthState, resolver SessionResolver, options ...PlatformAuthorizerOption) (*PlatformAuthorizer, error) {
|
|
if state.Configured && resolver == nil {
|
|
return nil, fmt.Errorf("platform authorization: configured authentication requires a session resolver")
|
|
}
|
|
authorizer := &PlatformAuthorizer{state: state, resolver: resolver, now: time.Now, localDevelopmentFallback: true}
|
|
for _, option := range options {
|
|
if option != nil {
|
|
option(authorizer)
|
|
}
|
|
}
|
|
return authorizer, nil
|
|
}
|
|
|
|
// Authorize returns either a database-refreshed platform Session, the exact
|
|
// local-development fallback when authentication is optional, an expected
|
|
// typed denial, or an unclassified infrastructure error.
|
|
func (authorizer *PlatformAuthorizer) Authorize(r *http.Request, requirement PlatformRequirement) (identity.Session, error) {
|
|
if authorizer == nil {
|
|
return identity.Session{}, fmt.Errorf("platform authorizer is not configured")
|
|
}
|
|
if !validPlatformRequirement(requirement) {
|
|
return identity.Session{}, fmt.Errorf("unsupported platform authorization requirement %q", requirement)
|
|
}
|
|
|
|
if authorizer.state.Configured {
|
|
cookieValue, found := readSessionCookie(r)
|
|
if found && len(cookieValue) <= identity.CookieMaxValueLength {
|
|
session, err := authorizer.resolver.Resolve(r.Context(), cookieValue)
|
|
if err == nil {
|
|
return authorizePlatformRole(session, requirement)
|
|
}
|
|
if !errors.Is(err, identity.ErrUnauthenticated) {
|
|
return identity.Session{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if !authorizer.state.Required && authorizer.localDevelopmentFallback {
|
|
return authorizePlatformRole(authorizer.localSession(), requirement)
|
|
}
|
|
if !authorizer.state.Required {
|
|
return identity.Session{}, &PlatformAuthError{
|
|
Kind: PlatformUnauthenticated, Status: http.StatusUnauthorized,
|
|
Message: "请先登录。",
|
|
}
|
|
}
|
|
if !authorizer.state.Configured {
|
|
return identity.Session{}, &PlatformAuthError{
|
|
Kind: PlatformConfigurationError, Status: http.StatusServiceUnavailable,
|
|
Message: "认证配置不完整。",
|
|
}
|
|
}
|
|
return identity.Session{}, &PlatformAuthError{
|
|
Kind: PlatformUnauthenticated, Status: http.StatusUnauthorized,
|
|
Message: "请先登录。",
|
|
}
|
|
}
|
|
|
|
func authorizePlatformRole(session identity.Session, requirement PlatformRequirement) (identity.Session, error) {
|
|
allowed := requirement == PlatformApp
|
|
if requirement == PlatformAdmin {
|
|
allowed = session.AuthMode == identity.AuthModeAdmin &&
|
|
(session.User.Role == "organization_admin" || session.User.Role == "super_admin")
|
|
}
|
|
if requirement == PlatformSuperAdmin {
|
|
allowed = session.User.Role == "super_admin"
|
|
}
|
|
if allowed {
|
|
return session, nil
|
|
}
|
|
return identity.Session{}, &PlatformAuthError{
|
|
Kind: PlatformForbidden, Status: http.StatusForbidden,
|
|
Message: "需要管理员权限。",
|
|
}
|
|
}
|
|
|
|
func validPlatformRequirement(requirement PlatformRequirement) bool {
|
|
return requirement == PlatformApp || requirement == PlatformAdmin || requirement == PlatformSuperAdmin
|
|
}
|
|
|
|
func (authorizer *PlatformAuthorizer) localSession() identity.Session {
|
|
now := authorizer.now()
|
|
return identity.Session{
|
|
Version: 1,
|
|
AuthMode: identity.AuthModeAdmin,
|
|
IssuedAt: now.Unix(),
|
|
ExpiresAt: now.Add(24 * time.Hour).Unix(),
|
|
User: identity.User{
|
|
ID: "demo-merchant",
|
|
Subject: "demo-merchant",
|
|
Username: "13800000000",
|
|
Phone: "13800000000",
|
|
DisplayName: "智念用户",
|
|
ClientID: "local-dev",
|
|
OrganizationID: "org-demo",
|
|
OrganizationName: "演示组织",
|
|
Role: "super_admin",
|
|
Status: "active",
|
|
Authorities: []string{"zhinian_admin"},
|
|
Scope: []string{},
|
|
},
|
|
}
|
|
}
|