Files
LinkPort/internal/model/credential_lifecycle.go
曾志威 9da95beb42
continuous-integration/drone Build is failing
feat: refine management UI, connection diagnostics and local startup
Migrate the workspace to React and Ant Design, improve account forms and responsive layouts, support outbound diagnostics and SSH fingerprint confirmation, and persist the management startup port. Preserve database migrations, remove S3 settings storage, and improve gateway audit handling.
2026-10-13 19:43:23 +08:00

122 lines
3.5 KiB
Go

package model
import (
"errors"
"time"
)
var ErrCredentialUnavailable = errors.New("credential expired or unavailable")
// CredentialDue uses seven days when reminderDays is zero. Empty expiry never
// expires; malformed timestamps fail closed, including for imported snapshots.
func CredentialDue(expiresAt string, reminderDays int, now time.Time) (due, expired bool) {
if expiresAt == "" {
return false, false
}
expiry, err := time.Parse(time.RFC3339, expiresAt)
if err != nil {
return true, true
}
if reminderDays == 0 {
reminderDays = 7
}
return !now.Before(expiry.Add(-time.Duration(reminderDays) * 24 * time.Hour)), !now.Before(expiry)
}
func credentialExpired(expiresAt string, now time.Time) bool {
_, expired := CredentialDue(expiresAt, 0, now)
return expired
}
func validateCredentialExpiry(expiresAt string, days int) error {
if days < 0 || days > 365 {
return errors.New("rotationReminderDays must be between 0 and 365 (0 defaults to 7)")
}
if expiresAt != "" {
if _, err := time.Parse(time.RFC3339, expiresAt); err != nil {
return errors.New("expiresAt must be RFC3339")
}
}
return nil
}
// ValidateAccountReferences permits incomplete, stopped gateways, but never
// dangling account references, even on disabled gateway accounts.
func (t Target) ValidateAccountReferences(p Proxy) error {
if p.TargetID == t.ID && p.TargetAccountID != "" && !t.HasAccount(p.TargetAccountID) {
return errors.New("target account not found")
}
for _, a := range p.GatewayAccounts {
targetID := a.TargetID
if targetID == "" {
targetID = p.TargetID
}
if targetID == t.ID && a.TargetAccountID != "" && !t.HasAccount(a.TargetAccountID) {
return errors.New("gateway target account not found")
}
}
return nil
}
func (p Proxy) selectTargetAccount(override string) Proxy {
if override != "" {
p.TargetAccountID = override
}
if len(p.TargetAccounts) == 0 && override == "" && (p.Type == "tcp" || p.Type == "redis" || p.Type == "mongodb") {
p.TargetUser, p.TargetPassword = "", ""
return p
}
for _, a := range p.TargetAccounts {
if a.ID == p.TargetAccountID {
return a.Apply(p)
}
}
p.TargetUser, p.TargetPassword = "", ""
p.SSH.PrivateKey, p.SSH.Passphrase = "", ""
p.SSH.PrivateKeyAssetID, p.SSH.Certificate = "", ""
p.TargetCredentialError = ErrCredentialUnavailable
return p
}
func (p Proxy) CheckTargetCredential() error {
if p.TargetCredentialError != nil {
return p.TargetCredentialError
}
if credentialExpired(p.TargetCredentialExpiresAt, time.Now()) {
return ErrCredentialUnavailable
}
return nil
}
// ValidateTargetSelection checks completeness without rejecting a whole gateway
// when one account is expired. Each authentication independently enforces expiry.
func (p Proxy) ValidateTargetSelection() error {
if p.TCPForward != nil {
return p.ValidateTCPForward()
}
if p.Type == "tcp" {
if p.TargetID == "" || p.TargetAccountID != "" || len(p.GatewayAccounts) != 0 {
return errors.New("select a TCP target without accounts")
}
return nil
}
t := Target{ID: p.TargetID, Accounts: p.TargetAccounts}
if err := t.ValidateAccountReferences(p); err != nil {
return err
}
if len(p.GatewayAccounts) == 0 {
if !p.ValidTargetAccountSelection() {
return errors.New("select a target account")
}
return nil
}
for _, a := range p.GatewayAccounts {
if a.Disabled {
continue
}
selected := a.Apply(p)
if selected.TargetID == "" || !selected.ValidTargetAccountSelection() {
return errors.New("select a target account for each enabled gateway account")
}
}
return nil
}