사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
228 lines
7.5 KiB
Go
228 lines
7.5 KiB
Go
package openai
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"iop/apps/edge/internal/authprojection"
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
// Principal source tags. These label how an openAIPrincipal was resolved and
|
|
// are surfaced in dispatch metadata for usage/audit purposes.
|
|
const (
|
|
principalSourceToken = "principal_token"
|
|
principalSourceProjection = "control_plane_projection"
|
|
principalSourceLegacy = "legacy_bearer_token"
|
|
principalSourceUnknown = "unknown"
|
|
)
|
|
|
|
type credentialMode uint8
|
|
|
|
const (
|
|
credentialModeLegacy credentialMode = iota
|
|
credentialModeManaged
|
|
)
|
|
|
|
// Dispatch metadata keys carrying the resolved principal. These are set from
|
|
// the authenticated request context only; metadata.user from the caller's
|
|
// request body is never used as an identity source.
|
|
const (
|
|
principalMetaRef = "iop_principal_ref"
|
|
principalMetaAlias = "iop_principal_alias"
|
|
principalMetaToken = "iop_token_ref"
|
|
principalMetaSource = "iop_principal_source"
|
|
routeMetaID = "iop_route_id"
|
|
routeMetaRevision = "iop_route_revision"
|
|
credentialSlotMeta = "iop_credential_slot_ref"
|
|
credentialRevisionMeta = "iop_credential_revision"
|
|
)
|
|
|
|
// openAIPrincipal is the caller identity resolved from an OpenAI-compatible
|
|
// bearer token via IOP bearer-token-operation mapping. It never carries the
|
|
// raw token value.
|
|
type openAIPrincipal struct {
|
|
PrincipalRef string
|
|
PrincipalAlias string
|
|
TokenRef string
|
|
Source string
|
|
}
|
|
|
|
type principalContextKey struct{}
|
|
type projectionViewContextKey struct{}
|
|
|
|
// withPrincipal returns a context carrying the resolved caller principal.
|
|
func withPrincipal(ctx context.Context, p openAIPrincipal) context.Context {
|
|
return context.WithValue(ctx, principalContextKey{}, p)
|
|
}
|
|
|
|
// principalFromContext returns the caller principal stored by withAuth, if
|
|
// any.
|
|
func principalFromContext(ctx context.Context) (openAIPrincipal, bool) {
|
|
p, ok := ctx.Value(principalContextKey{}).(openAIPrincipal)
|
|
return p, ok
|
|
}
|
|
|
|
func withAuthenticatedProjectionView(ctx context.Context, view authprojection.AuthenticatedView) context.Context {
|
|
return context.WithValue(ctx, projectionViewContextKey{}, view)
|
|
}
|
|
|
|
func authenticatedProjectionViewFromContext(ctx context.Context) (authprojection.AuthenticatedView, bool) {
|
|
view, ok := ctx.Value(projectionViewContextKey{}).(authprojection.AuthenticatedView)
|
|
return view, ok && view.State == authprojection.StateFresh
|
|
}
|
|
|
|
// principalMetadata renders the request context's principal as dispatch
|
|
// metadata. It returns nil when no principal was resolved for the context.
|
|
func principalMetadata(ctx context.Context) map[string]string {
|
|
p, ok := principalFromContext(ctx)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
meta := map[string]string{principalMetaSource: p.Source}
|
|
if p.PrincipalRef != "" {
|
|
meta[principalMetaRef] = p.PrincipalRef
|
|
}
|
|
if p.PrincipalAlias != "" {
|
|
meta[principalMetaAlias] = p.PrincipalAlias
|
|
}
|
|
if p.TokenRef != "" {
|
|
meta[principalMetaToken] = p.TokenRef
|
|
}
|
|
return meta
|
|
}
|
|
|
|
// bearerTokenFromHeader extracts the raw bearer token value from an
|
|
// Authorization header, or "" if the header is missing or malformed.
|
|
func bearerTokenFromHeader(authHeader string) string {
|
|
const prefix = "Bearer "
|
|
if !strings.HasPrefix(authHeader, prefix) {
|
|
return ""
|
|
}
|
|
return strings.TrimPrefix(authHeader, prefix)
|
|
}
|
|
|
|
// resolvePrincipal authenticates an OpenAI-compatible bearer token against
|
|
// cfg.PrincipalTokens (hash mapping, checked first) and cfg.BearerToken
|
|
// (legacy fallback). It returns the resolved principal and whether the
|
|
// caller is authenticated. When neither principal_tokens nor bearer_token is
|
|
// configured, the endpoint is open: authenticated=true with an
|
|
// unknown-source principal, matching the pre-existing no-auth default.
|
|
func resolvePrincipal(cfg config.EdgeOpenAIConf, authHeader string) (openAIPrincipal, bool) {
|
|
hasPrincipalTokens := len(cfg.PrincipalTokens) > 0
|
|
hasLegacyToken := strings.TrimSpace(cfg.BearerToken) != ""
|
|
if !hasPrincipalTokens && !hasLegacyToken {
|
|
return openAIPrincipal{Source: principalSourceUnknown}, true
|
|
}
|
|
|
|
token := bearerTokenFromHeader(authHeader)
|
|
if token == "" {
|
|
return openAIPrincipal{}, false
|
|
}
|
|
|
|
if hasPrincipalTokens {
|
|
if p, ok := matchPrincipalToken(cfg.PrincipalTokens, token); ok {
|
|
return p, true
|
|
}
|
|
}
|
|
|
|
if hasLegacyToken {
|
|
expected := []byte(cfg.BearerToken)
|
|
if subtle.ConstantTimeCompare([]byte(token), expected) == 1 {
|
|
return openAIPrincipal{Source: principalSourceLegacy}, true
|
|
}
|
|
}
|
|
|
|
return openAIPrincipal{}, false
|
|
}
|
|
|
|
// matchPrincipalToken hashes token and compares it against each configured
|
|
// entry's stored hash using a constant-time comparison per candidate.
|
|
func matchPrincipalToken(tokens []config.OpenAIPrincipalTokenConf, token string) (openAIPrincipal, bool) {
|
|
sum := sha256.Sum256([]byte(token))
|
|
hash := hex.EncodeToString(sum[:])
|
|
for _, t := range tokens {
|
|
configured := strings.ToLower(strings.TrimSpace(t.TokenHashSHA256))
|
|
if subtle.ConstantTimeCompare([]byte(hash), []byte(configured)) == 1 {
|
|
return openAIPrincipal{
|
|
PrincipalRef: t.PrincipalRef,
|
|
PrincipalAlias: t.PrincipalAlias,
|
|
TokenRef: t.TokenRef,
|
|
Source: principalSourceToken,
|
|
}, true
|
|
}
|
|
}
|
|
return openAIPrincipal{}, false
|
|
}
|
|
|
|
// principalFromRequest resolves r's caller principal from its Authorization
|
|
// header against cfg.
|
|
func principalTokenFromRequest(r *http.Request) (string, bool) {
|
|
authorization := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
bearer := bearerTokenFromHeader(authorization)
|
|
apiKey := ""
|
|
if isAnthropicRequest(r) {
|
|
apiKey = strings.TrimSpace(r.Header.Get("X-Api-Key"))
|
|
}
|
|
if authorization != "" && bearer == "" {
|
|
return "", false
|
|
}
|
|
if bearer != "" && apiKey != "" {
|
|
if subtle.ConstantTimeCompare([]byte(bearer), []byte(apiKey)) != 1 {
|
|
return "", false
|
|
}
|
|
}
|
|
token := bearer
|
|
if token == "" {
|
|
token = apiKey
|
|
}
|
|
return token, true
|
|
}
|
|
|
|
// principalFromRequest resolves r's caller principal from its supported
|
|
// surface-specific headers against the legacy static configuration.
|
|
func principalFromRequest(r *http.Request, cfg config.EdgeOpenAIConf) (openAIPrincipal, bool) {
|
|
token, ok := principalTokenFromRequest(r)
|
|
if !ok {
|
|
return openAIPrincipal{}, false
|
|
}
|
|
if token == "" {
|
|
return resolvePrincipal(cfg, "")
|
|
}
|
|
return resolvePrincipal(cfg, "Bearer "+token)
|
|
}
|
|
|
|
// authenticatePrincipal selects exactly one authentication source from the
|
|
// explicit credential mode. Managed mode requires a fresh shared projection;
|
|
// unavailable, unmanaged, expired, and revoked projection states fail closed.
|
|
func (s *Server) authenticatePrincipal(r *http.Request) (openAIPrincipal, authprojection.AuthenticatedView, bool) {
|
|
token, headersOK := principalTokenFromRequest(r)
|
|
if s.managedCredentialPlane() {
|
|
projection := s.PrincipalProjection()
|
|
if !headersOK || token == "" || projection == nil {
|
|
return openAIPrincipal{}, authprojection.AuthenticatedView{}, false
|
|
}
|
|
digest := sha256.Sum256([]byte(token))
|
|
view, matched := projection.AuthenticatedView(digest)
|
|
if view.State != authprojection.StateFresh || !matched {
|
|
return openAIPrincipal{}, view, false
|
|
}
|
|
return openAIPrincipal{
|
|
PrincipalRef: view.Principal.PrincipalRef, PrincipalAlias: view.Principal.PrincipalAlias,
|
|
TokenRef: view.Principal.TokenRef, Source: principalSourceProjection,
|
|
}, view, true
|
|
}
|
|
if !headersOK {
|
|
return openAIPrincipal{}, authprojection.AuthenticatedView{}, false
|
|
}
|
|
if token == "" {
|
|
p, ok := resolvePrincipal(s.cfg, "")
|
|
return p, authprojection.AuthenticatedView{}, ok
|
|
}
|
|
p, ok := resolvePrincipal(s.cfg, "Bearer "+token)
|
|
return p, authprojection.AuthenticatedView{}, ok
|
|
}
|