feat: define DWH installation credentials
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// Package record defines the persisted credential-record contract.
|
||||
package record
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
SchemaVersion = 1
|
||||
KindV1 Kind = "per_installation_v1"
|
||||
KindLegacyRaw Kind = "legacy_raw"
|
||||
LegacyKeyID = "legacy-shared"
|
||||
)
|
||||
|
||||
type Digest [32]byte
|
||||
|
||||
type Record struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
Kind Kind `json:"credential_kind"`
|
||||
KeyID string `json:"key_id"`
|
||||
InstallationID string `json:"installation_id"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SecretSHA256 string `json:"secret_sha256"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
RevokedAt *time.Time `json:"revoked_at,omitempty"`
|
||||
RevocationReason string `json:"revocation_reason,omitempty"`
|
||||
}
|
||||
|
||||
// Validate verifies that r conforms to the version 1 persisted-record contract.
|
||||
func Validate(r Record) error {
|
||||
if r.SchemaVersion != SchemaVersion {
|
||||
return fmt.Errorf("unsupported schema version %d", r.SchemaVersion)
|
||||
}
|
||||
if err := validateKindAndKeyID(r.Kind, r.KeyID); err != nil {
|
||||
return err
|
||||
}
|
||||
if !validInstallationID(r.InstallationID) {
|
||||
return errors.New("invalid installation ID")
|
||||
}
|
||||
if err := validateMetadata("description", r.Description); err != nil {
|
||||
return err
|
||||
}
|
||||
if !validDigest(r.SecretSHA256) {
|
||||
return errors.New("invalid secret SHA-256 digest")
|
||||
}
|
||||
if !validTimestamp(r.CreatedAt) {
|
||||
return errors.New("invalid creation timestamp")
|
||||
}
|
||||
if r.ExpiresAt != nil {
|
||||
if !validTimestamp(*r.ExpiresAt) {
|
||||
return errors.New("invalid expiry timestamp")
|
||||
}
|
||||
if !r.ExpiresAt.After(r.CreatedAt) {
|
||||
return errors.New("expiry must be after creation")
|
||||
}
|
||||
}
|
||||
if (r.RevokedAt == nil) != (r.RevocationReason == "") {
|
||||
return errors.New("revocation timestamp and reason must be paired")
|
||||
}
|
||||
if r.RevokedAt != nil && !validTimestamp(*r.RevokedAt) {
|
||||
return errors.New("invalid revocation timestamp")
|
||||
}
|
||||
return validateMetadata("revocation reason", r.RevocationReason)
|
||||
}
|
||||
|
||||
func validateKindAndKeyID(kind Kind, keyID string) error {
|
||||
switch kind {
|
||||
case KindV1:
|
||||
if !validV1KeyID(keyID) {
|
||||
return errors.New("invalid v1 key ID")
|
||||
}
|
||||
case KindLegacyRaw:
|
||||
if keyID != LegacyKeyID {
|
||||
return errors.New("legacy record must use the legacy key ID")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid credential kind %q", kind)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validV1KeyID(value string) bool {
|
||||
if len(value) != 16 {
|
||||
return false
|
||||
}
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(value)
|
||||
return err == nil && len(decoded) == 12 && base64.RawURLEncoding.EncodeToString(decoded) == value
|
||||
}
|
||||
|
||||
func validInstallationID(value string) bool {
|
||||
if len(value) < 1 || len(value) > 63 {
|
||||
return false
|
||||
}
|
||||
for i := range len(value) {
|
||||
c := value[i]
|
||||
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (i > 0 && c == '-') {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validDigest(value string) bool {
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(value)
|
||||
return err == nil && len(decoded) == 32 && base64.RawURLEncoding.EncodeToString(decoded) == value
|
||||
}
|
||||
|
||||
func validTimestamp(value time.Time) bool {
|
||||
return !value.IsZero() && value.Location() == time.UTC
|
||||
}
|
||||
|
||||
func validateMetadata(name, value string) error {
|
||||
if !utf8.ValidString(value) {
|
||||
return fmt.Errorf("%s is not valid UTF-8", name)
|
||||
}
|
||||
if utf8.RuneCountInString(value) > 160 {
|
||||
return fmt.Errorf("%s exceeds 160 characters", name)
|
||||
}
|
||||
if strings.IndexFunc(value, unicode.IsControl) >= 0 {
|
||||
return fmt.Errorf("%s contains a control character", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user