Compare commits
3 Commits
b7d492d13c
...
6c2ec1e2f9
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c2ec1e2f9 | |||
| 547acc82df | |||
| 0db8db498e |
@@ -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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user