Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 001f0487e2 | |||
| fd28f0b722 | |||
| 7c21b910b1 |
@@ -20,7 +20,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, 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, 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 +29,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.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.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -52,10 +52,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, 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, 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.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.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return item, err
|
return item, err
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ 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, 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, 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 created_by_kind = 'user' AND created_by_subject_id = ?
|
||||||
ORDER BY name, host, port`, trimmedUserID)
|
ORDER BY name, host, port`, trimmedUserID)
|
||||||
@@ -85,7 +85,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.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.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -150,6 +150,7 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
if err != nil { return item, err }
|
if err != nil { return item, err }
|
||||||
}
|
}
|
||||||
if item.Port <= 0 { item.Port = 22 }
|
if item.Port <= 0 { item.Port = 22 }
|
||||||
|
item.HostKeyPolicy = normalizeSSHServerHostKeyPolicy(item.HostKeyPolicy)
|
||||||
item.Tags = normalizeStringList(item.Tags)
|
item.Tags = normalizeStringList(item.Tags)
|
||||||
tagsJSON, err = encodeStringList(item.Tags)
|
tagsJSON, err = encodeStringList(item.Tags)
|
||||||
if err != nil { return item, err }
|
if err != nil { return item, err }
|
||||||
@@ -165,12 +166,13 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
description,
|
description,
|
||||||
tags_json,
|
tags_json,
|
||||||
enabled,
|
enabled,
|
||||||
|
host_key_policy,
|
||||||
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),
|
||||||
@@ -178,6 +180,7 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
strings.TrimSpace(item.Description),
|
strings.TrimSpace(item.Description),
|
||||||
tagsJSON,
|
tagsJSON,
|
||||||
item.Enabled,
|
item.Enabled,
|
||||||
|
item.HostKeyPolicy,
|
||||||
strings.TrimSpace(item.CreatedByKind),
|
strings.TrimSpace(item.CreatedByKind),
|
||||||
strings.TrimSpace(item.CreatedBySubjectID),
|
strings.TrimSpace(item.CreatedBySubjectID),
|
||||||
strings.TrimSpace(item.CreatedBySubjectName),
|
strings.TrimSpace(item.CreatedBySubjectName),
|
||||||
@@ -192,6 +195,7 @@ func (s *Store) UpdateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
var tagsJSON string
|
var tagsJSON string
|
||||||
|
|
||||||
if item.Port <= 0 { item.Port = 22 }
|
if item.Port <= 0 { item.Port = 22 }
|
||||||
|
item.HostKeyPolicy = normalizeSSHServerHostKeyPolicy(item.HostKeyPolicy)
|
||||||
item.Tags = normalizeStringList(item.Tags)
|
item.Tags = normalizeStringList(item.Tags)
|
||||||
tagsJSON, err = encodeStringList(item.Tags)
|
tagsJSON, err = encodeStringList(item.Tags)
|
||||||
if err != nil { return item, err }
|
if err != nil { return item, err }
|
||||||
@@ -199,7 +203,7 @@ func (s *Store) UpdateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
|
|
||||||
// TODO: need to protect it with tx.
|
// TODO: need to protect it with tx.
|
||||||
_, err = s.Exec(`UPDATE ssh_servers
|
_, err = s.Exec(`UPDATE ssh_servers
|
||||||
SET name = ?, host = ?, port = ?, description = ?, tags_json = ?, enabled = ?, updated_at = ?
|
SET name = ?, host = ?, port = ?, description = ?, tags_json = ?, enabled = ?, host_key_policy = ?, updated_at = ?
|
||||||
WHERE public_id = ?`,
|
WHERE public_id = ?`,
|
||||||
strings.TrimSpace(item.Name),
|
strings.TrimSpace(item.Name),
|
||||||
strings.TrimSpace(item.Host),
|
strings.TrimSpace(item.Host),
|
||||||
@@ -207,6 +211,7 @@ func (s *Store) UpdateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
strings.TrimSpace(item.Description),
|
strings.TrimSpace(item.Description),
|
||||||
tagsJSON,
|
tagsJSON,
|
||||||
item.Enabled,
|
item.Enabled,
|
||||||
|
item.HostKeyPolicy,
|
||||||
item.UpdatedAt,
|
item.UpdatedAt,
|
||||||
strings.TrimSpace(item.ID),
|
strings.TrimSpace(item.ID),
|
||||||
)
|
)
|
||||||
@@ -281,6 +286,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
|||||||
s.description,
|
s.description,
|
||||||
s.tags_json,
|
s.tags_json,
|
||||||
s.enabled,
|
s.enabled,
|
||||||
|
s.host_key_policy,
|
||||||
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,
|
||||||
@@ -338,6 +344,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
|||||||
&item.Server.Description,
|
&item.Server.Description,
|
||||||
&serverTagsJSON,
|
&serverTagsJSON,
|
||||||
&item.Server.Enabled,
|
&item.Server.Enabled,
|
||||||
|
&item.Server.HostKeyPolicy,
|
||||||
&item.Server.CreatedByKind,
|
&item.Server.CreatedByKind,
|
||||||
&item.Server.CreatedBySubjectID,
|
&item.Server.CreatedBySubjectID,
|
||||||
&item.Server.CreatedBySubjectName,
|
&item.Server.CreatedBySubjectName,
|
||||||
@@ -415,6 +422,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
|||||||
s.description,
|
s.description,
|
||||||
s.tags_json,
|
s.tags_json,
|
||||||
s.enabled,
|
s.enabled,
|
||||||
|
s.host_key_policy,
|
||||||
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,
|
||||||
@@ -468,6 +476,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
|||||||
&item.Server.Description,
|
&item.Server.Description,
|
||||||
&serverTagsJSON,
|
&serverTagsJSON,
|
||||||
&item.Server.Enabled,
|
&item.Server.Enabled,
|
||||||
|
&item.Server.HostKeyPolicy,
|
||||||
&item.Server.CreatedByKind,
|
&item.Server.CreatedByKind,
|
||||||
&item.Server.CreatedBySubjectID,
|
&item.Server.CreatedBySubjectID,
|
||||||
&item.Server.CreatedBySubjectName,
|
&item.Server.CreatedBySubjectName,
|
||||||
@@ -857,6 +866,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
|||||||
s.description,
|
s.description,
|
||||||
s.tags_json,
|
s.tags_json,
|
||||||
s.enabled,
|
s.enabled,
|
||||||
|
s.host_key_policy,
|
||||||
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,
|
||||||
@@ -934,6 +944,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
|||||||
&item.Server.Description,
|
&item.Server.Description,
|
||||||
&serverTagsJSON,
|
&serverTagsJSON,
|
||||||
&item.Server.Enabled,
|
&item.Server.Enabled,
|
||||||
|
&item.Server.HostKeyPolicy,
|
||||||
&item.Server.CreatedByKind,
|
&item.Server.CreatedByKind,
|
||||||
&item.Server.CreatedBySubjectID,
|
&item.Server.CreatedBySubjectID,
|
||||||
&item.Server.CreatedBySubjectName,
|
&item.Server.CreatedBySubjectName,
|
||||||
@@ -1501,6 +1512,14 @@ func emptyStringToNil(raw string) any {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeSSHServerHostKeyPolicy(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "trust_on_first_use" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return "strict"
|
||||||
|
}
|
||||||
|
|
||||||
func NormalizeSSHServerTags(items []string) []string {
|
func NormalizeSSHServerTags(items []string) []string {
|
||||||
var out []string
|
var out []string
|
||||||
|
|
||||||
|
|||||||
@@ -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.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.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.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.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 }
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ import "codit/internal/models"
|
|||||||
|
|
||||||
const logIDSSHBroker string = "ssh-broker"
|
const logIDSSHBroker string = "ssh-broker"
|
||||||
type sshServerRequest struct {
|
type sshServerRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
HostKeyPolicy string `json:"host_key_policy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshCredentialRequest struct {
|
type sshCredentialRequest struct {
|
||||||
@@ -124,10 +125,11 @@ type sshSessionStreamMessage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type sshSessionPrepared struct {
|
type sshSessionPrepared struct {
|
||||||
Session models.SSHSession
|
Session models.SSHSession
|
||||||
Profile models.SSHAccessProfile
|
Profile models.SSHAccessProfile
|
||||||
HostKeys []models.SSHServerHostKey
|
HostKeys []models.SSHServerHostKey
|
||||||
AuthMethods []ssh.AuthMethod
|
AuthMethods []ssh.AuthMethod
|
||||||
|
PinHostKeyOnFirstUse bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshWorkspaceAttachment struct {
|
type sshWorkspaceAttachment struct {
|
||||||
@@ -318,6 +320,7 @@ func (api *API) CreateSSHServerAdmin(w http.ResponseWriter, r *http.Request, _ m
|
|||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Tags: normalizeSSHBrokerTags(req.Tags),
|
Tags: normalizeSSHBrokerTags(req.Tags),
|
||||||
Enabled: req.Enabled,
|
Enabled: req.Enabled,
|
||||||
|
HostKeyPolicy: normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy),
|
||||||
CreatedByKind: actorKind,
|
CreatedByKind: actorKind,
|
||||||
CreatedBySubjectID: actorID,
|
CreatedBySubjectID: actorID,
|
||||||
CreatedBySubjectName: actorName,
|
CreatedBySubjectName: actorName,
|
||||||
@@ -373,6 +376,7 @@ func (api *API) UpdateSSHServerAdmin(w http.ResponseWriter, r *http.Request, par
|
|||||||
item.Description = req.Description
|
item.Description = req.Description
|
||||||
item.Tags = normalizeSSHBrokerTags(req.Tags)
|
item.Tags = normalizeSSHBrokerTags(req.Tags)
|
||||||
item.Enabled = req.Enabled
|
item.Enabled = req.Enabled
|
||||||
|
item.HostKeyPolicy = normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy)
|
||||||
item, err = api.store(r).UpdateSSHServer(item)
|
item, err = api.store(r).UpdateSSHServer(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
@@ -1086,6 +1090,7 @@ func (api *API) CreateSSHServerForSelf(w http.ResponseWriter, r *http.Request, _
|
|||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Tags: normalizeSSHBrokerTags(req.Tags),
|
Tags: normalizeSSHBrokerTags(req.Tags),
|
||||||
Enabled: req.Enabled,
|
Enabled: req.Enabled,
|
||||||
|
HostKeyPolicy: normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy),
|
||||||
CreatedByKind: "user",
|
CreatedByKind: "user",
|
||||||
CreatedBySubjectID: user.ID,
|
CreatedBySubjectID: user.ID,
|
||||||
CreatedBySubjectName: user.Username,
|
CreatedBySubjectName: user.Username,
|
||||||
@@ -1147,6 +1152,7 @@ func (api *API) UpdateSSHServerForSelf(w http.ResponseWriter, r *http.Request, p
|
|||||||
item.Description = req.Description
|
item.Description = req.Description
|
||||||
item.Tags = normalizeSSHBrokerTags(req.Tags)
|
item.Tags = normalizeSSHBrokerTags(req.Tags)
|
||||||
item.Enabled = req.Enabled
|
item.Enabled = req.Enabled
|
||||||
|
item.HostKeyPolicy = normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy)
|
||||||
item, err = api.store(r).UpdateSSHServer(item)
|
item, err = api.store(r).UpdateSSHServer(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
@@ -1549,6 +1555,7 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
|
|||||||
var response sshSessionConnectResponse
|
var response sshSessionConnectResponse
|
||||||
var prepared sshSessionPrepared
|
var prepared sshSessionPrepared
|
||||||
var prepareStage string
|
var prepareStage string
|
||||||
|
var hostKeyFingerprint string
|
||||||
var belongs bool
|
var belongs bool
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -1654,6 +1661,9 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
|
|||||||
api.SSHPreparedSessionStore.Put(item.ID, prepared)
|
api.SSHPreparedSessionStore.Put(item.ID, prepared)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if len(prepared.HostKeys) > 0 {
|
||||||
|
hostKeyFingerprint = prepared.HostKeys[0].Fingerprint
|
||||||
|
}
|
||||||
response = sshSessionConnectResponse{
|
response = sshSessionConnectResponse{
|
||||||
SessionID: item.ID,
|
SessionID: item.ID,
|
||||||
Status: item.Status,
|
Status: item.Status,
|
||||||
@@ -1662,7 +1672,7 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
|
|||||||
Host: profile.Server.Host,
|
Host: profile.Server.Host,
|
||||||
Port: profile.Server.Port,
|
Port: profile.Server.Port,
|
||||||
RemoteUsername: profile.RemoteUsername,
|
RemoteUsername: profile.RemoteUsername,
|
||||||
HostKeyFingerprint: prepared.HostKeys[0].Fingerprint,
|
HostKeyFingerprint: hostKeyFingerprint,
|
||||||
}
|
}
|
||||||
WriteJSON(w, http.StatusCreated, response)
|
WriteJSON(w, http.StatusCreated, response)
|
||||||
}
|
}
|
||||||
@@ -2156,6 +2166,14 @@ func normalizeSSHBrokerTags(raw []string) []string {
|
|||||||
return db.NormalizeSSHServerTags(raw)
|
return db.NormalizeSSHServerTags(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeSSHServerHostKeyPolicy(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "trust_on_first_use" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return "strict"
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeSSHBrokerNames(raw []string) []string {
|
func normalizeSSHBrokerNames(raw []string) []string {
|
||||||
var out []string
|
var out []string
|
||||||
var seen map[string]bool
|
var seen map[string]bool
|
||||||
@@ -2357,7 +2375,10 @@ func (api *API) prepareSSHSession(store *db.Store, user models.User, sessionItem
|
|||||||
return prepared, "load_host_keys", err
|
return prepared, "load_host_keys", err
|
||||||
}
|
}
|
||||||
if len(prepared.HostKeys) == 0 {
|
if len(prepared.HostKeys) == 0 {
|
||||||
return prepared, "host_key_missing", errors.New("no pinned host key for this server")
|
if normalizeSSHServerHostKeyPolicy(prepared.Profile.Server.HostKeyPolicy) != "trust_on_first_use" {
|
||||||
|
return prepared, "host_key_missing", errors.New("no pinned host key for " + prepared.Profile.Server.Name)
|
||||||
|
}
|
||||||
|
prepared.PinHostKeyOnFirstUse = true
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared.AuthMethods, err = api.buildSSHSessionAuth(store, prepared.Profile, promptedPassword, otpCode)
|
prepared.AuthMethods, err = api.buildSSHSessionAuth(store, prepared.Profile, promptedPassword, otpCode)
|
||||||
@@ -2539,8 +2560,12 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
|
|||||||
var closeReason string
|
var closeReason string
|
||||||
var err error
|
var err error
|
||||||
var connectedFingerprint string
|
var connectedFingerprint string
|
||||||
|
var connectedHostKey ssh.PublicKey
|
||||||
var connectedAt int64
|
var connectedAt int64
|
||||||
var now int64
|
var now int64
|
||||||
|
var pinnedHostKey models.SSHServerHostKey
|
||||||
|
var reloadedHostKeys []models.SSHServerHostKey
|
||||||
|
var i int
|
||||||
var shellErrCh chan error
|
var shellErrCh chan error
|
||||||
var done bool
|
var done bool
|
||||||
var waitErr error
|
var waitErr error
|
||||||
@@ -2629,7 +2654,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
|
|||||||
sshConfig = &ssh.ClientConfig{
|
sshConfig = &ssh.ClientConfig{
|
||||||
User: profile.RemoteUsername,
|
User: profile.RemoteUsername,
|
||||||
Auth: authMethods,
|
Auth: authMethods,
|
||||||
HostKeyCallback: sshHostKeyCallback(hostKeys, &connectedFingerprint),
|
HostKeyCallback: sshHostKeyCallback(hostKeys, &connectedFingerprint, &connectedHostKey, prepared.PinHostKeyOnFirstUse),
|
||||||
Timeout: 15 * time.Second,
|
Timeout: 15 * time.Second,
|
||||||
}
|
}
|
||||||
sshClient, err = ssh.Dial("tcp", net.JoinHostPort(profile.Server.Host, fmt.Sprintf("%d", profile.Server.Port)), sshConfig)
|
sshClient, err = ssh.Dial("tcp", net.JoinHostPort(profile.Server.Host, fmt.Sprintf("%d", profile.Server.Port)), sshConfig)
|
||||||
@@ -2643,6 +2668,53 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer sshClient.Close()
|
defer sshClient.Close()
|
||||||
|
if prepared.PinHostKeyOnFirstUse {
|
||||||
|
if connectedHostKey == nil {
|
||||||
|
err = errors.New("failed to capture ssh host key")
|
||||||
|
api.logSSHSessionConnectFailure(sessionItem, profile, user, "pin_host_key", err)
|
||||||
|
if sendStatus != nil {
|
||||||
|
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
|
||||||
|
}
|
||||||
|
now = time.Now().UTC().Unix()
|
||||||
|
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "error", connectedFingerprint, 0, now, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pinnedHostKey = models.SSHServerHostKey{
|
||||||
|
ServerID: sessionItem.ServerID,
|
||||||
|
Algorithm: connectedHostKey.Type(),
|
||||||
|
PublicKey: strings.TrimSpace(string(ssh.MarshalAuthorizedKey(connectedHostKey))),
|
||||||
|
Fingerprint: connectedFingerprint,
|
||||||
|
}
|
||||||
|
pinnedHostKey, err = store.CreateSSHServerHostKey(pinnedHostKey)
|
||||||
|
if err != nil {
|
||||||
|
pinnedHostKey = models.SSHServerHostKey{}
|
||||||
|
reloadedHostKeys, err = store.ListSSHServerHostKeys(sessionItem.ServerID)
|
||||||
|
if err == nil {
|
||||||
|
for i = 0; i < len(reloadedHostKeys); i++ {
|
||||||
|
if strings.TrimSpace(reloadedHostKeys[i].Fingerprint) == connectedFingerprint {
|
||||||
|
pinnedHostKey = reloadedHostKeys[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pinnedHostKey.ID == "" {
|
||||||
|
api.logSSHSessionConnectFailure(sessionItem, profile, user, "pin_host_key", err)
|
||||||
|
if sendStatus != nil {
|
||||||
|
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: "failed to pin host key: " + err.Error()})
|
||||||
|
}
|
||||||
|
now = time.Now().UTC().Unix()
|
||||||
|
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "error", connectedFingerprint, 0, now, "failed to pin host key: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
|
||||||
|
"host key pinned session=%s server=%s server_name=%q fingerprint=%s algorithm=%s",
|
||||||
|
sessionItem.ID,
|
||||||
|
profile.Server.ID,
|
||||||
|
profile.Server.Name,
|
||||||
|
pinnedHostKey.Fingerprint,
|
||||||
|
pinnedHostKey.Algorithm)
|
||||||
|
}
|
||||||
runtimeSession.SetResources(transportWS, sshClient, nil, nil)
|
runtimeSession.SetResources(transportWS, sshClient, nil, nil)
|
||||||
sshSession, err = sshClient.NewSession()
|
sshSession, err = sshClient.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3015,7 +3087,7 @@ func (api *API) resolveSSHAccessProfilePrincipals(store *db.Store, profile model
|
|||||||
return principals, nil
|
return principals, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshHostKeyCallback(items []models.SSHServerHostKey, connectedFingerprint *string) ssh.HostKeyCallback {
|
func sshHostKeyCallback(items []models.SSHServerHostKey, connectedFingerprint *string, connectedHostKey *ssh.PublicKey, trustOnFirstUse bool) ssh.HostKeyCallback {
|
||||||
var allowed map[string]bool
|
var allowed map[string]bool
|
||||||
var i int
|
var i int
|
||||||
|
|
||||||
@@ -3027,7 +3099,9 @@ func sshHostKeyCallback(items []models.SSHServerHostKey, connectedFingerprint *s
|
|||||||
var fingerprint string
|
var fingerprint string
|
||||||
fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(key))
|
fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(key))
|
||||||
if connectedFingerprint != nil { *connectedFingerprint = fingerprint }
|
if connectedFingerprint != nil { *connectedFingerprint = fingerprint }
|
||||||
|
if connectedHostKey != nil { *connectedHostKey = key }
|
||||||
if allowed[fingerprint] { return nil}
|
if allowed[fingerprint] { return nil}
|
||||||
|
if trustOnFirstUse && len(items) == 0 { return nil }
|
||||||
return errors.New("host key mismatch: " + fingerprint)
|
return errors.New("host key mismatch: " + fingerprint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,29 +144,18 @@ func (api *API) SetupMyTOTP(w http.ResponseWriter, r *http.Request, _ map[string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) totpIssuer(r *http.Request) string {
|
func (api *API) totpIssuer(r *http.Request) string {
|
||||||
var raw string
|
|
||||||
var parsed *url.URL
|
|
||||||
var err error
|
|
||||||
var host string
|
var host string
|
||||||
|
|
||||||
raw = strings.TrimSpace(api.Cfg.PublicBaseURL)
|
// if the site name is set, use it as a totp issuer
|
||||||
if raw != "" {
|
host = api.siteName()
|
||||||
parsed, err = url.Parse(raw)
|
if host != "" { return host }
|
||||||
if err == nil {
|
|
||||||
host = strings.TrimSpace(parsed.Host)
|
// use the actual access host name as a totp issuer
|
||||||
if host != "" {
|
|
||||||
return host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
host = cleanTOTPHost(r.Header.Get("X-Forwarded-Host"))
|
host = cleanTOTPHost(r.Header.Get("X-Forwarded-Host"))
|
||||||
if host != "" {
|
if host != "" { return host }
|
||||||
return host
|
|
||||||
}
|
|
||||||
host = cleanTOTPHost(r.Host)
|
host = cleanTOTPHost(r.Host)
|
||||||
if host != "" {
|
if host != "" { return host }
|
||||||
return host
|
|
||||||
}
|
|
||||||
return api.serverTitle()
|
return api.serverTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -526,6 +526,7 @@ type SSHServer struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
HostKeyPolicy string `json:"host_key_policy"`
|
||||||
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 @@
|
|||||||
|
ALTER TABLE ssh_servers ADD COLUMN host_key_policy TEXT NOT NULL DEFAULT 'strict';
|
||||||
+15
-4
@@ -15,6 +15,7 @@ import "log"
|
|||||||
import "net"
|
import "net"
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "net/netip"
|
import "net/netip"
|
||||||
|
import "net/url"
|
||||||
import "os"
|
import "os"
|
||||||
import "path/filepath"
|
import "path/filepath"
|
||||||
import "sync"
|
import "sync"
|
||||||
@@ -412,13 +413,23 @@ func newServerWithInternalConfig(cfg *codit_config.Config, logger Logger, identi
|
|||||||
cfg.DockerHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.DockerHTTPPrefix, "/v2")
|
cfg.DockerHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.DockerHTTPPrefix, "/v2")
|
||||||
cfg.GitHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.GitHTTPPrefix, "/git")
|
cfg.GitHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.GitHTTPPrefix, "/git")
|
||||||
cfg.RPMHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.RPMHTTPPrefix, "/rpm")
|
cfg.RPMHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.RPMHTTPPrefix, "/rpm")
|
||||||
|
|
||||||
app.cfg = cfg
|
app.cfg = cfg
|
||||||
app.logger = logger
|
app.logger = logger
|
||||||
app.serverId = NormalizeServerId(identifier)
|
app.serverId = NormalizeServerId(identifier)
|
||||||
if cfg.App.SiteName == "" {
|
app.siteName = strings.TrimSpace(cfg.App.SiteName)
|
||||||
app.siteName = TitleFromServerId(app.serverId)
|
if app.siteName == "" {
|
||||||
} else {
|
// get the host from the public base url if set.
|
||||||
app.siteName = cfg.App.SiteName
|
var tmp string
|
||||||
|
tmp = strings.TrimSpace(cfg.PublicBaseURL)
|
||||||
|
if tmp != "" {
|
||||||
|
var parsed *url.URL
|
||||||
|
parsed, err = url.Parse(tmp)
|
||||||
|
if err == nil && parsed != nil {
|
||||||
|
app.siteName = strings.TrimSpace(parsed.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if app.siteName == "" { app.siteName = TitleFromServerId(app.serverId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
app.docker_base_dir = filepath.Join(cfg.DataDir, "docker")
|
app.docker_base_dir = filepath.Join(cfg.DataDir, "docker")
|
||||||
|
|||||||
@@ -521,9 +521,11 @@ Important fields:
|
|||||||
- `name`, `host`, `port`
|
- `name`, `host`, `port`
|
||||||
- `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.
|
||||||
- `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.
|
`created_by_kind = 'user'` indicates a personal server; admin-created servers are shared through access profiles and targets.
|
||||||
|
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`
|
||||||
|
|
||||||
|
|||||||
+40
-8
@@ -3922,11 +3922,11 @@ paths:
|
|||||||
operationId: CreateSSHServerAdmin
|
operationId: CreateSSHServerAdmin
|
||||||
x-codit-handler: CreateSSHServerAdmin
|
x-codit-handler: CreateSSHServerAdmin
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RequestObject'
|
$ref: '#/components/schemas/SSHServerRequest'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful response.
|
description: Successful response.
|
||||||
@@ -3977,11 +3977,11 @@ paths:
|
|||||||
operationId: UpdateSSHServerAdmin
|
operationId: UpdateSSHServerAdmin
|
||||||
x-codit-handler: UpdateSSHServerAdmin
|
x-codit-handler: UpdateSSHServerAdmin
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RequestObject'
|
$ref: '#/components/schemas/SSHServerRequest'
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@@ -4838,11 +4838,11 @@ paths:
|
|||||||
operationId: CreateSSHServerForSelf
|
operationId: CreateSSHServerForSelf
|
||||||
x-codit-handler: CreateSSHServerForSelf
|
x-codit-handler: CreateSSHServerForSelf
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RequestObject'
|
$ref: '#/components/schemas/SSHServerRequest'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful response.
|
description: Successful response.
|
||||||
@@ -4889,11 +4889,11 @@ paths:
|
|||||||
operationId: UpdateSSHServerForSelf
|
operationId: UpdateSSHServerForSelf
|
||||||
x-codit-handler: UpdateSSHServerForSelf
|
x-codit-handler: UpdateSSHServerForSelf
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RequestObject'
|
$ref: '#/components/schemas/SSHServerRequest'
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@@ -9515,9 +9515,41 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
host_key_policy:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- strict
|
||||||
|
- trust_on_first_use
|
||||||
editable:
|
editable:
|
||||||
type: boolean
|
type: boolean
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
|
SSHServerRequest:
|
||||||
|
type: object
|
||||||
|
description: 'Request payload for creating or updating an SSH server. host_key_policy defaults to strict if omitted or invalid.'
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
host:
|
||||||
|
type: string
|
||||||
|
port:
|
||||||
|
type: integer
|
||||||
|
default: 22
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
host_key_policy:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- strict
|
||||||
|
- trust_on_first_use
|
||||||
|
default: strict
|
||||||
|
description: 'strict requires a pinned host key before connecting. trust_on_first_use accepts the first observed host key and stores it as a pinned key.'
|
||||||
|
additionalProperties: false
|
||||||
SSHServerHostKey:
|
SSHServerHostKey:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -911,6 +911,7 @@ export interface SSHServer {
|
|||||||
description: string
|
description: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
host_key_policy: 'strict' | 'trust_on_first_use'
|
||||||
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
|
||||||
@@ -991,6 +992,7 @@ export type SSHServerUpsertPayload = {
|
|||||||
description: string
|
description: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
host_key_policy: 'strict' | 'trust_on_first_use'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SSHAccessProfileTargetPayload = {
|
export type SSHAccessProfileTargetPayload = {
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ function fmt(value: number): string {
|
|||||||
return new Date(value * 1000).toLocaleString()
|
return new Date(value * 1000).toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatHostKeyPolicy(value: string | undefined): string {
|
||||||
|
if (value === 'trust_on_first_use') {
|
||||||
|
return 'Trust on first use'
|
||||||
|
}
|
||||||
|
return 'Strict'
|
||||||
|
}
|
||||||
|
|
||||||
export default function SSHServerDetailsDialog(props: SSHServerDetailsDialogProps) {
|
export default function SSHServerDetailsDialog(props: SSHServerDetailsDialogProps) {
|
||||||
const item: SSHServer | null = props.item
|
const item: SSHServer | null = props.item
|
||||||
const showAdminFields: boolean = Boolean(props.showAdminFields)
|
const showAdminFields: boolean = Boolean(props.showAdminFields)
|
||||||
@@ -34,6 +41,7 @@ export default function SSHServerDetailsDialog(props: SSHServerDetailsDialogProp
|
|||||||
<TextField label="Host" value={item?.host || ''} InputProps={{ readOnly: true }} />
|
<TextField label="Host" value={item?.host || ''} InputProps={{ readOnly: true }} />
|
||||||
<TextField label="Port" value={String(item?.port || 0)} InputProps={{ readOnly: true }} />
|
<TextField label="Port" value={String(item?.port || 0)} InputProps={{ readOnly: true }} />
|
||||||
<TextField label="Enabled" value={item?.enabled ? 'yes' : 'no'} InputProps={{ readOnly: true }} />
|
<TextField label="Enabled" value={item?.enabled ? 'yes' : 'no'} InputProps={{ readOnly: true }} />
|
||||||
|
<TextField label="Host Key Policy" value={formatHostKeyPolicy(item?.host_key_policy)} InputProps={{ readOnly: true }} />
|
||||||
<TextField label="Tags" value={(item?.tags || []).join(', ') || '-'} InputProps={{ readOnly: true }} />
|
<TextField label="Tags" value={(item?.tags || []).join(', ') || '-'} InputProps={{ readOnly: true }} />
|
||||||
{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}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Dialog from '@mui/material/Dialog'
|
|||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import FormDialogContent from './FormDialogContent'
|
import FormDialogContent from './FormDialogContent'
|
||||||
@@ -17,6 +18,7 @@ export type SSHServerFormState = {
|
|||||||
description: string
|
description: string
|
||||||
tagsText: string
|
tagsText: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
hostKeyPolicy: 'strict' | 'trust_on_first_use'
|
||||||
}
|
}
|
||||||
|
|
||||||
type SSHServerFormDialogProps = {
|
type SSHServerFormDialogProps = {
|
||||||
@@ -67,6 +69,16 @@ export default function SSHServerFormDialog(props: SSHServerFormDialogProps) {
|
|||||||
onChange={(event) => props.setForm((prev) => ({ ...prev, tagsText: event.target.value }))}
|
onChange={(event) => props.setForm((prev) => ({ ...prev, tagsText: event.target.value }))}
|
||||||
helperText="Comma-separated tags."
|
helperText="Comma-separated tags."
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Host Key Policy"
|
||||||
|
value={props.form.hostKeyPolicy}
|
||||||
|
onChange={(event) => props.setForm((prev) => ({ ...prev, hostKeyPolicy: event.target.value as 'strict' | 'trust_on_first_use' }))}
|
||||||
|
helperText="Strict requires a pinned key. Trust on first use pins the first observed key."
|
||||||
|
>
|
||||||
|
<MenuItem value="strict">Strict - require pinned host key</MenuItem>
|
||||||
|
<MenuItem value="trust_on_first_use">Trust on first use - pin first observed host key</MenuItem>
|
||||||
|
</TextField>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<Checkbox checked={props.form.enabled} onChange={(event) => props.setForm((prev) => ({ ...prev, enabled: event.target.checked }))} />}
|
control={<Checkbox checked={props.form.enabled} onChange={(event) => props.setForm((prev) => ({ ...prev, enabled: event.target.checked }))} />}
|
||||||
label="Enabled"
|
label="Enabled"
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ const emptyForm = (): SSHServerFormState => ({
|
|||||||
port: 22,
|
port: 22,
|
||||||
description: '',
|
description: '',
|
||||||
tagsText: '',
|
tagsText: '',
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
hostKeyPolicy: 'strict'
|
||||||
})
|
})
|
||||||
|
|
||||||
const emptyGroupForm = (): SSHServerGroupFormState => ({
|
const emptyGroupForm = (): SSHServerGroupFormState => ({
|
||||||
@@ -249,7 +250,8 @@ export default function AdminSSHServersPage() {
|
|||||||
port: item.port,
|
port: item.port,
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
tagsText: (item.tags || []).join(', '),
|
tagsText: (item.tags || []).join(', '),
|
||||||
enabled: item.enabled
|
enabled: item.enabled,
|
||||||
|
hostKeyPolicy: item.host_key_policy || 'strict'
|
||||||
})
|
})
|
||||||
setDialogError(null)
|
setDialogError(null)
|
||||||
setDialogOpen(true)
|
setDialogOpen(true)
|
||||||
@@ -272,7 +274,8 @@ export default function AdminSSHServersPage() {
|
|||||||
port: Number(form.port) || 0,
|
port: Number(form.port) || 0,
|
||||||
description: form.description.trim(),
|
description: form.description.trim(),
|
||||||
tags: parseTags(form.tagsText),
|
tags: parseTags(form.tagsText),
|
||||||
enabled: form.enabled
|
enabled: form.enabled,
|
||||||
|
host_key_policy: form.hostKeyPolicy
|
||||||
}
|
}
|
||||||
if (!payload.name) {
|
if (!payload.name) {
|
||||||
setDialogError('Name is required')
|
setDialogError('Name is required')
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ const emptyServerForm = (): SSHServerFormState => ({
|
|||||||
port: 22,
|
port: 22,
|
||||||
description: '',
|
description: '',
|
||||||
tagsText: '',
|
tagsText: '',
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
hostKeyPolicy: 'trust_on_first_use'
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function SSHServersPage() {
|
export default function SSHServersPage() {
|
||||||
@@ -252,7 +253,8 @@ export default function SSHServersPage() {
|
|||||||
port: item.port,
|
port: item.port,
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
tagsText: (item.tags || []).join(', '),
|
tagsText: (item.tags || []).join(', '),
|
||||||
enabled: item.enabled
|
enabled: item.enabled,
|
||||||
|
hostKeyPolicy: item.host_key_policy || 'strict'
|
||||||
})
|
})
|
||||||
setServerDialogError(null)
|
setServerDialogError(null)
|
||||||
setServerDialogOpen(true)
|
setServerDialogOpen(true)
|
||||||
@@ -331,6 +333,18 @@ export default function SSHServersPage() {
|
|||||||
navigate(`/ssh-sessions?sessions=${encodedIDs}`)
|
navigate(`/ssh-sessions?sessions=${encodedIDs}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getConnectServerLabel = (item: SSHAccessProfile, serverID: string): string => {
|
||||||
|
const server: SSHServer | undefined = connectServerOptions.find((candidate: SSHServer) => candidate.id === serverID)
|
||||||
|
|
||||||
|
if (server) {
|
||||||
|
return `${server.name} (${server.host}:${server.port})`
|
||||||
|
}
|
||||||
|
if (!serverID && item.server) {
|
||||||
|
return `${item.server.name} (${item.server.host}:${item.server.port})`
|
||||||
|
}
|
||||||
|
return serverID || item.server_id || 'server'
|
||||||
|
}
|
||||||
|
|
||||||
const connectToProfileServers = async (item: SSHAccessProfile, serverIDs: string[], password?: string, otpCode?: string) => {
|
const connectToProfileServers = async (item: SSHAccessProfile, serverIDs: string[], password?: string, otpCode?: string) => {
|
||||||
let message: string
|
let message: string
|
||||||
let sessions: string[] = []
|
let sessions: string[] = []
|
||||||
@@ -349,11 +363,11 @@ export default function SSHServersPage() {
|
|||||||
sessions = [...sessions, session.session_id]
|
sessions = [...sessions, session.session_id]
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message = err instanceof Error ? err.message : 'Failed to connect'
|
message = err instanceof Error ? err.message : 'Failed to connect'
|
||||||
failures = [...failures, message]
|
failures = [...failures, `${getConnectServerLabel(item, serverID)}: ${message}`]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
throw new Error(failures[0] || 'Failed to connect')
|
throw new Error(failures.join('; ') || 'Failed to connect')
|
||||||
}
|
}
|
||||||
clearConnectServerSelection()
|
clearConnectServerSelection()
|
||||||
closeConnectPasswordPrompt()
|
closeConnectPasswordPrompt()
|
||||||
@@ -366,6 +380,7 @@ export default function SSHServersPage() {
|
|||||||
if (password !== undefined || otpCode !== undefined) {
|
if (password !== undefined || otpCode !== undefined) {
|
||||||
setConnectPasswordError(message)
|
setConnectPasswordError(message)
|
||||||
} else {
|
} else {
|
||||||
|
clearConnectServerSelection()
|
||||||
setError(message)
|
setError(message)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -489,7 +504,8 @@ export default function SSHServersPage() {
|
|||||||
port: Number(serverForm.port) || 0,
|
port: Number(serverForm.port) || 0,
|
||||||
description: serverForm.description.trim(),
|
description: serverForm.description.trim(),
|
||||||
tags: parseTags(serverForm.tagsText),
|
tags: parseTags(serverForm.tagsText),
|
||||||
enabled: serverForm.enabled
|
enabled: serverForm.enabled,
|
||||||
|
host_key_policy: serverForm.hostKeyPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!payload.name) {
|
if (!payload.name) {
|
||||||
|
|||||||
Reference in New Issue
Block a user