Compare commits
5 Commits
51c53418bd
...
aadda031a2
| Author | SHA1 | Date | |
|---|---|---|---|
| aadda031a2 | |||
| 3e669fdd60 | |||
| 5c2e013653 | |||
| 9cfe2fd398 | |||
| 8edce9f60a |
@@ -13,6 +13,16 @@ import "codit/internal/util"
|
|||||||
|
|
||||||
var ErrSSHServerHasSessionHistory error = errors.New("ssh server has session history")
|
var ErrSSHServerHasSessionHistory error = errors.New("ssh server has session history")
|
||||||
|
|
||||||
|
const SSHOwnerScopeUser = "user"
|
||||||
|
const SSHOwnerScopeAdmin = "admin"
|
||||||
|
const SSHOwnerScopeAdminShared = "admin_shared"
|
||||||
|
|
||||||
|
const SSHAccessProfileServerTargetTypeServer = "server"
|
||||||
|
const SSHAccessProfileServerTargetTypeGroup = "group"
|
||||||
|
|
||||||
|
const SSHAccessProfileTargetTypeUser = "user"
|
||||||
|
const SSHAccessProfileTargetTypeGroup = "group"
|
||||||
|
|
||||||
type SSHServerDeleteReferenceError struct {
|
type SSHServerDeleteReferenceError struct {
|
||||||
Kind string
|
Kind string
|
||||||
Count int64
|
Count int64
|
||||||
@@ -73,13 +83,10 @@ func (s *Store) GetSSHServer(id string) (models.SSHServer, error) {
|
|||||||
LEFT JOIN users ou ON ou.id = srv.owner_user_id
|
LEFT JOIN users ou ON ou.id = srv.owner_user_id
|
||||||
WHERE srv.public_id = ?`, strings.TrimSpace(id))
|
WHERE srv.public_id = ?`, strings.TrimSpace(id))
|
||||||
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)
|
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
|
|
||||||
}
|
|
||||||
item.Tags, err = decodeStringList(tagsJSON)
|
item.Tags, err = decodeStringList(tagsJSON)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +118,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.OwnerScope == "user" && item.OwnerUserID == trimmedUserID
|
item.Editable = item.OwnerScope == SSHOwnerScopeUser && item.OwnerUserID == trimmedUserID
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
@@ -151,7 +158,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.OwnerScope == "user" && item.OwnerUserID == trimmedUserID
|
item.Editable = item.OwnerScope == SSHOwnerScopeUser && item.OwnerUserID == trimmedUserID
|
||||||
if !item.Editable {
|
if !item.Editable {
|
||||||
return item, sql.ErrNoRows
|
return item, sql.ErrNoRows
|
||||||
}
|
}
|
||||||
@@ -171,11 +178,11 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
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)
|
item.OwnerScope = strings.TrimSpace(item.OwnerScope)
|
||||||
if item.OwnerScope != "user" {
|
if item.OwnerScope != SSHOwnerScopeUser {
|
||||||
item.OwnerScope = "admin"
|
item.OwnerScope = SSHOwnerScopeAdmin
|
||||||
item.OwnerUserID = ""
|
item.OwnerUserID = ""
|
||||||
}
|
}
|
||||||
if item.OwnerScope == "user" && strings.TrimSpace(item.OwnerUserID) == "" {
|
if item.OwnerScope == SSHOwnerScopeUser && strings.TrimSpace(item.OwnerUserID) == "" {
|
||||||
return item, errors.New("owner_user_id is required")
|
return item, errors.New("owner_user_id is required")
|
||||||
}
|
}
|
||||||
tagsJSON, err = encodeStringList(item.Tags)
|
tagsJSON, err = encodeStringList(item.Tags)
|
||||||
@@ -1073,7 +1080,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
|||||||
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
|
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
|
||||||
if err != nil { return nil, err }
|
if err != nil { return nil, err }
|
||||||
|
|
||||||
item.Editable = item.OwnerScope == "user" && item.OwnerUserID == trimmedUserID
|
item.Editable = item.OwnerScope == SSHOwnerScopeUser && item.OwnerUserID == trimmedUserID
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
@@ -1083,21 +1090,214 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSHAccessProfile, error) {
|
func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSHAccessProfile, error) {
|
||||||
var items []models.SSHAccessProfile
|
var row *sql.Row
|
||||||
var item models.SSHAccessProfile
|
var item models.SSHAccessProfile
|
||||||
var i int
|
var grantIDsJSON string
|
||||||
|
var serverTagsJSON string
|
||||||
|
var trimmedUserID string
|
||||||
|
var trimmedID string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
items, err = s.ListSSHAccessProfilesForUser(userID)
|
// this function gates the profiles with s.enabled=1.
|
||||||
|
// if we want all profiles, we should remove the condition
|
||||||
|
|
||||||
|
trimmedUserID = strings.TrimSpace(userID)
|
||||||
|
trimmedID = strings.TrimSpace(id)
|
||||||
|
row = s.QueryRow(`SELECT DISTINCT
|
||||||
|
p.public_id,
|
||||||
|
COALESCE(s.public_id, ''),
|
||||||
|
COALESCE(p.server_target_type, 'server'),
|
||||||
|
COALESCE(g.public_id, ''),
|
||||||
|
COALESCE(g.name, ''),
|
||||||
|
COALESCE(g.description, ''),
|
||||||
|
COALESCE(g.enabled, 0),
|
||||||
|
COALESCE(g.created_by_kind, ''),
|
||||||
|
COALESCE(g.created_by_subject_id, ''),
|
||||||
|
COALESCE(g.created_by_subject_name, ''),
|
||||||
|
COALESCE(g.created_at, 0),
|
||||||
|
COALESCE(g.updated_at, 0),
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.remote_username,
|
||||||
|
p.auth_method,
|
||||||
|
p.second_factor_mode,
|
||||||
|
p.owner_scope,
|
||||||
|
COALESCE(pou.public_id, ''),
|
||||||
|
p.allow_user_edit,
|
||||||
|
p.enabled,
|
||||||
|
COALESCE(sec.public_id, ''),
|
||||||
|
COALESCE(c.public_id, ''),
|
||||||
|
COALESCE(c.name, ''),
|
||||||
|
p.auth_public_key,
|
||||||
|
p.auth_public_key_fingerprint,
|
||||||
|
COALESCE(ca.public_id, ''),
|
||||||
|
p.ssh_principal_grant_ids_json,
|
||||||
|
p.default_cert_valid_seconds,
|
||||||
|
p.max_cert_valid_seconds,
|
||||||
|
p.valid_after,
|
||||||
|
p.valid_before,
|
||||||
|
p.created_by_kind,
|
||||||
|
p.created_by_subject_id,
|
||||||
|
p.created_by_subject_name,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at,
|
||||||
|
s.public_id,
|
||||||
|
s.name,
|
||||||
|
s.host,
|
||||||
|
s.port,
|
||||||
|
s.description,
|
||||||
|
s.tags_json,
|
||||||
|
s.enabled,
|
||||||
|
s.host_key_policy,
|
||||||
|
s.owner_scope,
|
||||||
|
COALESCE(sou.public_id, ''),
|
||||||
|
s.created_by_kind,
|
||||||
|
s.created_by_subject_id,
|
||||||
|
s.created_by_subject_name,
|
||||||
|
s.created_at,
|
||||||
|
s.updated_at
|
||||||
|
FROM ssh_access_profiles p
|
||||||
|
JOIN ssh_servers s ON s.id = p.server_id
|
||||||
|
LEFT JOIN users sou ON sou.id = s.owner_user_id
|
||||||
|
LEFT JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||||
|
LEFT JOIN users pou ON pou.id = p.owner_user_id
|
||||||
|
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||||
|
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||||
|
LEFT JOIN ssh_user_cas ca ON ca.id = p.ssh_user_ca_id
|
||||||
|
LEFT JOIN ssh_access_profile_targets t ON t.profile_id = p.id
|
||||||
|
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.id = t.target_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 tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||||
|
WHERE p.public_id = ?
|
||||||
|
AND (
|
||||||
|
(p.owner_scope = 'user' AND pou.public_id = ?)
|
||||||
|
OR
|
||||||
|
(
|
||||||
|
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 (
|
||||||
|
(t.target_type = 'user' AND tu.public_id = ?)
|
||||||
|
OR
|
||||||
|
(t.target_type = 'group' AND ug.disabled = 0 AND (ug.scope = 'all_users' OR gu.public_id = ?))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
LIMIT 1`, trimmedID, trimmedUserID, trimmedUserID, trimmedUserID)
|
||||||
|
err = row.Scan(
|
||||||
|
&item.ID,
|
||||||
|
&item.ServerID,
|
||||||
|
&item.ServerTargetType,
|
||||||
|
&item.ServerGroupID,
|
||||||
|
&item.ServerGroup.Name,
|
||||||
|
&item.ServerGroup.Description,
|
||||||
|
&item.ServerGroup.Enabled,
|
||||||
|
&item.ServerGroup.CreatedByKind,
|
||||||
|
&item.ServerGroup.CreatedBySubjectID,
|
||||||
|
&item.ServerGroup.CreatedBySubjectName,
|
||||||
|
&item.ServerGroup.CreatedAt,
|
||||||
|
&item.ServerGroup.UpdatedAt,
|
||||||
|
&item.Name,
|
||||||
|
&item.Description,
|
||||||
|
&item.RemoteUsername,
|
||||||
|
&item.AuthMethod,
|
||||||
|
&item.SecondFactorMode,
|
||||||
|
&item.OwnerScope,
|
||||||
|
&item.OwnerUserID,
|
||||||
|
&item.AllowUserEdit,
|
||||||
|
&item.Enabled,
|
||||||
|
&item.SecretID,
|
||||||
|
&item.SSHCredentialID,
|
||||||
|
&item.SSHCredentialName,
|
||||||
|
&item.AuthPublicKey,
|
||||||
|
&item.AuthPublicKeyFingerprint,
|
||||||
|
&item.SSHUserCAID,
|
||||||
|
&grantIDsJSON,
|
||||||
|
&item.DefaultCertValidSeconds,
|
||||||
|
&item.MaxCertValidSeconds,
|
||||||
|
&item.ValidAfter,
|
||||||
|
&item.ValidBefore,
|
||||||
|
&item.CreatedByKind,
|
||||||
|
&item.CreatedBySubjectID,
|
||||||
|
&item.CreatedBySubjectName,
|
||||||
|
&item.CreatedAt,
|
||||||
|
&item.UpdatedAt,
|
||||||
|
&item.Server.ID,
|
||||||
|
&item.Server.Name,
|
||||||
|
&item.Server.Host,
|
||||||
|
&item.Server.Port,
|
||||||
|
&item.Server.Description,
|
||||||
|
&serverTagsJSON,
|
||||||
|
&item.Server.Enabled,
|
||||||
|
&item.Server.HostKeyPolicy,
|
||||||
|
&item.Server.OwnerScope,
|
||||||
|
&item.Server.OwnerUserID,
|
||||||
|
&item.Server.CreatedByKind,
|
||||||
|
&item.Server.CreatedBySubjectID,
|
||||||
|
&item.Server.CreatedBySubjectName,
|
||||||
|
&item.Server.CreatedAt,
|
||||||
|
&item.Server.UpdatedAt,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return item, err
|
return item, err
|
||||||
}
|
}
|
||||||
for i = 0; i < len(items); i++ {
|
if item.ServerGroupID != "" {
|
||||||
if items[i].ID == strings.TrimSpace(id) {
|
item.ServerGroup.ID = item.ServerGroupID
|
||||||
return items[i], nil
|
|
||||||
}
|
}
|
||||||
|
item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON)
|
||||||
|
if err != nil {
|
||||||
|
return item, err
|
||||||
}
|
}
|
||||||
return item, sql.ErrNoRows
|
item.Server.Tags, err = decodeStringList(serverTagsJSON)
|
||||||
|
if err != nil {
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
|
||||||
|
if err != nil {
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
item.Editable = item.OwnerScope == SSHOwnerScopeUser && item.OwnerUserID == trimmedUserID
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetSSHAccessProfileServerForUser(userID string, id string, serverID string) (models.SSHServer, error) {
|
||||||
|
var profile models.SSHAccessProfile
|
||||||
|
var item models.SSHServer
|
||||||
|
var trimmedServerID string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// the ssh access profile must be found(id)
|
||||||
|
// the ssh access profile must be visible to the user(userID)
|
||||||
|
// the access profile must be bound to a server (serverID)
|
||||||
|
|
||||||
|
profile, err = s.GetSSHAccessProfileForUser(userID, id)
|
||||||
|
if err != nil { return item, err }
|
||||||
|
|
||||||
|
trimmedServerID = strings.TrimSpace(serverID)
|
||||||
|
if profile.ServerTargetType != SSHAccessProfileServerTargetTypeServer || profile.ServerID != trimmedServerID { return item, sql.ErrNoRows }
|
||||||
|
|
||||||
|
return profile.Server, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListSSHServersForAccessProfileServerGroupForUser(userID string, id string, serverGroupID string) ([]models.SSHServer, error) {
|
||||||
|
var profile models.SSHAccessProfile
|
||||||
|
var trimmedGroupID string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// the ssh access profile must be found(id)
|
||||||
|
// the ssh access profile must be visible to the user(userID)
|
||||||
|
// the access profile must be bound to a server group (serverGroupID)
|
||||||
|
|
||||||
|
profile, err = s.GetSSHAccessProfileForUser(userID, id)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
|
||||||
|
trimmedGroupID = strings.TrimSpace(serverGroupID)
|
||||||
|
if profile.ServerTargetType != SSHAccessProfileServerTargetTypeGroup || profile.ServerGroupID != trimmedGroupID { return nil, sql.ErrNoRows }
|
||||||
|
|
||||||
|
return s.ListSSHServersForGroup(trimmedGroupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) listSSHAccessProfileTargets(profileID string) ([]models.SSHAccessProfileTarget, error) {
|
func (s *Store) listSSHAccessProfileTargets(profileID string) ([]models.SSHAccessProfileTarget, error) {
|
||||||
@@ -1816,7 +2016,7 @@ func insertSSHAccessProfileTargetTx(tx *sql.Tx, profileID string, targetType str
|
|||||||
|
|
||||||
targetType = strings.ToLower(strings.TrimSpace(targetType))
|
targetType = strings.ToLower(strings.TrimSpace(targetType))
|
||||||
targetID = strings.TrimSpace(targetID)
|
targetID = strings.TrimSpace(targetID)
|
||||||
if targetType != "user" && targetType != "group" {
|
if targetType != SSHAccessProfileTargetTypeUser && targetType != SSHAccessProfileTargetTypeGroup {
|
||||||
return errors.New("target_type must be user or group")
|
return errors.New("target_type must be user or group")
|
||||||
}
|
}
|
||||||
if targetID == "" {
|
if targetID == "" {
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ func normalizeSSHCredentialOwner(item models.SSHCredential) models.SSHCredential
|
|||||||
item.OwnerScope = strings.TrimSpace(item.OwnerScope)
|
item.OwnerScope = strings.TrimSpace(item.OwnerScope)
|
||||||
item.OwnerUserID = strings.TrimSpace(item.OwnerUserID)
|
item.OwnerUserID = strings.TrimSpace(item.OwnerUserID)
|
||||||
if item.OwnerScope == "" {
|
if item.OwnerScope == "" {
|
||||||
item.OwnerScope = "admin"
|
item.OwnerScope = SSHOwnerScopeAdmin
|
||||||
}
|
}
|
||||||
if item.OwnerScope != "user" {
|
if item.OwnerScope != SSHOwnerScopeUser {
|
||||||
item.OwnerScope = "admin"
|
item.OwnerScope = SSHOwnerScopeAdmin
|
||||||
item.OwnerUserID = ""
|
item.OwnerUserID = ""
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
@@ -111,7 +111,7 @@ func (s *Store) CreateSSHCredential(item models.SSHCredential, secret models.SSH
|
|||||||
if strings.TrimSpace(item.Type) == "" { item.Type = "private_key" }
|
if strings.TrimSpace(item.Type) == "" { item.Type = "private_key" }
|
||||||
if strings.TrimSpace(item.Type) != "private_key" { return item, errors.New("unsupported credential type") }
|
if strings.TrimSpace(item.Type) != "private_key" { return item, errors.New("unsupported credential type") }
|
||||||
item = normalizeSSHCredentialOwner(item)
|
item = normalizeSSHCredentialOwner(item)
|
||||||
if item.OwnerScope == "user" && item.OwnerUserID == "" { return item, errors.New("owner_user_id is required") }
|
if item.OwnerScope == SSHOwnerScopeUser && item.OwnerUserID == "" { return item, errors.New("owner_user_id is required") }
|
||||||
if strings.TrimSpace(item.ID) == "" {
|
if strings.TrimSpace(item.ID) == "" {
|
||||||
item.ID, err = util.NewID()
|
item.ID, err = util.NewID()
|
||||||
if err != nil { return item, err }
|
if err != nil { return item, err }
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ 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",
|
OwnerScope: db.SSHOwnerScopeAdmin,
|
||||||
OwnerUserID: "",
|
OwnerUserID: "",
|
||||||
CreatedByKind: actorKind,
|
CreatedByKind: actorKind,
|
||||||
CreatedBySubjectID: actorID,
|
CreatedBySubjectID: actorID,
|
||||||
@@ -632,7 +632,7 @@ func (api *API) CreateSSHCredentialAdmin(w http.ResponseWriter, r *http.Request,
|
|||||||
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())),
|
||||||
Enabled: req.Enabled,
|
Enabled: req.Enabled,
|
||||||
OwnerScope: "admin",
|
OwnerScope: db.SSHOwnerScopeAdmin,
|
||||||
CreatedByKind: "user",
|
CreatedByKind: "user",
|
||||||
CreatedBySubjectID: user.ID,
|
CreatedBySubjectID: user.ID,
|
||||||
CreatedBySubjectName: user.Username,
|
CreatedBySubjectName: user.Username,
|
||||||
@@ -745,7 +745,7 @@ func (api *API) CreateSSHCredentialForSelf(w http.ResponseWriter, r *http.Reques
|
|||||||
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())),
|
||||||
Enabled: req.Enabled,
|
Enabled: req.Enabled,
|
||||||
OwnerScope: "user",
|
OwnerScope: db.SSHOwnerScopeUser,
|
||||||
OwnerUserID: user.ID,
|
OwnerUserID: user.ID,
|
||||||
CreatedByKind: "user",
|
CreatedByKind: "user",
|
||||||
CreatedBySubjectID: user.ID,
|
CreatedBySubjectID: user.ID,
|
||||||
@@ -869,7 +869,7 @@ func (api *API) CreateSSHAccessProfileAdmin(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
actorKind, actorID, actorName = currentSSHBrokerActor(r)
|
actorKind, actorID, actorName = currentSSHBrokerActor(r)
|
||||||
options = sshAccessProfileNormalizeOptions{
|
options = sshAccessProfileNormalizeOptions{
|
||||||
OwnerScope: "admin_shared",
|
OwnerScope: db.SSHOwnerScopeAdminShared,
|
||||||
OwnerUserID: "",
|
OwnerUserID: "",
|
||||||
AllowUserEdit: false,
|
AllowUserEdit: false,
|
||||||
RequireTargets: true,
|
RequireTargets: true,
|
||||||
@@ -917,7 +917,7 @@ func (api *API) UpdateSSHAccessProfileAdmin(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
actorKind, actorID, actorName = currentSSHBrokerActor(r)
|
actorKind, actorID, actorName = currentSSHBrokerActor(r)
|
||||||
options = sshAccessProfileNormalizeOptions{
|
options = sshAccessProfileNormalizeOptions{
|
||||||
OwnerScope: "admin_shared",
|
OwnerScope: db.SSHOwnerScopeAdminShared,
|
||||||
OwnerUserID: "",
|
OwnerUserID: "",
|
||||||
AllowUserEdit: false,
|
AllowUserEdit: false,
|
||||||
RequireTargets: true,
|
RequireTargets: true,
|
||||||
@@ -1059,21 +1059,79 @@ func (api *API) ListSSHAccessProfileServersForSelf(w http.ResponseWriter, r *htt
|
|||||||
}
|
}
|
||||||
profile, err = api.store(r).GetSSHAccessProfileForUser(user.ID, params["id"])
|
profile, err = api.store(r).GetSSHAccessProfileForUser(user.ID, params["id"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "ssh access profile not found")
|
var msg string = "ssh access profile not found"
|
||||||
|
if !errors.Is(err, sql.ErrNoRows) { msg = fmt.Sprintf("%s - %s", msg, err.Error()) }
|
||||||
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if profile.ServerTargetType == "group" {
|
|
||||||
|
if profile.ServerTargetType == db.SSHAccessProfileServerTargetTypeGroup {
|
||||||
items, err = api.store(r).ListSSHServersForGroup(profile.ServerGroupID)
|
items, err = api.store(r).ListSSHServersForGroup(profile.ServerGroupID)
|
||||||
} else {
|
|
||||||
items = []models.SSHServer{profile.Server}
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
items = []models.SSHServer{profile.Server}
|
||||||
|
}
|
||||||
|
|
||||||
WriteJSON(w, http.StatusOK, items)
|
WriteJSON(w, http.StatusOK, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) GetSSHAccessProfileServerForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
var user models.User
|
||||||
|
var server models.SSHServer
|
||||||
|
var ok bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
user, ok = middleware.UserFromContext(r.Context())
|
||||||
|
if !ok || user.Disabled {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function returns the server information of the server bound to the access profile.
|
||||||
|
// the server may not be owned by the caller in this context. it allows information
|
||||||
|
// retrieval if the access profile is shared with the caller.
|
||||||
|
|
||||||
|
server, err = api.store(r).GetSSHAccessProfileServerForUser(user.ID, params["id"], params["serverId"])
|
||||||
|
if err != nil {
|
||||||
|
var msg string = "ssh access profile server not found"
|
||||||
|
if !errors.Is(err, sql.ErrNoRows) { msg = fmt.Sprintf("%s - %s", msg, err.Error()) }
|
||||||
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJSON(w, http.StatusOK, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) ListSSHAccessProfileServerGroupMembersForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
var user models.User
|
||||||
|
var servers []models.SSHServer
|
||||||
|
var ok bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
user, ok = middleware.UserFromContext(r.Context())
|
||||||
|
if !ok || user.Disabled {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function returns the list of servers in the server group bound to the access profile.
|
||||||
|
// the servers may not be owned by the caller in this context. it allows information
|
||||||
|
// retrieval if the access profile is shared with the caller.
|
||||||
|
|
||||||
|
servers, err = api.store(r).ListSSHServersForAccessProfileServerGroupForUser(user.ID, params["id"], params["serverGroupId"])
|
||||||
|
if err != nil {
|
||||||
|
var msg string = "ssh access profile server group not found"
|
||||||
|
if !errors.Is(err, sql.ErrNoRows) { msg = fmt.Sprintf("%s - %s", msg, err.Error()) }
|
||||||
|
WriteJSONWithErrorReason(w, r, http.StatusNotFound, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJSON(w, http.StatusOK, servers)
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) ListSSHServersForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
func (api *API) ListSSHServersForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||||
var user models.User
|
var user models.User
|
||||||
var ok bool
|
var ok bool
|
||||||
@@ -1133,7 +1191,7 @@ 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",
|
OwnerScope: db.SSHOwnerScopeUser,
|
||||||
OwnerUserID: user.ID,
|
OwnerUserID: user.ID,
|
||||||
CreatedByKind: "user",
|
CreatedByKind: "user",
|
||||||
CreatedBySubjectID: user.ID,
|
CreatedBySubjectID: user.ID,
|
||||||
@@ -1389,7 +1447,7 @@ func (api *API) CreateSSHAccessProfileForSelf(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
options = sshAccessProfileNormalizeOptions{
|
options = sshAccessProfileNormalizeOptions{
|
||||||
OwnerScope: "user",
|
OwnerScope: db.SSHOwnerScopeUser,
|
||||||
OwnerUserID: user.ID,
|
OwnerUserID: user.ID,
|
||||||
AllowUserEdit: true,
|
AllowUserEdit: true,
|
||||||
RequireTargets: false,
|
RequireTargets: false,
|
||||||
@@ -1431,7 +1489,7 @@ func (api *API) UpdateSSHAccessProfileForSelf(w http.ResponseWriter, r *http.Req
|
|||||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if existing.OwnerScope != "user" || existing.OwnerUserID != user.ID {
|
if existing.OwnerScope != db.SSHOwnerScopeUser || existing.OwnerUserID != user.ID {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1441,7 +1499,7 @@ func (api *API) UpdateSSHAccessProfileForSelf(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
options = sshAccessProfileNormalizeOptions{
|
options = sshAccessProfileNormalizeOptions{
|
||||||
OwnerScope: "user",
|
OwnerScope: db.SSHOwnerScopeUser,
|
||||||
OwnerUserID: user.ID,
|
OwnerUserID: user.ID,
|
||||||
AllowUserEdit: true,
|
AllowUserEdit: true,
|
||||||
RequireTargets: false,
|
RequireTargets: false,
|
||||||
@@ -1480,7 +1538,7 @@ func (api *API) DeleteSSHAccessProfileForSelf(w http.ResponseWriter, r *http.Req
|
|||||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if item.OwnerScope != "user" || item.OwnerUserID != user.ID {
|
if item.OwnerScope != db.SSHOwnerScopeUser || item.OwnerUserID != user.ID {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1654,7 +1712,7 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
|
|||||||
map[string]string{"error": fmt.Sprintf("otp_code is required for %s", sshSecondFactorPromptedTOTP)})
|
map[string]string{"error": fmt.Sprintf("otp_code is required for %s", sshSecondFactorPromptedTOTP)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if profile.ServerTargetType == "group" {
|
if profile.ServerTargetType == db.SSHAccessProfileServerTargetTypeGroup {
|
||||||
req.ServerID = strings.TrimSpace(req.ServerID)
|
req.ServerID = strings.TrimSpace(req.ServerID)
|
||||||
if req.ServerID == "" {
|
if req.ServerID == "" {
|
||||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "server_id is required for server group profiles")
|
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "server_id is required for server group profiles")
|
||||||
@@ -2003,7 +2061,7 @@ func (api *API) normalizeSSHAccessProfileRequest(r *http.Request, req sshAccessP
|
|||||||
req.ServerTargetType = strings.TrimSpace(req.ServerTargetType)
|
req.ServerTargetType = strings.TrimSpace(req.ServerTargetType)
|
||||||
req.ServerGroupID = strings.TrimSpace(req.ServerGroupID)
|
req.ServerGroupID = strings.TrimSpace(req.ServerGroupID)
|
||||||
if req.ServerTargetType == "" {
|
if req.ServerTargetType == "" {
|
||||||
req.ServerTargetType = "server"
|
req.ServerTargetType = db.SSHAccessProfileServerTargetTypeServer
|
||||||
}
|
}
|
||||||
req.Name = strings.TrimSpace(req.Name)
|
req.Name = strings.TrimSpace(req.Name)
|
||||||
req.Description = strings.TrimSpace(req.Description)
|
req.Description = strings.TrimSpace(req.Description)
|
||||||
@@ -2018,11 +2076,11 @@ func (api *API) normalizeSSHAccessProfileRequest(r *http.Request, req sshAccessP
|
|||||||
if err != nil { return item, err }
|
if err != nil { return item, err }
|
||||||
|
|
||||||
if req.Name == "" { return item, errors.New("name is required") }
|
if req.Name == "" { return item, errors.New("name is required") }
|
||||||
if req.ServerTargetType != "server" && req.ServerTargetType != "group" {
|
if req.ServerTargetType != db.SSHAccessProfileServerTargetTypeServer && req.ServerTargetType != db.SSHAccessProfileServerTargetTypeGroup {
|
||||||
return item, errors.New("server_target_type must be server or group")
|
return item, errors.New("server_target_type must be server or group")
|
||||||
}
|
}
|
||||||
if req.ServerTargetType == "server" && req.ServerID == "" { return item, errors.New("server_id is required") }
|
if req.ServerTargetType == db.SSHAccessProfileServerTargetTypeServer && req.ServerID == "" { return item, errors.New("server_id is required") }
|
||||||
if req.ServerTargetType == "group" && req.ServerGroupID == "" { return item, errors.New("server_group_id is required") }
|
if req.ServerTargetType == db.SSHAccessProfileServerTargetTypeGroup && req.ServerGroupID == "" { return item, errors.New("server_group_id is required") }
|
||||||
if req.RemoteUsername == "" { return item, errors.New("remote_username is required") }
|
if req.RemoteUsername == "" { return item, errors.New("remote_username is required") }
|
||||||
|
|
||||||
if req.AuthMethod == "" {
|
if req.AuthMethod == "" {
|
||||||
@@ -2095,10 +2153,10 @@ func (api *API) normalizeSSHAccessProfileRequest(r *http.Request, req sshAccessP
|
|||||||
return item, errors.New("ssh_credential_id is disabled")
|
return item, errors.New("ssh_credential_id is disabled")
|
||||||
}
|
}
|
||||||
if options.SelfService {
|
if options.SelfService {
|
||||||
if credential.OwnerScope != "user" || credential.OwnerUserID != options.OwnerUserID {
|
if credential.OwnerScope != db.SSHOwnerScopeUser || credential.OwnerUserID != options.OwnerUserID {
|
||||||
return item, errors.New("ssh_credential_id not available to user")
|
return item, errors.New("ssh_credential_id not available to user")
|
||||||
}
|
}
|
||||||
} else if credential.OwnerScope == "user" {
|
} else if credential.OwnerScope == db.SSHOwnerScopeUser {
|
||||||
return item, errors.New("user-owned ssh credential is not allowed for admin access profiles")
|
return item, errors.New("user-owned ssh credential is not allowed for admin access profiles")
|
||||||
}
|
}
|
||||||
item.SSHCredentialID = credential.ID
|
item.SSHCredentialID = credential.ID
|
||||||
@@ -2163,7 +2221,7 @@ func (api *API) normalizeSSHAccessProfileRequest(r *http.Request, req sshAccessP
|
|||||||
if req.ValidAfter > 0 && req.ValidBefore > 0 && req.ValidAfter > req.ValidBefore {
|
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")
|
return item, errors.New("valid_after must be less than or equal to valid_before")
|
||||||
}
|
}
|
||||||
if req.ServerTargetType == "group" {
|
if req.ServerTargetType == db.SSHAccessProfileServerTargetTypeGroup {
|
||||||
var group models.SSHServerGroup
|
var group models.SSHServerGroup
|
||||||
group, err = api.store(r).GetSSHServerGroup(req.ServerGroupID)
|
group, err = api.store(r).GetSSHServerGroup(req.ServerGroupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2300,7 +2358,7 @@ func normalizeSSHAccessProfileTargets(raw []sshAccessProfileTargetRequest, allow
|
|||||||
for i = 0; i < len(raw); i++ {
|
for i = 0; i < len(raw); i++ {
|
||||||
targetType = strings.ToLower(strings.TrimSpace(raw[i].TargetType))
|
targetType = strings.ToLower(strings.TrimSpace(raw[i].TargetType))
|
||||||
targetID = strings.TrimSpace(raw[i].TargetID)
|
targetID = strings.TrimSpace(raw[i].TargetID)
|
||||||
if targetType != "user" && targetType != "group" {
|
if targetType != db.SSHAccessProfileTargetTypeUser && targetType != db.SSHAccessProfileTargetTypeGroup {
|
||||||
return nil, errors.New("target_type must be user or group")
|
return nil, errors.New("target_type must be user or group")
|
||||||
}
|
}
|
||||||
if targetID == "" {
|
if targetID == "" {
|
||||||
@@ -2327,7 +2385,7 @@ func validateSSHAccessProfileTargets(store *db.Store, targets []models.SSHAccess
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for i = 0; i < len(targets); i++ {
|
for i = 0; i < len(targets); i++ {
|
||||||
if targets[i].TargetType == "user" {
|
if targets[i].TargetType == db.SSHAccessProfileTargetTypeUser {
|
||||||
_, err = store.GetUserByID(targets[i].TargetID)
|
_, err = store.GetUserByID(targets[i].TargetID)
|
||||||
} else {
|
} else {
|
||||||
_, err = store.GetUserGroup(targets[i].TargetID)
|
_, err = store.GetUserGroup(targets[i].TargetID)
|
||||||
|
|||||||
@@ -12,14 +12,12 @@ type Handler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type route struct {
|
type route struct {
|
||||||
method string
|
parts []string // pre-split pattern segments
|
||||||
pattern string
|
|
||||||
//handler HandlerFunc
|
|
||||||
handler Handler
|
handler Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
routes []route
|
index map[string]map[int][]route // method -> segment count -> routes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hf HandlerFunc) Handle(w http.ResponseWriter, req *http.Request, params Params) {
|
func (hf HandlerFunc) Handle(w http.ResponseWriter, req *http.Request, params Params) {
|
||||||
@@ -27,18 +25,21 @@ func (hf HandlerFunc) Handle(w http.ResponseWriter, req *http.Request, params Pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter() *Router {
|
func NewRouter() *Router {
|
||||||
return &Router{}
|
return &Router{
|
||||||
|
index: make(map[string]map[int][]route),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) AddHandler(method string, pattern string, handler Handler) {
|
func (r *Router) AddHandler(method string, pattern string, handler Handler) {
|
||||||
|
var parts []string
|
||||||
var entry route
|
var entry route
|
||||||
|
|
||||||
// TODO: can I split the pattern a tree structure for faster lookup later?
|
parts = split(pattern)
|
||||||
// TODO: duplication check?
|
entry = route{parts: parts, handler: handler}
|
||||||
entry.method = method
|
if r.index[method] == nil {
|
||||||
entry.pattern = pattern
|
r.index[method] = make(map[int][]route)
|
||||||
entry.handler = handler
|
}
|
||||||
r.routes = append(r.routes, entry)
|
r.index[method][len(parts)] = append(r.index[method][len(parts)], entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) Handle(method string, pattern string, handler HandlerFunc) {
|
func (r *Router) Handle(method string, pattern string, handler HandlerFunc) {
|
||||||
@@ -46,58 +47,60 @@ func (r *Router) Handle(method string, pattern string, handler HandlerFunc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
var i int
|
var pathParts []string
|
||||||
|
var byDepth map[int][]route
|
||||||
|
var routes []route
|
||||||
var rt route
|
var rt route
|
||||||
var params Params
|
var params Params
|
||||||
var ok bool
|
var ok bool
|
||||||
for i = 0; i < len(r.routes); i++ {
|
|
||||||
rt = r.routes[i]
|
|
||||||
if rt.method != req.Method {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
params, ok = match(rt.pattern, req.URL.Path)
|
|
||||||
if !ok { continue }
|
|
||||||
|
|
||||||
|
pathParts = split(req.URL.Path)
|
||||||
|
|
||||||
|
byDepth, ok = r.index[req.Method]
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, ok = byDepth[len(pathParts)]
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rt = range routes {
|
||||||
|
params, ok = match(rt.parts, pathParts)
|
||||||
|
if !ok { continue }
|
||||||
//rt.handler(w, req, params) <-- if Handler is HandlerFunc
|
//rt.handler(w, req, params) <-- if Handler is HandlerFunc
|
||||||
rt.handler.Handle(w, req, params) // <-- handler is the Handler interface
|
rt.handler.Handle(w, req, params) // <-- handler is the Handler interface
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func match(pattern, path string) (Params, bool) {
|
// match assumes pParts and pathParts are the same length (guaranteed by the index).
|
||||||
var pParts []string
|
func match(pParts, pathParts []string) (Params, bool) {
|
||||||
var pathParts []string
|
|
||||||
var params Params
|
var params Params
|
||||||
var i int
|
var i int
|
||||||
var p string
|
var p string
|
||||||
var seg string
|
var seg string
|
||||||
pParts = split(pattern)
|
|
||||||
pathParts = split(path)
|
|
||||||
if len(pParts) != len(pathParts) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
params = Params{}
|
|
||||||
for i = 0; i < len(pParts); i++ {
|
|
||||||
p = pParts[i]
|
|
||||||
seg = pathParts[i]
|
|
||||||
|
|
||||||
|
for i, p = range pParts {
|
||||||
|
seg = pathParts[i]
|
||||||
if strings.HasPrefix(p, ":") {
|
if strings.HasPrefix(p, ":") {
|
||||||
|
if params == nil { params = make(Params) }
|
||||||
params[p[1:]] = seg
|
params[p[1:]] = seg
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if p != seg { return nil, false }
|
||||||
if p != seg {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if params == nil { params = Params{} }
|
||||||
return params, true
|
return params, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func split(path string) []string {
|
func split(path string) []string {
|
||||||
path = strings.Trim(path, "/")
|
path = strings.Trim(path, "/")
|
||||||
if path == "" {
|
if path == "" { return []string{} }
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return strings.Split(path, "/")
|
return strings.Split(path, "/")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -779,11 +779,15 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
|||||||
router.Handle("POST", "/api/ssh/credentials", api.CreateSSHCredentialForSelf)
|
router.Handle("POST", "/api/ssh/credentials", api.CreateSSHCredentialForSelf)
|
||||||
router.Handle("PATCH", "/api/ssh/credentials/:id", api.UpdateSSHCredentialForSelf)
|
router.Handle("PATCH", "/api/ssh/credentials/:id", api.UpdateSSHCredentialForSelf)
|
||||||
router.Handle("DELETE", "/api/ssh/credentials/:id", api.DeleteSSHCredentialForSelf)
|
router.Handle("DELETE", "/api/ssh/credentials/:id", api.DeleteSSHCredentialForSelf)
|
||||||
|
|
||||||
router.Handle("GET", "/api/ssh/access-profiles", api.ListSSHAccessProfilesForSelf)
|
router.Handle("GET", "/api/ssh/access-profiles", api.ListSSHAccessProfilesForSelf)
|
||||||
router.Handle("POST", "/api/ssh/access-profiles", api.CreateSSHAccessProfileForSelf)
|
router.Handle("POST", "/api/ssh/access-profiles", api.CreateSSHAccessProfileForSelf)
|
||||||
router.Handle("GET", "/api/ssh/access-profiles/:id", api.GetSSHAccessProfileForSelf)
|
router.Handle("GET", "/api/ssh/access-profiles/:id", api.GetSSHAccessProfileForSelf)
|
||||||
router.Handle("GET", "/api/ssh/access-profiles/:id/credential", api.GetSSHAccessProfileCredentialForSelf)
|
router.Handle("GET", "/api/ssh/access-profiles/:id/credential", api.GetSSHAccessProfileCredentialForSelf)
|
||||||
router.Handle("GET", "/api/ssh/access-profiles/:id/servers", api.ListSSHAccessProfileServersForSelf)
|
router.Handle("GET", "/api/ssh/access-profiles/:id/servers", api.ListSSHAccessProfileServersForSelf)
|
||||||
|
router.Handle("GET", "/api/ssh/access-profiles/:id/servers/:serverId", api.GetSSHAccessProfileServerForSelf)
|
||||||
|
router.Handle("GET", "/api/ssh/access-profiles/:id/server-groups/:serverGroupId/servers", api.ListSSHAccessProfileServerGroupMembersForSelf)
|
||||||
|
|
||||||
router.Handle("PATCH", "/api/ssh/access-profiles/:id", api.UpdateSSHAccessProfileForSelf)
|
router.Handle("PATCH", "/api/ssh/access-profiles/:id", api.UpdateSSHAccessProfileForSelf)
|
||||||
router.Handle("DELETE", "/api/ssh/access-profiles/:id", api.DeleteSSHAccessProfileForSelf)
|
router.Handle("DELETE", "/api/ssh/access-profiles/:id", api.DeleteSSHAccessProfileForSelf)
|
||||||
router.Handle("POST", "/api/ssh/access-profiles/:id/connect", api.CreateSSHSessionForSelf)
|
router.Handle("POST", "/api/ssh/access-profiles/:id/connect", api.CreateSSHSessionForSelf)
|
||||||
|
|||||||
@@ -1604,6 +1604,10 @@ export const api = {
|
|||||||
getSSHAccessProfileForSelf: (id: string, options?: RequestInit) => request<SSHAccessProfile>(`/api/ssh/access-profiles/${id}`),
|
getSSHAccessProfileForSelf: (id: string, options?: RequestInit) => request<SSHAccessProfile>(`/api/ssh/access-profiles/${id}`),
|
||||||
getSSHAccessProfileCredentialForSelf: (id: string) => request<SSHCredential>(`/api/ssh/access-profiles/${id}/credential`),
|
getSSHAccessProfileCredentialForSelf: (id: string) => request<SSHCredential>(`/api/ssh/access-profiles/${id}/credential`),
|
||||||
listSSHAccessProfileServersForSelf: (id: string) => request<SSHServer[]>(`/api/ssh/access-profiles/${id}/servers`),
|
listSSHAccessProfileServersForSelf: (id: string) => request<SSHServer[]>(`/api/ssh/access-profiles/${id}/servers`),
|
||||||
|
|
||||||
|
getSSHAccessProfileServerForSelf: (id: string, serverID: string) => request<SSHServer>(`/api/ssh/access-profiles/${id}/servers/${serverID}`),
|
||||||
|
listSSHAccessProfileServerGroupMembersForSelf: (id: string, serverGroupID: string) => request<SSHServer[]>(`/api/ssh/access-profiles/${id}/server-groups/${serverGroupID}/servers`),
|
||||||
|
|
||||||
updateSSHAccessProfileForSelf: (id: string, payload: SSHAccessProfileSelfUpsertPayload) =>
|
updateSSHAccessProfileForSelf: (id: string, payload: SSHAccessProfileSelfUpsertPayload) =>
|
||||||
request<SSHAccessProfile>(`/api/ssh/access-profiles/${id}`, { method: 'PATCH', body: JSON.stringify(payload) }),
|
request<SSHAccessProfile>(`/api/ssh/access-profiles/${id}`, { method: 'PATCH', body: JSON.stringify(payload) }),
|
||||||
deleteSSHAccessProfileForSelf: (id: string) => request<void>(`/api/ssh/access-profiles/${id}`, { method: 'DELETE' }),
|
deleteSSHAccessProfileForSelf: (id: string) => request<void>(`/api/ssh/access-profiles/${id}`, { method: 'DELETE' }),
|
||||||
|
|||||||
@@ -7,5 +7,20 @@ const compactSx: SxProps<Theme> = { minWidth: 0, m: 0 }
|
|||||||
export default function CompactListItemText(props: ListItemTextProps) {
|
export default function CompactListItemText(props: ListItemTextProps) {
|
||||||
const sx: SxProps<Theme> = (props.sx ? [compactSx, props.sx] : compactSx) as SxProps<Theme>
|
const sx: SxProps<Theme> = (props.sx ? [compactSx, props.sx] : compactSx) as SxProps<Theme>
|
||||||
|
|
||||||
return <ListItemText {...props} sx={sx} />
|
// <ListItemText primary="Users" secondary="42 members" />
|
||||||
|
// is roughly becomes
|
||||||
|
// <span class="MuiListItemText-primary">Users</span>
|
||||||
|
// <p class="MuiListItemText-secondary">42 members</p>
|
||||||
|
// If we use some structural items that can be translated to <div>,
|
||||||
|
// i get something like "Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>".
|
||||||
|
|
||||||
|
//return <ListItemText {...props} disableTypography sx={sx} />
|
||||||
|
return <ListItemText
|
||||||
|
{...props}
|
||||||
|
sx={sx}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
component: 'div', // defaults to div
|
||||||
|
...props.secondaryTypographyProps, // the caller still can override component
|
||||||
|
}}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type SSHServerGroupDetailsDialogProps = {
|
|||||||
item: SSHServerGroup | null
|
item: SSHServerGroup | null
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
showAdminFields?: boolean
|
showAdminFields?: boolean
|
||||||
|
accessProfileID?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmt(value: number): string {
|
function fmt(value: number): string {
|
||||||
@@ -49,7 +50,12 @@ export default function SSHServerGroupDetailsDialog(props: SSHServerGroupDetails
|
|||||||
|
|
||||||
setMembersLoading(true)
|
setMembersLoading(true)
|
||||||
setMembersError(null)
|
setMembersError(null)
|
||||||
api.listSSHServerGroupMembersAdmin(item.id).then((list: SSHServer[]) => {
|
|
||||||
|
const request: Promise<SSHServer[]> = props.accessProfileID?
|
||||||
|
api.listSSHAccessProfileServerGroupMembersForSelf(props.accessProfileID, item.id):
|
||||||
|
api.listSSHServerGroupMembersAdmin(item.id)
|
||||||
|
|
||||||
|
request.then((list: SSHServer[]) => {
|
||||||
if (cancelled) { return }
|
if (cancelled) { return }
|
||||||
setMemberServers(Array.isArray(list) ? list : [])
|
setMemberServers(Array.isArray(list) ? list : [])
|
||||||
}).catch((err: unknown) => {
|
}).catch((err: unknown) => {
|
||||||
@@ -62,7 +68,7 @@ export default function SSHServerGroupDetailsDialog(props: SSHServerGroupDetails
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [item])
|
}, [item, props.accessProfileID])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={Boolean(item)} onClose={props.onClose} maxWidth="sm" fullWidth>
|
<Dialog open={Boolean(item)} onClose={props.onClose} maxWidth="sm" fullWidth>
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export default function AdminApiKeysPage() {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{key.name}({key.prefix})
|
{key.name}({key.prefix})
|
||||||
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export default function AdminPKIClientProfilesPage() {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ export default function AdminSSHAccessProfilesPage() {
|
|||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{item.name}{' '}
|
{item.name}{' '}
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default function AdminSSHCredentialsPage() {
|
|||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ export default function AdminSSHServersPage() {
|
|||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -560,7 +560,7 @@ export default function AdminSSHServersPage() {
|
|||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export default function AdminSSHSessionsPage() {
|
|||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{sessionTitle(item)}{' '}
|
{sessionTitle(item)}{' '}
|
||||||
<Chip
|
<Chip
|
||||||
label={item.status || 'unknown'}
|
label={item.status || 'unknown'}
|
||||||
@@ -476,7 +476,7 @@ export default function AdminSSHSessionsPage() {
|
|||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{formatTransferOperation(item.operation)} · {item.server_name || item.server_id || '-'}
|
{formatTransferOperation(item.operation)} · {item.server_name || item.server_id || '-'}
|
||||||
{item.target_server_name ? ` → ${item.target_server_name}` : ''}{' '}
|
{item.target_server_name ? ` → ${item.target_server_name}` : ''}{' '}
|
||||||
<Chip
|
<Chip
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ export default function AdminSSHUserCAPage() {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ export default function AdminUserGroupsPage() {
|
|||||||
>
|
>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography noWrap>
|
<Typography component="div">
|
||||||
{group.name}
|
{group.name}
|
||||||
{group.disabled? (<> <Chip size="small" label="Disabled" color="error" sx={{ mt: 0.25, flexShrink: 0 }} /></>): null}
|
{group.disabled? (<> <Chip size="small" label="Disabled" color="error" sx={{ mt: 0.25, flexShrink: 0 }} /></>): null}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export default function AdminUsersPage() {
|
|||||||
>
|
>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{u.display_name || u.username}({u.username})
|
{u.display_name || u.username}({u.username})
|
||||||
{u.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
{u.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export default function ApiKeysPage() {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{key.name}({key.prefix})
|
{key.name}({key.prefix})
|
||||||
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ export default function ClientCertificatesPage() {
|
|||||||
>
|
>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{item.common_name} ({item.cert_id})
|
{item.common_name} ({item.cert_id})
|
||||||
{' '}
|
{' '}
|
||||||
<Chip
|
<Chip
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default function SSHCredentialsPage() {
|
|||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<CompactListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography component="div">
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export default function SSHServersPage() {
|
|||||||
const [viewServerItem, setViewServerItem] = useState<SSHServer | null>(null)
|
const [viewServerItem, setViewServerItem] = useState<SSHServer | null>(null)
|
||||||
const [viewSharedServerItem, setViewSharedServerItem] = useState<SSHServer | null>(null)
|
const [viewSharedServerItem, setViewSharedServerItem] = useState<SSHServer | null>(null)
|
||||||
const [viewServerGroupItem, setViewServerGroupItem] = useState<SSHServerGroup | null>(null)
|
const [viewServerGroupItem, setViewServerGroupItem] = useState<SSHServerGroup | null>(null)
|
||||||
|
const [accessProfileIdForServerGroup, setAccessProfileIdForServerGroup] = useState<string | null>(null)
|
||||||
const [hostKeyServer, setHostKeyServer] = useState<SSHServer | null>(null)
|
const [hostKeyServer, setHostKeyServer] = useState<SSHServer | null>(null)
|
||||||
const [hostKeys, setHostKeys] = useState<SSHServerHostKey[]>([])
|
const [hostKeys, setHostKeys] = useState<SSHServerHostKey[]>([])
|
||||||
const [hostKeysLoading, setHostKeysLoading] = useState(false)
|
const [hostKeysLoading, setHostKeysLoading] = useState(false)
|
||||||
@@ -208,9 +209,10 @@ export default function SSHServersPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openServerGroupView = (item: SSHServerGroup | null) => {
|
const openServerGroupView = (profile_id: string, group: SSHServerGroup | null) => {
|
||||||
// open the ssh server group details dialog
|
// open the ssh server group details dialog
|
||||||
setViewServerGroupItem(item)
|
setViewServerGroupItem(group)
|
||||||
|
setAccessProfileIdForServerGroup(profile_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
@@ -664,7 +666,7 @@ 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>
|
<Typography component="div">
|
||||||
{item.name}
|
{item.name}
|
||||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -675,7 +677,7 @@ export default function SSHServersPage() {
|
|||||||
{item.server_target_type === 'group' ? (
|
{item.server_target_type === 'group' ? (
|
||||||
<Link
|
<Link
|
||||||
underline="hover"
|
underline="hover"
|
||||||
onClick={() => void openServerGroupView(item.server_group)}
|
onClick={() => void openServerGroupView(item.id, item.server_group)}
|
||||||
sx={{ cursor: 'default' }}
|
sx={{ cursor: 'default' }}
|
||||||
>
|
>
|
||||||
Group: {item.server_group?.name || item.server_group_id || '-'}
|
Group: {item.server_group?.name || item.server_group_id || '-'}
|
||||||
@@ -753,8 +755,8 @@ 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>
|
<Typography component="div">
|
||||||
{item.name}
|
{item.name}({item.id})
|
||||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
@@ -764,9 +766,9 @@ export default function SSHServersPage() {
|
|||||||
{item.host}:{item.port}
|
{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(', ') || '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
{item.description ? (
|
{item.description? (
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
{item.description}
|
{item.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -920,6 +922,7 @@ export default function SSHServersPage() {
|
|||||||
<SSHServerGroupDetailsDialog
|
<SSHServerGroupDetailsDialog
|
||||||
item={viewServerGroupItem}
|
item={viewServerGroupItem}
|
||||||
onClose={() => setViewServerGroupItem(null) }
|
onClose={() => setViewServerGroupItem(null) }
|
||||||
|
accessProfileID={accessProfileIdForServerGroup}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDeleteDialog
|
<ConfirmDeleteDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user