143 lines
4.6 KiB
Go
143 lines
4.6 KiB
Go
package openai
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"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"
|
|
principalSourceLegacy = "legacy_bearer_token"
|
|
principalSourceUnknown = "unknown"
|
|
)
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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{}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 principalFromRequest(r *http.Request, cfg config.EdgeOpenAIConf) (openAIPrincipal, bool) {
|
|
return resolvePrincipal(cfg, r.Header.Get("Authorization"))
|
|
}
|