continuous-integration/drone Build is failing
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.
161 lines
5.5 KiB
Go
161 lines
5.5 KiB
Go
package model
|
|
|
|
import (
|
|
"errors"
|
|
"golang.org/x/crypto/ssh"
|
|
"net/netip"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type GatewayAccount struct {
|
|
TargetID string `json:"targetId,omitempty"`
|
|
TargetAccountID string `json:"targetAccountId,omitempty"`
|
|
ExpiresAt string `json:"expiresAt,omitempty"`
|
|
RotationReminderDays int `json:"rotationReminderDays,omitempty"`
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password,omitempty"`
|
|
Disabled bool `json:"disabled,omitempty"`
|
|
AccessMode string `json:"accessMode,omitempty"`
|
|
AllowCIDRs []string `json:"allowCIDRs,omitempty"`
|
|
AuthorizedKeys []string `json:"authorizedKeys,omitempty"`
|
|
AuthorizedKeysAssetID string `json:"authorizedKeysAssetId,omitempty"`
|
|
}
|
|
|
|
// Redis and MongoDB gateways with no client accounts accept downstream
|
|
// connections without a local authentication step. Every other gateway,
|
|
// requires gateway authentication.
|
|
func (p Proxy) RequiresGatewayAuthentication() bool {
|
|
return (p.Type != "redis" && p.Type != "mongodb") || len(p.GatewayAccounts) > 0
|
|
}
|
|
|
|
func (p Proxy) GatewayAccount(username string) (GatewayAccount, bool) {
|
|
for _, a := range p.GatewayAccounts {
|
|
if !a.Disabled && !credentialExpired(a.ExpiresAt, time.Now()) && a.Username == username {
|
|
return a, true
|
|
}
|
|
}
|
|
// Redis AUTH password authenticates the protocol's default user. An empty
|
|
// configured username is the explicit password-only form of that identity.
|
|
if p.Type == "redis" && username == "default" {
|
|
for _, a := range p.GatewayAccounts {
|
|
if !a.Disabled && !credentialExpired(a.ExpiresAt, time.Now()) && a.Username == "" {
|
|
return a, true
|
|
}
|
|
}
|
|
}
|
|
return GatewayAccount{}, false
|
|
}
|
|
func (a GatewayAccount) Apply(p Proxy) Proxy {
|
|
if a.TargetID != "" {
|
|
t, ok := p.Targets[a.TargetID]
|
|
if !ok && a.TargetID != p.TargetID {
|
|
p = (Target{}).Apply(p)
|
|
p.TargetCredentialError = ErrCredentialUnavailable
|
|
return p
|
|
}
|
|
p.TargetAccountID = ""
|
|
if ok {
|
|
p = t.Apply(p)
|
|
}
|
|
}
|
|
p = p.selectTargetAccount(a.TargetAccountID)
|
|
if a.Disabled || credentialExpired(a.ExpiresAt, time.Now()) {
|
|
p.TargetCredentialError = ErrCredentialUnavailable
|
|
}
|
|
p.GatewayUser, p.GatewayPassword = a.Username, a.Password
|
|
p.AccessMode = a.AccessMode
|
|
if p.AccessMode == "" && !IsSSH(p.Type) {
|
|
p.AccessMode = "readwrite"
|
|
}
|
|
p.SSH.AuthorizedKeys = a.AuthorizedKeys
|
|
return p
|
|
}
|
|
|
|
func (p Proxy) validateGatewayAccounts() error {
|
|
if len(p.GatewayAccounts) > 32 {
|
|
return errors.New("at most 32 gateway accounts")
|
|
}
|
|
ids, users := map[string]bool{}, map[string]bool{}
|
|
for _, a := range p.GatewayAccounts {
|
|
if err := validateCredentialExpiry(a.ExpiresAt, a.RotationReminderDays); err != nil {
|
|
return err
|
|
}
|
|
if !accountIDPattern.MatchString(a.ID) || ids[a.ID] {
|
|
return errors.New("invalid or duplicate gateway account id")
|
|
}
|
|
ids[a.ID] = true
|
|
passwordOnlyRedis := p.Type == "redis" && a.Username == "" && a.Password != ""
|
|
usernameKey := a.Username
|
|
if p.Type == "redis" && usernameKey == "" {
|
|
usernameKey = "default"
|
|
}
|
|
if (strings.TrimSpace(a.Username) == "" && !passwordOnlyRedis) || len(a.Username) > 128 || strings.ContainsAny(a.Username, "\r\n\x00") || users[usernameKey] {
|
|
return errors.New("invalid or duplicate gateway username")
|
|
}
|
|
users[usernameKey] = true
|
|
if a.Password == "" && (!IsSSH(p.Type) || (len(a.AuthorizedKeys) == 0 && a.AuthorizedKeysAssetID == "")) {
|
|
return errors.New("gateway account password is required")
|
|
}
|
|
if a.AccessMode != "" && a.AccessMode != "readonly" && a.AccessMode != "readwrite" && a.AccessMode != "unrestricted" {
|
|
return errors.New("invalid gateway account accessMode")
|
|
}
|
|
if a.AccessMode == "unrestricted" && p.Type != "mysql" {
|
|
return errors.New("unrestricted accessMode is supported only for MySQL")
|
|
}
|
|
if a.AccessMode == "readonly" && p.Type != "mysql" && p.Type != "mongodb" {
|
|
return errors.New("readonly is currently supported only for MySQL and MongoDB")
|
|
}
|
|
if IsSSH(p.Type) && a.AccessMode != "" {
|
|
return errors.New("SSH has no accessMode")
|
|
}
|
|
for _, cidr := range a.AllowCIDRs {
|
|
if _, e := netip.ParsePrefix(cidr); e != nil {
|
|
if _, e = netip.ParseAddr(cidr); e != nil {
|
|
return errors.New("invalid account source IP/CIDR")
|
|
}
|
|
}
|
|
}
|
|
for _, key := range a.AuthorizedKeys {
|
|
if _, _, _, _, e := ssh.ParseAuthorizedKey([]byte(key)); e != nil {
|
|
return errors.New("invalid gateway authorized key")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func mergeGatewaySecrets(current Proxy, patch map[string]any, out *Proxy) error {
|
|
raw, present := patch["gatewayAccounts"]
|
|
if !present {
|
|
return nil
|
|
}
|
|
entries, ok := raw.([]any)
|
|
if !ok || (len(entries) == 0 && out.Type != "tcp" && out.Type != "redis" && out.Type != "mongodb") {
|
|
return errors.New("gatewayAccounts must contain at least one account")
|
|
}
|
|
byID := map[string]GatewayAccount{}
|
|
for _, a := range current.GatewayAccounts {
|
|
byID[a.ID] = a
|
|
}
|
|
for i, entry := range entries {
|
|
fields, ok := entry.(map[string]any)
|
|
if !ok {
|
|
return errors.New("invalid gateway account")
|
|
}
|
|
if err := exactFields(fields, reflect.TypeOf(GatewayAccount{})); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := fields["password"]; !ok {
|
|
out.GatewayAccounts[i].Password = byID[out.GatewayAccounts[i].ID].Password
|
|
}
|
|
old := byID[out.GatewayAccounts[i].ID]
|
|
if old.AuthorizedKeysAssetID != "" && old.AuthorizedKeysAssetID != out.GatewayAccounts[i].AuthorizedKeysAssetID {
|
|
out.GatewayAccounts[i].AuthorizedKeys = nil
|
|
}
|
|
}
|
|
out.GatewayUser, out.GatewayPassword = "", ""
|
|
return nil
|
|
}
|