Files

91 lines
3.4 KiB
Go

package ai
import (
"encoding/json"
"errors"
"fmt"
"time"
"git.ipao.vip/rogee/go-sip/internal/contract"
)
type Authorization struct {
AuthorizationID string `json:"authorization_id"`
TenantID string `json:"tenant_id"`
TenantKey string `json:"tenant_key"`
AgentVersionID string `json:"agent_version_id"`
ConfigSHA256 string `json:"config_sha256"`
Mode Mode `json:"mode"`
IssuedAt string `json:"issued_at"`
ExpiresAt string `json:"expires_at"`
Source string `json:"source"`
CredentialRefs map[string]string `json:"credential_refs"`
AllowedEgressPoolIDs []string `json:"allowed_egress_pool_ids"`
Revoked bool `json:"revoked"`
RevocationReason string `json:"revocation_reason"`
}
// DecodeBoundAuthorization validates identity and immutable configuration binding.
// It deliberately preserves revoked/expired grants as facts, not permissions.
func DecodeBoundAuthorization(raw []byte, snapshot Snapshot, tenantID, tenantKey string) (Authorization, error) {
if err := contract.ValidateSourceSchema("ai-authorization.schema.json", raw); err != nil {
return Authorization{}, err
}
var authorization Authorization
if err := json.Unmarshal(raw, &authorization); err != nil {
return Authorization{}, err
}
if authorization.TenantID != tenantID || authorization.TenantKey != tenantKey {
return Authorization{}, errors.New("AI authorization tenant binding mismatch")
}
if authorization.AgentVersionID != snapshot.AgentVersionID || authorization.ConfigSHA256 != snapshot.Digest || authorization.Mode != snapshot.Mode {
return Authorization{}, errors.New("AI authorization does not match immutable snapshot")
}
issuedAt, err := time.Parse(time.RFC3339, authorization.IssuedAt)
if err != nil {
return Authorization{}, fmt.Errorf("parse AI authorization issued_at: %w", err)
}
expiresAt, err := time.Parse(time.RFC3339, authorization.ExpiresAt)
if err != nil {
return Authorization{}, fmt.Errorf("parse AI authorization expires_at: %w", err)
}
if !issuedAt.Before(expiresAt) {
return Authorization{}, errors.New("AI authorization has an invalid validity window")
}
return authorization, nil
}
func ValidateAuthorization(raw []byte, snapshot Snapshot, tenantID, tenantKey, egressPoolID string, now time.Time) (Authorization, error) {
authorization, err := DecodeBoundAuthorization(raw, snapshot, tenantID, tenantKey)
if err != nil {
return Authorization{}, err
}
if authorization.Revoked {
return Authorization{}, errors.New("AI authorization is revoked")
}
issuedAt, err := time.Parse(time.RFC3339, authorization.IssuedAt)
if err != nil {
return Authorization{}, err
}
expiresAt, err := time.Parse(time.RFC3339, authorization.ExpiresAt)
if err != nil {
return Authorization{}, err
}
if now.Before(issuedAt) || !now.Before(expiresAt) {
return Authorization{}, errors.New("AI authorization is outside its validity window")
}
if egressPoolID != "" {
allowed := false
for _, value := range authorization.AllowedEgressPoolIDs {
if value == egressPoolID {
allowed = true
break
}
}
if !allowed {
return Authorization{}, errors.New("AI authorization does not allow this egress pool")
}
}
return authorization, nil
}