91 lines
3.4 KiB
Go
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
|
|
}
|