iop/apps/edge/internal/openai/principal.go
toki 4c8441e6c9 feat(credential): Provider Credential Slot 라우팅을 구현한다
사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
2026-08-02 09:10:11 +09:00

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
}