Compare commits

..

3 Commits

14 changed files with 325 additions and 91 deletions
+119 -40
View File
@@ -13,6 +13,21 @@ import "codit/internal/util"
var ErrSSHServerHasSessionHistory error = errors.New("ssh server has session history") var ErrSSHServerHasSessionHistory error = errors.New("ssh server has session history")
type SSHServerDeleteReferenceError struct {
Kind string
Count int64
}
func (e *SSHServerDeleteReferenceError) Error() string {
var suffix string
suffix = ""
if e.Count != 1 {
suffix = "s"
}
return fmt.Sprintf("cannot delete ssh server because it is used by %d %s%s", e.Count, e.Kind, suffix)
}
func (s *Store) ListSSHServers() ([]models.SSHServer, error) { func (s *Store) ListSSHServers() ([]models.SSHServer, error) {
var rows *sql.Rows var rows *sql.Rows
var err error var err error
@@ -20,7 +35,7 @@ func (s *Store) ListSSHServers() ([]models.SSHServer, error) {
var item models.SSHServer var item models.SSHServer
var tagsJSON string var tagsJSON string
rows, err = s.Query(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at rows, err = s.Query(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
FROM ssh_servers FROM ssh_servers
ORDER BY name, host, port`) ORDER BY name, host, port`)
if err != nil { if err != nil {
@@ -29,7 +44,7 @@ func (s *Store) ListSSHServers() ([]models.SSHServer, error) {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
err = rows.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt) err = rows.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.OwnerScope, &item.OwnerUserID, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -52,10 +67,10 @@ func (s *Store) GetSSHServer(id string) (models.SSHServer, error) {
var tagsJSON string var tagsJSON string
var err error var err error
row = s.QueryRow(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at row = s.QueryRow(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
FROM ssh_servers FROM ssh_servers
WHERE public_id = ?`, strings.TrimSpace(id)) WHERE public_id = ?`, strings.TrimSpace(id))
err = row.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt) err = row.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.OwnerScope, &item.OwnerUserID, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
if err != nil { if err != nil {
return item, err return item, err
} }
@@ -75,9 +90,9 @@ func (s *Store) ListSSHServersForUser(userID string) ([]models.SSHServer, error)
var trimmedUserID string var trimmedUserID string
trimmedUserID = strings.TrimSpace(userID) trimmedUserID = strings.TrimSpace(userID)
rows, err = s.Query(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at rows, err = s.Query(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
FROM ssh_servers FROM ssh_servers
WHERE created_by_kind = 'user' AND created_by_subject_id = ? WHERE owner_scope = 'user' AND owner_user_public_id = ?
ORDER BY name, host, port`, trimmedUserID) ORDER BY name, host, port`, trimmedUserID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -85,7 +100,7 @@ func (s *Store) ListSSHServersForUser(userID string) ([]models.SSHServer, error)
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
err = rows.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt) err = rows.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.OwnerScope, &item.OwnerUserID, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -93,7 +108,7 @@ func (s *Store) ListSSHServersForUser(userID string) ([]models.SSHServer, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
item.Editable = item.CreatedByKind == "user" && item.CreatedBySubjectID == trimmedUserID item.Editable = item.OwnerScope == "user" && item.OwnerUserID == trimmedUserID
items = append(items, item) items = append(items, item)
} }
err = rows.Err() err = rows.Err()
@@ -133,7 +148,7 @@ func (s *Store) GetOwnedSSHServerForUser(userID string, id string) (models.SSHSe
if err != nil { if err != nil {
return item, err return item, err
} }
item.Editable = item.CreatedByKind == "user" && item.CreatedBySubjectID == trimmedUserID item.Editable = item.OwnerScope == "user" && item.OwnerUserID == trimmedUserID
if !item.Editable { if !item.Editable {
return item, sql.ErrNoRows return item, sql.ErrNoRows
} }
@@ -152,6 +167,14 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
if item.Port <= 0 { item.Port = 22 } if item.Port <= 0 { item.Port = 22 }
item.HostKeyPolicy = normalizeSSHServerHostKeyPolicy(item.HostKeyPolicy) item.HostKeyPolicy = normalizeSSHServerHostKeyPolicy(item.HostKeyPolicy)
item.Tags = normalizeStringList(item.Tags) item.Tags = normalizeStringList(item.Tags)
item.OwnerScope = strings.TrimSpace(item.OwnerScope)
if item.OwnerScope != "user" {
item.OwnerScope = "admin"
item.OwnerUserID = ""
}
if item.OwnerScope == "user" && strings.TrimSpace(item.OwnerUserID) == "" {
return item, errors.New("owner_user_id is required")
}
tagsJSON, err = encodeStringList(item.Tags) tagsJSON, err = encodeStringList(item.Tags)
if err != nil { return item, err } if err != nil { return item, err }
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
@@ -167,12 +190,14 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
tags_json, tags_json,
enabled, enabled,
host_key_policy, host_key_policy,
owner_scope,
owner_user_public_id,
created_by_kind, created_by_kind,
created_by_subject_id, created_by_subject_id,
created_by_subject_name, created_by_subject_name,
created_at, created_at,
updated_at updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
item.ID, item.ID,
strings.TrimSpace(item.Name), strings.TrimSpace(item.Name),
strings.TrimSpace(item.Host), strings.TrimSpace(item.Host),
@@ -181,6 +206,8 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
tagsJSON, tagsJSON,
item.Enabled, item.Enabled,
item.HostKeyPolicy, item.HostKeyPolicy,
strings.TrimSpace(item.OwnerScope),
strings.TrimSpace(item.OwnerUserID),
strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedByKind),
strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectID),
strings.TrimSpace(item.CreatedBySubjectName), strings.TrimSpace(item.CreatedBySubjectName),
@@ -224,14 +251,28 @@ func (s *Store) DeleteSSHServer(id string) error {
var err error var err error
var row *sql.Row var row *sql.Row
var count int64 var count int64
var trimmedID string
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_sessions WHERE server_public_id = ?`, strings.TrimSpace(id)) trimmedID = strings.TrimSpace(id)
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_access_profiles WHERE server_public_id = ?`, trimmedID)
err = row.Scan(&count) err = row.Scan(&count)
if err != nil { return err } if err != nil { return err }
if count > 0 { if count > 0 {
return ErrSSHServerHasSessionHistory return &SSHServerDeleteReferenceError{Kind: "SSH access profile", Count: count}
} }
_, err = s.Exec(`DELETE FROM ssh_servers WHERE public_id = ?`, strings.TrimSpace(id)) row = s.QueryRow(`SELECT COUNT(*) FROM ssh_server_group_members WHERE server_public_id = ?`, trimmedID)
err = row.Scan(&count)
if err != nil { return err }
if count > 0 {
return &SSHServerDeleteReferenceError{Kind: "SSH server group membership", Count: count}
}
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_sessions WHERE server_public_id = ?`, trimmedID)
err = row.Scan(&count)
if err != nil { return err }
if count > 0 {
return &SSHServerDeleteReferenceError{Kind: "SSH session history record", Count: count}
}
_, err = s.Exec(`DELETE FROM ssh_servers WHERE public_id = ?`, trimmedID)
return err return err
} }
@@ -272,8 +313,10 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
p.auth_public_key_fingerprint, p.auth_public_key_fingerprint,
COALESCE(p.ssh_user_ca_public_id, ''), COALESCE(p.ssh_user_ca_public_id, ''),
p.ssh_principal_grant_ids_json, p.ssh_principal_grant_ids_json,
p.default_valid_seconds, p.default_cert_valid_seconds,
p.max_valid_seconds, p.max_cert_valid_seconds,
p.valid_after,
p.valid_before,
p.created_by_kind, p.created_by_kind,
p.created_by_subject_id, p.created_by_subject_id,
p.created_by_subject_name, p.created_by_subject_name,
@@ -287,6 +330,8 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
s.tags_json, s.tags_json,
s.enabled, s.enabled,
s.host_key_policy, s.host_key_policy,
s.owner_scope,
s.owner_user_public_id,
s.created_by_kind, s.created_by_kind,
s.created_by_subject_id, s.created_by_subject_id,
s.created_by_subject_name, s.created_by_subject_name,
@@ -330,8 +375,10 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
&item.AuthPublicKeyFingerprint, &item.AuthPublicKeyFingerprint,
&item.SSHUserCAID, &item.SSHUserCAID,
&grantIDsJSON, &grantIDsJSON,
&item.DefaultValidSeconds, &item.DefaultCertValidSeconds,
&item.MaxValidSeconds, &item.MaxCertValidSeconds,
&item.ValidAfter,
&item.ValidBefore,
&item.CreatedByKind, &item.CreatedByKind,
&item.CreatedBySubjectID, &item.CreatedBySubjectID,
&item.CreatedBySubjectName, &item.CreatedBySubjectName,
@@ -345,6 +392,8 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
&serverTagsJSON, &serverTagsJSON,
&item.Server.Enabled, &item.Server.Enabled,
&item.Server.HostKeyPolicy, &item.Server.HostKeyPolicy,
&item.Server.OwnerScope,
&item.Server.OwnerUserID,
&item.Server.CreatedByKind, &item.Server.CreatedByKind,
&item.Server.CreatedBySubjectID, &item.Server.CreatedBySubjectID,
&item.Server.CreatedBySubjectName, &item.Server.CreatedBySubjectName,
@@ -408,8 +457,10 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
p.auth_public_key_fingerprint, p.auth_public_key_fingerprint,
COALESCE(p.ssh_user_ca_public_id, ''), COALESCE(p.ssh_user_ca_public_id, ''),
p.ssh_principal_grant_ids_json, p.ssh_principal_grant_ids_json,
p.default_valid_seconds, p.default_cert_valid_seconds,
p.max_valid_seconds, p.max_cert_valid_seconds,
p.valid_after,
p.valid_before,
p.created_by_kind, p.created_by_kind,
p.created_by_subject_id, p.created_by_subject_id,
p.created_by_subject_name, p.created_by_subject_name,
@@ -423,6 +474,8 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
s.tags_json, s.tags_json,
s.enabled, s.enabled,
s.host_key_policy, s.host_key_policy,
s.owner_scope,
s.owner_user_public_id,
s.created_by_kind, s.created_by_kind,
s.created_by_subject_id, s.created_by_subject_id,
s.created_by_subject_name, s.created_by_subject_name,
@@ -462,8 +515,10 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
&item.AuthPublicKeyFingerprint, &item.AuthPublicKeyFingerprint,
&item.SSHUserCAID, &item.SSHUserCAID,
&grantIDsJSON, &grantIDsJSON,
&item.DefaultValidSeconds, &item.DefaultCertValidSeconds,
&item.MaxValidSeconds, &item.MaxCertValidSeconds,
&item.ValidAfter,
&item.ValidBefore,
&item.CreatedByKind, &item.CreatedByKind,
&item.CreatedBySubjectID, &item.CreatedBySubjectID,
&item.CreatedBySubjectName, &item.CreatedBySubjectName,
@@ -477,6 +532,8 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
&serverTagsJSON, &serverTagsJSON,
&item.Server.Enabled, &item.Server.Enabled,
&item.Server.HostKeyPolicy, &item.Server.HostKeyPolicy,
&item.Server.OwnerScope,
&item.Server.OwnerUserID,
&item.Server.CreatedByKind, &item.Server.CreatedByKind,
&item.Server.CreatedBySubjectID, &item.Server.CreatedBySubjectID,
&item.Server.CreatedBySubjectName, &item.Server.CreatedBySubjectName,
@@ -531,8 +588,10 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
item.ID, err = util.NewID() item.ID, err = util.NewID()
if err != nil { return item, err } if err != nil { return item, err }
} }
if item.DefaultValidSeconds <= 0 { item.DefaultValidSeconds = 3600 } if item.DefaultCertValidSeconds <= 0 { item.DefaultCertValidSeconds = 3600 }
if item.MaxValidSeconds <= 0 { item.MaxValidSeconds = item.DefaultValidSeconds } if item.MaxCertValidSeconds <= 0 { item.MaxCertValidSeconds = item.DefaultCertValidSeconds }
if item.ValidAfter < 0 { item.ValidAfter = 0 }
if item.ValidBefore < 0 { item.ValidBefore = 0 }
item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs) item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs)
grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs) grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs)
@@ -593,14 +652,16 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
auth_public_key_fingerprint, auth_public_key_fingerprint,
ssh_user_ca_public_id, ssh_user_ca_public_id,
ssh_principal_grant_ids_json, ssh_principal_grant_ids_json,
default_valid_seconds, default_cert_valid_seconds,
max_valid_seconds, max_cert_valid_seconds,
valid_after,
valid_before,
created_by_kind, created_by_kind,
created_by_subject_id, created_by_subject_id,
created_by_subject_name, created_by_subject_name,
created_at, created_at,
updated_at updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
item.ID, item.ID,
strings.TrimSpace(item.ServerID), strings.TrimSpace(item.ServerID),
strings.TrimSpace(item.ServerTargetType), strings.TrimSpace(item.ServerTargetType),
@@ -620,8 +681,10 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
strings.TrimSpace(item.AuthPublicKeyFingerprint), strings.TrimSpace(item.AuthPublicKeyFingerprint),
emptyStringToNil(item.SSHUserCAID), emptyStringToNil(item.SSHUserCAID),
grantIDsJSON, grantIDsJSON,
item.DefaultValidSeconds, item.DefaultCertValidSeconds,
item.MaxValidSeconds, item.MaxCertValidSeconds,
item.ValidAfter,
item.ValidBefore,
strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedByKind),
strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectID),
strings.TrimSpace(item.CreatedBySubjectName), strings.TrimSpace(item.CreatedBySubjectName),
@@ -674,8 +737,10 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
return item, errors.New("auth_method is required") return item, errors.New("auth_method is required")
} }
if item.DefaultValidSeconds <= 0 { item.DefaultValidSeconds = 3600 } if item.DefaultCertValidSeconds <= 0 { item.DefaultCertValidSeconds = 3600 }
if item.MaxValidSeconds <= 0 { item.MaxValidSeconds = item.DefaultValidSeconds } if item.MaxCertValidSeconds <= 0 { item.MaxCertValidSeconds = item.DefaultCertValidSeconds }
if item.ValidAfter < 0 { item.ValidAfter = 0 }
if item.ValidBefore < 0 { item.ValidBefore = 0 }
item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs) item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs)
grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs) grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs)
@@ -723,7 +788,7 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
item.SecretID = secret.ID item.SecretID = secret.ID
} }
_, err = tx.Exec(`UPDATE ssh_access_profiles _, err = tx.Exec(`UPDATE ssh_access_profiles
SET server_public_id = ?, server_target_type = ?, server_group_public_id = ?, name = ?, description = ?, remote_username = ?, auth_method = ?, second_factor_mode = ?, owner_scope = ?, owner_user_public_id = ?, allow_user_edit = ?, enabled = ?, secret_public_id = ?, ssh_credential_public_id = ?, auth_public_key = ?, auth_public_key_fingerprint = ?, ssh_user_ca_public_id = ?, ssh_principal_grant_ids_json = ?, default_valid_seconds = ?, max_valid_seconds = ?, updated_at = ? SET server_public_id = ?, server_target_type = ?, server_group_public_id = ?, name = ?, description = ?, remote_username = ?, auth_method = ?, second_factor_mode = ?, owner_scope = ?, owner_user_public_id = ?, allow_user_edit = ?, enabled = ?, secret_public_id = ?, ssh_credential_public_id = ?, auth_public_key = ?, auth_public_key_fingerprint = ?, ssh_user_ca_public_id = ?, ssh_principal_grant_ids_json = ?, default_cert_valid_seconds = ?, max_cert_valid_seconds = ?, valid_after = ?, valid_before = ?, updated_at = ?
WHERE public_id = ?`, WHERE public_id = ?`,
strings.TrimSpace(item.ServerID), strings.TrimSpace(item.ServerID),
strings.TrimSpace(item.ServerTargetType), strings.TrimSpace(item.ServerTargetType),
@@ -743,8 +808,10 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
strings.TrimSpace(item.AuthPublicKeyFingerprint), strings.TrimSpace(item.AuthPublicKeyFingerprint),
emptyStringToNil(item.SSHUserCAID), emptyStringToNil(item.SSHUserCAID),
grantIDsJSON, grantIDsJSON,
item.DefaultValidSeconds, item.DefaultCertValidSeconds,
item.MaxValidSeconds, item.MaxCertValidSeconds,
item.ValidAfter,
item.ValidBefore,
item.UpdatedAt, item.UpdatedAt,
strings.TrimSpace(item.ID), strings.TrimSpace(item.ID),
) )
@@ -852,8 +919,10 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
p.auth_public_key_fingerprint, p.auth_public_key_fingerprint,
COALESCE(p.ssh_user_ca_public_id, ''), COALESCE(p.ssh_user_ca_public_id, ''),
p.ssh_principal_grant_ids_json, p.ssh_principal_grant_ids_json,
p.default_valid_seconds, p.default_cert_valid_seconds,
p.max_valid_seconds, p.max_cert_valid_seconds,
p.valid_after,
p.valid_before,
p.created_by_kind, p.created_by_kind,
p.created_by_subject_id, p.created_by_subject_id,
p.created_by_subject_name, p.created_by_subject_name,
@@ -867,6 +936,8 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
s.tags_json, s.tags_json,
s.enabled, s.enabled,
s.host_key_policy, s.host_key_policy,
s.owner_scope,
s.owner_user_public_id,
s.created_by_kind, s.created_by_kind,
s.created_by_subject_id, s.created_by_subject_id,
s.created_by_subject_name, s.created_by_subject_name,
@@ -880,13 +951,15 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.public_id = t.target_public_id LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.public_id = t.target_public_id
LEFT JOIN user_group_members gm ON t.target_type = 'group' AND gm.group_id = ug.id LEFT JOIN user_group_members gm ON t.target_type = 'group' AND gm.group_id = ug.id
LEFT JOIN users gu ON gm.user_id = gu.id LEFT JOIN users gu ON gm.user_id = gu.id
WHERE p.enabled = 1 WHERE (
AND s.enabled = 1
AND (
(p.owner_scope = 'user' AND p.owner_user_public_id = ?) (p.owner_scope = 'user' AND p.owner_user_public_id = ?)
OR OR
( (
p.owner_scope = 'admin_shared' p.owner_scope = 'admin_shared'
AND p.enabled = 1
AND s.enabled = 1
AND (p.valid_after = 0 OR p.valid_after <= strftime('%s','now'))
AND (p.valid_before = 0 OR p.valid_before >= strftime('%s','now'))
AND ( AND (
(t.target_type = 'user' AND t.target_public_id = ?) (t.target_type = 'user' AND t.target_public_id = ?)
OR OR
@@ -930,8 +1003,10 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
&item.AuthPublicKeyFingerprint, &item.AuthPublicKeyFingerprint,
&item.SSHUserCAID, &item.SSHUserCAID,
&grantIDsJSON, &grantIDsJSON,
&item.DefaultValidSeconds, &item.DefaultCertValidSeconds,
&item.MaxValidSeconds, &item.MaxCertValidSeconds,
&item.ValidAfter,
&item.ValidBefore,
&item.CreatedByKind, &item.CreatedByKind,
&item.CreatedBySubjectID, &item.CreatedBySubjectID,
&item.CreatedBySubjectName, &item.CreatedBySubjectName,
@@ -945,6 +1020,8 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
&serverTagsJSON, &serverTagsJSON,
&item.Server.Enabled, &item.Server.Enabled,
&item.Server.HostKeyPolicy, &item.Server.HostKeyPolicy,
&item.Server.OwnerScope,
&item.Server.OwnerUserID,
&item.Server.CreatedByKind, &item.Server.CreatedByKind,
&item.Server.CreatedBySubjectID, &item.Server.CreatedBySubjectID,
&item.Server.CreatedBySubjectName, &item.Server.CreatedBySubjectName,
@@ -1401,6 +1478,8 @@ func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, err
payload, payload,
password, password,
metadata_json, metadata_json,
owner_scope,
owner_user_public_id,
created_by_kind, created_by_kind,
created_by_subject_id, created_by_subject_id,
created_by_subject_name, created_by_subject_name,
+2 -2
View File
@@ -72,7 +72,7 @@ func (s *Store) ListSSHServersForGroup(groupID string) ([]models.SSHServer, erro
var tagsJSON string var tagsJSON string
var err error var err error
rows, err = s.Query(`SELECT s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.host_key_policy, s.created_by_kind, s.created_by_subject_id, s.created_by_subject_name, s.created_at, s.updated_at rows, err = s.Query(`SELECT s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.host_key_policy, s.owner_scope, s.owner_user_public_id, s.created_by_kind, s.created_by_subject_id, s.created_by_subject_name, s.created_at, s.updated_at
FROM ssh_server_group_members gm FROM ssh_server_group_members gm
JOIN ssh_servers s ON s.public_id = gm.server_public_id JOIN ssh_servers s ON s.public_id = gm.server_public_id
WHERE gm.group_public_id = ? WHERE gm.group_public_id = ?
@@ -80,7 +80,7 @@ func (s *Store) ListSSHServersForGroup(groupID string) ([]models.SSHServer, erro
if err != nil { return nil, err } if err != nil { return nil, err }
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
err = rows.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt) err = rows.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.OwnerScope, &item.OwnerUserID, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
if err != nil { return nil, err } if err != nil { return nil, err }
item.Tags, err = decodeStringList(tagsJSON) item.Tags, err = decodeStringList(tagsJSON)
if err != nil { return nil, err } if err != nil { return nil, err }
+39 -11
View File
@@ -62,8 +62,10 @@ type sshAccessProfileRequest struct {
PasswordText string `json:"password_text"` PasswordText string `json:"password_text"`
SSHUserCAID string `json:"ssh_user_ca_id"` SSHUserCAID string `json:"ssh_user_ca_id"`
SSHPrincipalGrantIDs []string `json:"ssh_principal_grant_ids"` SSHPrincipalGrantIDs []string `json:"ssh_principal_grant_ids"`
DefaultValidSeconds int64 `json:"default_valid_seconds"` DefaultCertValidSeconds int64 `json:"default_cert_valid_seconds"`
MaxValidSeconds int64 `json:"max_valid_seconds"` MaxCertValidSeconds int64 `json:"max_cert_valid_seconds"`
ValidAfter int64 `json:"valid_after"`
ValidBefore int64 `json:"valid_before"`
Targets []sshAccessProfileTargetRequest `json:"targets"` Targets []sshAccessProfileTargetRequest `json:"targets"`
} }
@@ -344,6 +346,8 @@ func (api *API) CreateSSHServerAdmin(w http.ResponseWriter, r *http.Request, _ m
Tags: normalizeSSHBrokerTags(req.Tags), Tags: normalizeSSHBrokerTags(req.Tags),
Enabled: req.Enabled, Enabled: req.Enabled,
HostKeyPolicy: normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy), HostKeyPolicy: normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy),
OwnerScope: "admin",
OwnerUserID: "",
CreatedByKind: actorKind, CreatedByKind: actorKind,
CreatedBySubjectID: actorID, CreatedBySubjectID: actorID,
CreatedBySubjectName: actorName, CreatedBySubjectName: actorName,
@@ -410,12 +414,17 @@ func (api *API) UpdateSSHServerAdmin(w http.ResponseWriter, r *http.Request, par
func (api *API) DeleteSSHServerAdmin(w http.ResponseWriter, r *http.Request, params map[string]string) { func (api *API) DeleteSSHServerAdmin(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error var err error
var referenceErr *db.SSHServerDeleteReferenceError
if !api.requireAdmin(w, r) { if !api.requireAdmin(w, r) {
return return
} }
err = api.store(r).DeleteSSHServer(params["id"]) err = api.store(r).DeleteSSHServer(params["id"])
if err != nil { if err != nil {
if errors.As(err, &referenceErr) {
WriteJSON(w, http.StatusConflict, map[string]string{"error": err.Error() + "; remove the references first or disable the server instead"})
return
}
if err == db.ErrSSHServerHasSessionHistory { if err == db.ErrSSHServerHasSessionHistory {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete ssh server while session history exists; disable it instead"}) WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete ssh server while session history exists; disable it instead"})
return return
@@ -1114,6 +1123,8 @@ func (api *API) CreateSSHServerForSelf(w http.ResponseWriter, r *http.Request, _
Tags: normalizeSSHBrokerTags(req.Tags), Tags: normalizeSSHBrokerTags(req.Tags),
Enabled: req.Enabled, Enabled: req.Enabled,
HostKeyPolicy: normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy), HostKeyPolicy: normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy),
OwnerScope: "user",
OwnerUserID: user.ID,
CreatedByKind: "user", CreatedByKind: "user",
CreatedBySubjectID: user.ID, CreatedBySubjectID: user.ID,
CreatedBySubjectName: user.Username, CreatedBySubjectName: user.Username,
@@ -1189,6 +1200,7 @@ func (api *API) DeleteSSHServerForSelf(w http.ResponseWriter, r *http.Request, p
var user models.User var user models.User
var ok bool var ok bool
var err error var err error
var referenceErr *db.SSHServerDeleteReferenceError
user, ok = middleware.UserFromContext(r.Context()) user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled { if !ok || user.Disabled {
@@ -1206,6 +1218,10 @@ func (api *API) DeleteSSHServerForSelf(w http.ResponseWriter, r *http.Request, p
} }
err = api.store(r).DeleteSSHServer(params["id"]) err = api.store(r).DeleteSSHServer(params["id"])
if err != nil { if err != nil {
if errors.As(err, &referenceErr) {
WriteJSON(w, http.StatusConflict, map[string]string{"error": err.Error() + "; remove the references first or disable the server instead"})
return
}
if err == db.ErrSSHServerHasSessionHistory { if err == db.ErrSSHServerHasSessionHistory {
WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete ssh server while session history exists; disable it instead"}) WriteJSON(w, http.StatusConflict, map[string]string{"error": "cannot delete ssh server while session history exists; disable it instead"})
return return
@@ -2060,14 +2076,24 @@ func (api *API) normalizeSSHAccessProfileRequest(r *http.Request, req sshAccessP
publicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))) publicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(signer.PublicKey())) fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(signer.PublicKey()))
} }
if req.DefaultValidSeconds <= 0 { if req.DefaultCertValidSeconds <= 0 {
req.DefaultValidSeconds = 3600 req.DefaultCertValidSeconds = 3600
} }
if req.MaxValidSeconds <= 0 { if req.MaxCertValidSeconds <= 0 {
req.MaxValidSeconds = req.DefaultValidSeconds req.MaxCertValidSeconds = req.DefaultCertValidSeconds
} }
if req.MaxValidSeconds < req.DefaultValidSeconds { if req.MaxCertValidSeconds < req.DefaultCertValidSeconds {
return item, errors.New("max_valid_seconds must be greater than or equal to default_valid_seconds") return item, errors.New("max_cert_valid_seconds must be greater than or equal to default_cert_valid_seconds")
}
if options.SelfService {
req.ValidAfter = 0
req.ValidBefore = 0
}
if req.ValidAfter < 0 || req.ValidBefore < 0 {
return item, errors.New("valid_after and valid_before must be zero or positive")
}
if req.ValidAfter > 0 && req.ValidBefore > 0 && req.ValidAfter > req.ValidBefore {
return item, errors.New("valid_after must be less than or equal to valid_before")
} }
if req.ServerTargetType == "group" { if req.ServerTargetType == "group" {
var group models.SSHServerGroup var group models.SSHServerGroup
@@ -2121,8 +2147,10 @@ func (api *API) normalizeSSHAccessProfileRequest(r *http.Request, req sshAccessP
item.Enabled = req.Enabled item.Enabled = req.Enabled
item.SSHUserCAID = req.SSHUserCAID item.SSHUserCAID = req.SSHUserCAID
item.SSHPrincipalGrantIDs = req.SSHPrincipalGrantIDs item.SSHPrincipalGrantIDs = req.SSHPrincipalGrantIDs
item.DefaultValidSeconds = req.DefaultValidSeconds item.DefaultCertValidSeconds = req.DefaultCertValidSeconds
item.MaxValidSeconds = req.MaxValidSeconds item.MaxCertValidSeconds = req.MaxCertValidSeconds
item.ValidAfter = req.ValidAfter
item.ValidBefore = req.ValidBefore
item.Targets = targets item.Targets = targets
if !isUpdate { if !isUpdate {
item.CreatedByKind = actorKind item.CreatedByKind = actorKind
@@ -3336,7 +3364,7 @@ func (api *API) buildSSHSessionAuth(store *db.Store, profile models.SSHAccessPro
principals, err = api.resolveSSHAccessProfilePrincipals(store, profile) principals, err = api.resolveSSHAccessProfilePrincipals(store, profile)
if err != nil { return nil, err } if err != nil { return nil, err }
signed, err = api.signSSHUserKeyWithCA(store, ca, profile.AuthPublicKey, "", principals, profile.DefaultValidSeconds, profile.MaxValidSeconds) signed, err = api.signSSHUserKeyWithCA(store, ca, profile.AuthPublicKey, "", principals, profile.DefaultCertValidSeconds, profile.MaxCertValidSeconds)
if err != nil { return nil, err } if err != nil { return nil, err }
certKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(signed.Certificate))) certKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(signed.Certificate)))
+6 -2
View File
@@ -527,6 +527,8 @@ type SSHServer struct {
Tags []string `json:"tags"` Tags []string `json:"tags"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
HostKeyPolicy string `json:"host_key_policy"` HostKeyPolicy string `json:"host_key_policy"`
OwnerScope string `json:"owner_scope"`
OwnerUserID string `json:"owner_user_id"`
CreatedByKind string `json:"created_by_kind"` CreatedByKind string `json:"created_by_kind"`
CreatedBySubjectID string `json:"created_by_subject_id"` CreatedBySubjectID string `json:"created_by_subject_id"`
CreatedBySubjectName string `json:"created_by_subject_name"` CreatedBySubjectName string `json:"created_by_subject_name"`
@@ -614,8 +616,10 @@ type SSHAccessProfile struct {
AuthPublicKeyFingerprint string `json:"auth_public_key_fingerprint"` AuthPublicKeyFingerprint string `json:"auth_public_key_fingerprint"`
SSHUserCAID string `json:"ssh_user_ca_id"` SSHUserCAID string `json:"ssh_user_ca_id"`
SSHPrincipalGrantIDs []string `json:"ssh_principal_grant_ids"` SSHPrincipalGrantIDs []string `json:"ssh_principal_grant_ids"`
DefaultValidSeconds int64 `json:"default_valid_seconds"` DefaultCertValidSeconds int64 `json:"default_cert_valid_seconds"`
MaxValidSeconds int64 `json:"max_valid_seconds"` MaxCertValidSeconds int64 `json:"max_cert_valid_seconds"`
ValidAfter int64 `json:"valid_after"`
ValidBefore int64 `json:"valid_before"`
CreatedByKind string `json:"created_by_kind"` CreatedByKind string `json:"created_by_kind"`
CreatedBySubjectID string `json:"created_by_subject_id"` CreatedBySubjectID string `json:"created_by_subject_id"`
CreatedBySubjectName string `json:"created_by_subject_name"` CreatedBySubjectName string `json:"created_by_subject_name"`
@@ -0,0 +1,14 @@
ALTER TABLE ssh_servers ADD COLUMN owner_scope TEXT NOT NULL DEFAULT 'admin';
ALTER TABLE ssh_servers ADD COLUMN owner_user_public_id TEXT NOT NULL DEFAULT '';
UPDATE ssh_servers
SET owner_scope = CASE
WHEN created_by_kind = 'user' AND created_by_subject_id IN (SELECT public_id FROM users WHERE is_admin = 0) THEN 'user'
ELSE 'admin'
END,
owner_user_public_id = CASE
WHEN created_by_kind = 'user' AND created_by_subject_id IN (SELECT public_id FROM users WHERE is_admin = 0) THEN created_by_subject_id
ELSE ''
END;
CREATE INDEX IF NOT EXISTS idx_ssh_servers_owner ON ssh_servers(owner_scope, owner_user_public_id);
@@ -0,0 +1,4 @@
ALTER TABLE ssh_access_profiles RENAME COLUMN default_valid_seconds TO default_cert_valid_seconds;
ALTER TABLE ssh_access_profiles RENAME COLUMN max_valid_seconds TO max_cert_valid_seconds;
ALTER TABLE ssh_access_profiles ADD COLUMN valid_after INTEGER NOT NULL DEFAULT 0;
ALTER TABLE ssh_access_profiles ADD COLUMN valid_before INTEGER NOT NULL DEFAULT 0;
+2 -2
View File
@@ -66,8 +66,8 @@ func TestSSHAccessProfileVisibilityForUserAndGroupTargets(t *testing.T) {
SecretPayload: "PRIVATE KEY", SecretPayload: "PRIVATE KEY",
AuthPublicKey: "ssh-ed25519 AAAA", AuthPublicKey: "ssh-ed25519 AAAA",
AuthPublicKeyFingerprint: "SHA256:test", AuthPublicKeyFingerprint: "SHA256:test",
DefaultValidSeconds: 3600, DefaultCertValidSeconds: 3600,
MaxValidSeconds: 3600, MaxCertValidSeconds: 3600,
CreatedByKind: "user", CreatedByKind: "user",
CreatedBySubjectID: user.ID, CreatedBySubjectID: user.ID,
CreatedBySubjectName: user.Username, CreatedBySubjectName: user.Username,
+5 -2
View File
@@ -547,9 +547,11 @@ Important fields:
- `description`, `tags_json` - `description`, `tags_json`
- `enabled` - `enabled`
- `host_key_policy`: `strict` requires an existing pinned host key; `trust_on_first_use` allows the first connection to pin the observed host key. - `host_key_policy`: `strict` requires an existing pinned host key; `trust_on_first_use` allows the first connection to pin the observed host key.
- `owner_scope`: `admin` for centrally managed servers or `user` for self-service personal servers.
- `owner_user_public_id`: owner user when `owner_scope = 'user'`.
- `created_by_kind`, `created_by_subject_id`, `created_by_subject_name` - `created_by_kind`, `created_by_subject_id`, `created_by_subject_name`
`created_by_kind = 'user'` indicates a personal server; admin-created servers are shared through access profiles and targets. Self-service server lists use `owner_scope = 'user'` and `owner_user_public_id`, not creator attribution. Admin-created servers stay centrally managed even when the admin actor is a normal user subject.
When `host_key_policy = 'trust_on_first_use'`, the first successful SSH connection writes the observed key into `ssh_server_host_keys`; later connections use the pinned-key check. When `host_key_policy = 'trust_on_first_use'`, the first successful SSH connection writes the observed key into `ssh_server_host_keys`; later connections use the pinned-key check.
### `ssh_server_host_keys` ### `ssh_server_host_keys`
@@ -634,7 +636,8 @@ Important fields:
- `auth_public_key`, `auth_public_key_fingerprint`: public-key metadata. - `auth_public_key`, `auth_public_key_fingerprint`: public-key metadata.
- `ssh_user_ca_public_id`: SSH CA used for managed cert auth. - `ssh_user_ca_public_id`: SSH CA used for managed cert auth.
- `ssh_principal_grant_ids_json`: grants used for managed cert auth. - `ssh_principal_grant_ids_json`: grants used for managed cert auth.
- `default_valid_seconds`, `max_valid_seconds`: cert/session validity bounds. - `default_cert_valid_seconds`, `max_cert_valid_seconds`: SSH certificate validity bounds for managed-cert connections.
- `valid_after`, `valid_before`: optional profile availability window. `0` means unbounded.
- `enabled`, `allow_user_edit`: availability/editing controls. - `enabled`, `allow_user_edit`: availability/editing controls.
Stored private-key and stored-password profiles ultimately reference `ssh_secrets`. Imported SSH credential private keys are not returned by the API; only public key metadata and fingerprints are exposed. Stored private-key and stored-password profiles ultimately reference `ssh_secrets`. Imported SSH credential private keys are not returned by the API; only public key metadata and fingerprints are exposed.
+19
View File
@@ -10107,6 +10107,13 @@ components:
enum: enum:
- strict - strict
- trust_on_first_use - trust_on_first_use
owner_scope:
type: string
enum:
- admin
- user
owner_user_id:
type: string
editable: editable:
type: boolean type: boolean
additionalProperties: true additionalProperties: true
@@ -10293,6 +10300,18 @@ components:
type: string type: string
ssh_user_ca_id: ssh_user_ca_id:
type: string type: string
default_cert_valid_seconds:
type: integer
format: int64
max_cert_valid_seconds:
type: integer
format: int64
valid_after:
type: integer
format: int64
valid_before:
type: integer
format: int64
enabled: enabled:
type: boolean type: boolean
editable: editable:
+12 -6
View File
@@ -912,6 +912,8 @@ export interface SSHServer {
tags: string[] tags: string[]
enabled: boolean enabled: boolean
host_key_policy: 'strict' | 'trust_on_first_use' host_key_policy: 'strict' | 'trust_on_first_use'
owner_scope: 'admin' | 'user'
owner_user_id: string
created_by_kind: string created_by_kind: string
created_by_subject_id: string created_by_subject_id: string
created_by_subject_name: string created_by_subject_name: string
@@ -974,8 +976,10 @@ export interface SSHAccessProfile {
auth_public_key_fingerprint: string auth_public_key_fingerprint: string
ssh_user_ca_id: string ssh_user_ca_id: string
ssh_principal_grant_ids: string[] ssh_principal_grant_ids: string[]
default_valid_seconds: number default_cert_valid_seconds: number
max_valid_seconds: number max_cert_valid_seconds: number
valid_after: number
valid_before: number
created_by_kind: string created_by_kind: string
created_by_subject_id: string created_by_subject_id: string
created_by_subject_name: string created_by_subject_name: string
@@ -1015,8 +1019,10 @@ export type SSHAccessProfileAdminUpsertPayload = {
password_text?: string password_text?: string
ssh_user_ca_id?: string ssh_user_ca_id?: string
ssh_principal_grant_ids?: string[] ssh_principal_grant_ids?: string[]
default_valid_seconds: number default_cert_valid_seconds: number
max_valid_seconds: number max_cert_valid_seconds: number
valid_after: number
valid_before: number
targets: SSHAccessProfileTargetPayload[] targets: SSHAccessProfileTargetPayload[]
} }
@@ -1033,8 +1039,8 @@ export type SSHAccessProfileSelfUpsertPayload = {
ssh_credential_id?: string ssh_credential_id?: string
private_key_pem?: string private_key_pem?: string
password_text?: string password_text?: string
default_valid_seconds: number default_cert_valid_seconds: number
max_valid_seconds: number max_cert_valid_seconds: number
} }
export interface SSHCredential { export interface SSHCredential {
@@ -57,10 +57,12 @@ export default function SSHAccessProfileDetailsDialog(props: SSHAccessProfileDet
<> <>
<TextField label="SSH User CA" value={item?.ssh_user_ca_id || ''} InputProps={{ readOnly: true }} /> <TextField label="SSH User CA" value={item?.ssh_user_ca_id || ''} InputProps={{ readOnly: true }} />
<TextField label="Principal Grants" value={(item?.ssh_principal_grant_ids || []).join(', ') || '-'} InputProps={{ readOnly: true }} /> <TextField label="Principal Grants" value={(item?.ssh_principal_grant_ids || []).join(', ') || '-'} InputProps={{ readOnly: true }} />
<TextField label="Default Validity Seconds" value={String(item?.default_valid_seconds || 0)} InputProps={{ readOnly: true }} /> <TextField label="Default Cert Validity Seconds" value={String(item?.default_cert_valid_seconds || 0)} InputProps={{ readOnly: true }} />
<TextField label="Max Validity Seconds" value={String(item?.max_valid_seconds || 0)} InputProps={{ readOnly: true }} /> <TextField label="Max Cert Validity Seconds" value={String(item?.max_cert_valid_seconds || 0)} InputProps={{ readOnly: true }} />
</> </>
) : null} ) : null}
<TextField label="Profile Valid After" value={fmt(item?.valid_after || 0)} InputProps={{ readOnly: true }} />
<TextField label="Profile Valid Before" value={fmt(item?.valid_before || 0)} InputProps={{ readOnly: true }} />
{showAdminFields ? <TextField label="Targets" value={targetsValue || '-'} multiline minRows={2} InputProps={{ readOnly: true }} /> : null} {showAdminFields ? <TextField label="Targets" value={targetsValue || '-'} multiline minRows={2} InputProps={{ readOnly: true }} /> : null}
{showAdminFields ? <TextField label="Created By Kind" value={item?.created_by_kind || '-'} InputProps={{ readOnly: true }} /> : null} {showAdminFields ? <TextField label="Created By Kind" value={item?.created_by_kind || '-'} InputProps={{ readOnly: true }} /> : null}
{showAdminFields ? <TextField label="Created By Subject ID" value={item?.created_by_subject_id || '-'} InputProps={{ readOnly: true }} /> : null} {showAdminFields ? <TextField label="Created By Subject ID" value={item?.created_by_subject_id || '-'} InputProps={{ readOnly: true }} /> : null}
@@ -29,8 +29,10 @@ export type SSHAccessProfileFormState = {
password_text: string password_text: string
ssh_user_ca_id: string ssh_user_ca_id: string
ssh_principal_grant_ids: string[] ssh_principal_grant_ids: string[]
default_valid_seconds: number default_cert_valid_seconds: number
max_valid_seconds: number max_cert_valid_seconds: number
valid_after: string
valid_before: string
user_ids: string[] user_ids: string[]
group_ids: string[] group_ids: string[]
} }
@@ -52,6 +54,7 @@ type SSHAccessProfileFormDialogProps = {
userOptions?: User[] userOptions?: User[]
groupOptions?: UserGroup[] groupOptions?: UserGroup[]
showTargets?: boolean showTargets?: boolean
showValidityWindow?: boolean
onClose: () => void onClose: () => void
onSave: () => void onSave: () => void
} }
@@ -61,6 +64,7 @@ export default function SSHAccessProfileFormDialog(props: SSHAccessProfileFormDi
const selectedUserOptions: User[] = (props.userOptions || []).filter((item) => props.form.user_ids.includes(item.id)) const selectedUserOptions: User[] = (props.userOptions || []).filter((item) => props.form.user_ids.includes(item.id))
const selectedGroupOptions: UserGroup[] = (props.groupOptions || []).filter((item) => props.form.group_ids.includes(item.id)) const selectedGroupOptions: UserGroup[] = (props.groupOptions || []).filter((item) => props.form.group_ids.includes(item.id))
const showTargets: boolean = Boolean(props.showTargets) const showTargets: boolean = Boolean(props.showTargets)
const showValidityWindow: boolean = Boolean(props.showValidityWindow)
const enabledServerGroups: SSHServerGroup[] = (props.serverGroups || []).filter((item) => item.enabled) const enabledServerGroups: SSHServerGroup[] = (props.serverGroups || []).filter((item) => item.enabled)
const canUseServerGroups: boolean = enabledServerGroups.length > 0 const canUseServerGroups: boolean = enabledServerGroups.length > 0
@@ -166,16 +170,16 @@ export default function SSHAccessProfileFormDialog(props: SSHAccessProfileFormDi
renderInput={(params) => <TextField {...params} label="Principal Grants" />} renderInput={(params) => <TextField {...params} label="Principal Grants" />}
/> />
<TextField <TextField
label="Default Validity Seconds" label="Default Cert Validity Seconds"
type="number" type="number"
value={props.form.default_valid_seconds} value={props.form.default_cert_valid_seconds}
onChange={(event) => props.setForm((prev) => ({ ...prev, default_valid_seconds: Number(event.target.value) || 0 }))} onChange={(event) => props.setForm((prev) => ({ ...prev, default_cert_valid_seconds: Number(event.target.value) || 0 }))}
/> />
<TextField <TextField
label="Max Validity Seconds" label="Max Cert Validity Seconds"
type="number" type="number"
value={props.form.max_valid_seconds} value={props.form.max_cert_valid_seconds}
onChange={(event) => props.setForm((prev) => ({ ...prev, max_valid_seconds: Number(event.target.value) || 0 }))} onChange={(event) => props.setForm((prev) => ({ ...prev, max_cert_valid_seconds: Number(event.target.value) || 0 }))}
/> />
<MonospaceTextField <MonospaceTextField
label="Managed Private Key PEM" label="Managed Private Key PEM"
@@ -221,6 +225,24 @@ export default function SSHAccessProfileFormDialog(props: SSHAccessProfileFormDi
helperText={props.editingID ? 'Leave blank to keep the existing password.' : 'Required for stored_password.'} helperText={props.editingID ? 'Leave blank to keep the existing password.' : 'Required for stored_password.'}
/> />
) : null} ) : null}
{showValidityWindow ? (
<TextField
label="Profile Valid After (optional)"
type="datetime-local"
value={props.form.valid_after}
onChange={(event) => props.setForm((prev) => ({ ...prev, valid_after: event.target.value }))}
InputLabelProps={{ shrink: true }}
/>
) : null}
{showValidityWindow ? (
<TextField
label="Profile Valid Before (optional)"
type="datetime-local"
value={props.form.valid_before}
onChange={(event) => props.setForm((prev) => ({ ...prev, valid_before: event.target.value }))}
InputLabelProps={{ shrink: true }}
/>
) : null}
{showTargets ? ( {showTargets ? (
<Autocomplete<User, true, false, false> <Autocomplete<User, true, false, false>
multiple multiple
@@ -29,6 +29,40 @@ import UserGroupDetailsDialog from '../components/UserGroupDetailsDialog'
import PageAlert from '../components/PageAlert' import PageAlert from '../components/PageAlert'
import { api, SSHAccessProfile, SSHAccessProfileAdminUpsertPayload, SSHCredential, SSHPrincipalGrant, SSHServer, SSHServerGroup, SSHUserCA, User, UserGroup } from '../api' import { api, SSHAccessProfile, SSHAccessProfileAdminUpsertPayload, SSHCredential, SSHPrincipalGrant, SSHServer, SSHServerGroup, SSHUserCA, User, UserGroup } from '../api'
function toDateTimeInput(value: number): string {
const date: Date = new Date(value * 1000)
const pad = (n: number): string => String(n).padStart(2, '0')
if (!value || value <= 0) {
return ''
}
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`
}
function parseDateTimeInput(value: string): number {
const ts: number = Date.parse(value)
if (!value) {
return 0
}
if (Number.isNaN(ts)) {
return 0
}
return Math.floor(ts / 1000)
}
function profileValidityState(item: SSHAccessProfile): 'Pending' | 'Expired' | null {
const now: number = Math.floor(Date.now() / 1000)
if (item.valid_after > 0 && item.valid_after > now) {
return 'Pending'
}
if (item.valid_before > 0 && item.valid_before < now) {
return 'Expired'
}
return null
}
const emptyForm = (): SSHAccessProfileFormState => ({ const emptyForm = (): SSHAccessProfileFormState => ({
server_id: '', server_id: '',
server_target_type: 'server', server_target_type: 'server',
@@ -44,8 +78,10 @@ const emptyForm = (): SSHAccessProfileFormState => ({
password_text: '', password_text: '',
ssh_user_ca_id: '', ssh_user_ca_id: '',
ssh_principal_grant_ids: [], ssh_principal_grant_ids: [],
default_valid_seconds: 3600, default_cert_valid_seconds: 3600,
max_valid_seconds: 3600, max_cert_valid_seconds: 3600,
valid_after: '',
valid_before: '',
user_ids: [], user_ids: [],
group_ids: [] group_ids: []
}) })
@@ -218,8 +254,10 @@ export default function AdminSSHAccessProfilesPage() {
password_text: '', password_text: '',
ssh_user_ca_id: item.ssh_user_ca_id || '', ssh_user_ca_id: item.ssh_user_ca_id || '',
ssh_principal_grant_ids: item.ssh_principal_grant_ids || [], ssh_principal_grant_ids: item.ssh_principal_grant_ids || [],
default_valid_seconds: item.default_valid_seconds || 3600, default_cert_valid_seconds: item.default_cert_valid_seconds || 3600,
max_valid_seconds: item.max_valid_seconds || item.default_valid_seconds || 3600, max_cert_valid_seconds: item.max_cert_valid_seconds || item.default_cert_valid_seconds || 3600,
valid_after: toDateTimeInput(item.valid_after),
valid_before: toDateTimeInput(item.valid_before),
user_ids: (item.targets || []).filter((target) => target.target_type === 'user').map((target) => target.target_id), user_ids: (item.targets || []).filter((target) => target.target_type === 'user').map((target) => target.target_id),
group_ids: (item.targets || []).filter((target) => target.target_type === 'group').map((target) => target.target_id) group_ids: (item.targets || []).filter((target) => target.target_type === 'group').map((target) => target.target_id)
}) })
@@ -337,8 +375,10 @@ export default function AdminSSHAccessProfilesPage() {
password_text: form.password_text.trim() || undefined, password_text: form.password_text.trim() || undefined,
ssh_user_ca_id: form.auth_method === 'managed_ssh_cert' ? (form.ssh_user_ca_id || undefined) : undefined, ssh_user_ca_id: form.auth_method === 'managed_ssh_cert' ? (form.ssh_user_ca_id || undefined) : undefined,
ssh_principal_grant_ids: form.auth_method === 'managed_ssh_cert' ? form.ssh_principal_grant_ids : [], ssh_principal_grant_ids: form.auth_method === 'managed_ssh_cert' ? form.ssh_principal_grant_ids : [],
default_valid_seconds: Number(form.default_valid_seconds) || 0, default_cert_valid_seconds: Number(form.default_cert_valid_seconds) || 0,
max_valid_seconds: Number(form.max_valid_seconds) || 0, max_cert_valid_seconds: Number(form.max_cert_valid_seconds) || 0,
valid_after: parseDateTimeInput(form.valid_after),
valid_before: parseDateTimeInput(form.valid_before),
targets: [ targets: [
...form.user_ids.map((id) => ({ target_type: 'user' as const, target_id: id })), ...form.user_ids.map((id) => ({ target_type: 'user' as const, target_id: id })),
...form.group_ids.map((id) => ({ target_type: 'group' as const, target_id: id })) ...form.group_ids.map((id) => ({ target_type: 'group' as const, target_id: id }))
@@ -446,6 +486,7 @@ export default function AdminSSHAccessProfilesPage() {
color={item.owner_scope === 'user' ? 'info' : 'default'} color={item.owner_scope === 'user' ? 'info' : 'default'}
/> />
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)} {item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
{profileValidityState(item) ? (<> <Chip size="small" color="warning" label={profileValidityState(item)} /></>) : null}
</Typography> </Typography>
} }
secondary={ secondary={
@@ -571,6 +612,7 @@ export default function AdminSSHAccessProfilesPage() {
userOptions={userOptions} userOptions={userOptions}
groupOptions={groupOptions} groupOptions={groupOptions}
showTargets showTargets
showValidityWindow
onClose={() => setDialogOpen(false)} onClose={() => setDialogOpen(false)}
onSave={handleSave} onSave={handleSave}
/> />
+21 -10
View File
@@ -1,5 +1,6 @@
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Chip from '@mui/material/Chip'
import Link from '@mui/material/Link' import Link from '@mui/material/Link'
import List from '@mui/material/List' import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem' import ListItem from '@mui/material/ListItem'
@@ -41,8 +42,10 @@ const emptyForm = (): SSHAccessProfileFormState => ({
password_text: '', password_text: '',
ssh_user_ca_id: '', ssh_user_ca_id: '',
ssh_principal_grant_ids: [], ssh_principal_grant_ids: [],
default_valid_seconds: 3600, default_cert_valid_seconds: 3600,
max_valid_seconds: 3600, max_cert_valid_seconds: 3600,
valid_after: '',
valid_before: '',
user_ids: [], user_ids: [],
group_ids: [] group_ids: []
}) })
@@ -259,8 +262,10 @@ export default function SSHServersPage() {
password_text: '', password_text: '',
ssh_user_ca_id: item.ssh_user_ca_id || '', ssh_user_ca_id: item.ssh_user_ca_id || '',
ssh_principal_grant_ids: item.ssh_principal_grant_ids || [], ssh_principal_grant_ids: item.ssh_principal_grant_ids || [],
default_valid_seconds: item.default_valid_seconds || 3600, default_cert_valid_seconds: item.default_cert_valid_seconds || 3600,
max_valid_seconds: item.max_valid_seconds || item.default_valid_seconds || 3600, max_cert_valid_seconds: item.max_cert_valid_seconds || item.default_cert_valid_seconds || 3600,
valid_after: '',
valid_before: '',
user_ids: [], user_ids: [],
group_ids: [] group_ids: []
}) })
@@ -479,8 +484,8 @@ export default function SSHServersPage() {
ssh_credential_id: form.auth_method === 'stored_private_key' ? (form.ssh_credential_id || undefined) : undefined, ssh_credential_id: form.auth_method === 'stored_private_key' ? (form.ssh_credential_id || undefined) : undefined,
private_key_pem: form.private_key_pem.trim() || undefined, private_key_pem: form.private_key_pem.trim() || undefined,
password_text: form.password_text.trim() || undefined, password_text: form.password_text.trim() || undefined,
default_valid_seconds: Number(form.default_valid_seconds) || 0, default_cert_valid_seconds: Number(form.default_cert_valid_seconds) || 0,
max_valid_seconds: Number(form.max_valid_seconds) || 0 max_cert_valid_seconds: Number(form.max_cert_valid_seconds) || 0
} }
if (!payload.server_id) { if (!payload.server_id) {
@@ -681,7 +686,10 @@ export default function SSHServersPage() {
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}> <ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
<CompactListItemText <CompactListItemText
primary={ primary={
<Typography>{item.name}</Typography> <Typography>
{item.name}
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
</Typography>
} }
secondary={ secondary={
<Box sx={{ display: 'grid' }}> <Box sx={{ display: 'grid' }}>
@@ -745,7 +753,7 @@ export default function SSHServersPage() {
Delete Delete
</ListRowActionButton> </ListRowActionButton>
) : null} ) : null}
<ListRowActionButton onClick={() => void handleConnect(item)} disabled={connectingID === item.id}> <ListRowActionButton onClick={() => void handleConnect(item)} disabled={connectingID === item.id || !item.enabled}>
{connectingID === item.id ? 'Connecting...' : 'Connect'} {connectingID === item.id ? 'Connecting...' : 'Connect'}
</ListRowActionButton> </ListRowActionButton>
</ListRowActions> </ListRowActions>
@@ -767,12 +775,15 @@ export default function SSHServersPage() {
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}> <ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
<CompactListItemText <CompactListItemText
primary={ primary={
<Typography>{item.name}</Typography> <Typography>
{item.name}
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
</Typography>
} }
secondary={ secondary={
<Box sx={{ display: 'grid' }}> <Box sx={{ display: 'grid' }}>
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}> <Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
{item.host}:{item.port} · {item.enabled ? 'enabled' : 'disabled'} {item.host}:{item.port}
</Typography> </Typography>
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}> <Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
Tags: {(item.tags || []).join(', ') || '-'} · {item.id} Tags: {(item.tags || []).join(', ') || '-'} · {item.id}