Compare commits
5 Commits
| 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")
|
||||
|
||||
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 {
|
||||
Kind string
|
||||
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
|
||||
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)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if err != nil { return item, err }
|
||||
|
||||
item.Tags, err = decodeStringList(tagsJSON)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if err != nil { return item, err }
|
||||
return item, nil
|
||||
}
|
||||
|
||||
@@ -111,7 +118,7 @@ func (s *Store) ListSSHServersForUser(userID string) ([]models.SSHServer, error)
|
||||
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)
|
||||
}
|
||||
err = rows.Err()
|
||||
@@ -151,7 +158,7 @@ func (s *Store) GetOwnedSSHServerForUser(userID string, id string) (models.SSHSe
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
item.Editable = item.OwnerScope == "user" && item.OwnerUserID == trimmedUserID
|
||||
item.Editable = item.OwnerScope == SSHOwnerScopeUser && item.OwnerUserID == trimmedUserID
|
||||
if !item.Editable {
|
||||
return item, sql.ErrNoRows
|
||||
}
|
||||
@@ -171,11 +178,11 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
||||
item.HostKeyPolicy = normalizeSSHServerHostKeyPolicy(item.HostKeyPolicy)
|
||||
item.Tags = normalizeStringList(item.Tags)
|
||||
item.OwnerScope = strings.TrimSpace(item.OwnerScope)
|
||||
if item.OwnerScope != "user" {
|
||||
item.OwnerScope = "admin"
|
||||
if item.OwnerScope != SSHOwnerScopeUser {
|
||||
item.OwnerScope = SSHOwnerScopeAdmin
|
||||
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")
|
||||
}
|
||||
tagsJSON, err = encodeStringList(item.Tags)
|
||||
@@ -1073,7 +1080,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
var items []models.SSHAccessProfile
|
||||
var row *sql.Row
|
||||
var item models.SSHAccessProfile
|
||||
var i int
|
||||
var grantIDsJSON string
|
||||
var serverTagsJSON string
|
||||
var trimmedUserID string
|
||||
var trimmedID string
|
||||
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 {
|
||||
return item, err
|
||||
}
|
||||
for i = 0; i < len(items); i++ {
|
||||
if items[i].ID == strings.TrimSpace(id) {
|
||||
return items[i], nil
|
||||
}
|
||||
if item.ServerGroupID != "" {
|
||||
item.ServerGroup.ID = item.ServerGroupID
|
||||
}
|
||||
return item, sql.ErrNoRows
|
||||
item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
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) {
|
||||
@@ -1816,7 +2016,7 @@ func insertSSHAccessProfileTargetTx(tx *sql.Tx, profileID string, targetType str
|
||||
|
||||
targetType = strings.ToLower(strings.TrimSpace(targetType))
|
||||
targetID = strings.TrimSpace(targetID)
|
||||
if targetType != "user" && targetType != "group" {
|
||||
if targetType != SSHAccessProfileTargetTypeUser && targetType != SSHAccessProfileTargetTypeGroup {
|
||||
return errors.New("target_type must be user or group")
|
||||
}
|
||||
if targetID == "" {
|
||||
|
||||
@@ -16,10 +16,10 @@ func normalizeSSHCredentialOwner(item models.SSHCredential) models.SSHCredential
|
||||
item.OwnerScope = strings.TrimSpace(item.OwnerScope)
|
||||
item.OwnerUserID = strings.TrimSpace(item.OwnerUserID)
|
||||
if item.OwnerScope == "" {
|
||||
item.OwnerScope = "admin"
|
||||
item.OwnerScope = SSHOwnerScopeAdmin
|
||||
}
|
||||
if item.OwnerScope != "user" {
|
||||
item.OwnerScope = "admin"
|
||||
if item.OwnerScope != SSHOwnerScopeUser {
|
||||
item.OwnerScope = SSHOwnerScopeAdmin
|
||||
item.OwnerUserID = ""
|
||||
}
|
||||
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) != "private_key" { return item, errors.New("unsupported credential type") }
|
||||
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) == "" {
|
||||
item.ID, err = util.NewID()
|
||||
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),
|
||||
Enabled: req.Enabled,
|
||||
HostKeyPolicy: normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy),
|
||||
OwnerScope: "admin",
|
||||
OwnerScope: db.SSHOwnerScopeAdmin,
|
||||
OwnerUserID: "",
|
||||
CreatedByKind: actorKind,
|
||||
CreatedBySubjectID: actorID,
|
||||
@@ -632,7 +632,7 @@ func (api *API) CreateSSHCredentialAdmin(w http.ResponseWriter, r *http.Request,
|
||||
PublicKey: strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))),
|
||||
Fingerprint: strings.TrimSpace(ssh.FingerprintSHA256(signer.PublicKey())),
|
||||
Enabled: req.Enabled,
|
||||
OwnerScope: "admin",
|
||||
OwnerScope: db.SSHOwnerScopeAdmin,
|
||||
CreatedByKind: "user",
|
||||
CreatedBySubjectID: user.ID,
|
||||
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()))),
|
||||
Fingerprint: strings.TrimSpace(ssh.FingerprintSHA256(signer.PublicKey())),
|
||||
Enabled: req.Enabled,
|
||||
OwnerScope: "user",
|
||||
OwnerScope: db.SSHOwnerScopeUser,
|
||||
OwnerUserID: user.ID,
|
||||
CreatedByKind: "user",
|
||||
CreatedBySubjectID: user.ID,
|
||||
@@ -869,7 +869,7 @@ func (api *API) CreateSSHAccessProfileAdmin(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
actorKind, actorID, actorName = currentSSHBrokerActor(r)
|
||||
options = sshAccessProfileNormalizeOptions{
|
||||
OwnerScope: "admin_shared",
|
||||
OwnerScope: db.SSHOwnerScopeAdminShared,
|
||||
OwnerUserID: "",
|
||||
AllowUserEdit: false,
|
||||
RequireTargets: true,
|
||||
@@ -917,7 +917,7 @@ func (api *API) UpdateSSHAccessProfileAdmin(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
actorKind, actorID, actorName = currentSSHBrokerActor(r)
|
||||
options = sshAccessProfileNormalizeOptions{
|
||||
OwnerScope: "admin_shared",
|
||||
OwnerScope: db.SSHOwnerScopeAdminShared,
|
||||
OwnerUserID: "",
|
||||
AllowUserEdit: false,
|
||||
RequireTargets: true,
|
||||
@@ -1059,19 +1059,77 @@ func (api *API) ListSSHAccessProfileServersForSelf(w http.ResponseWriter, r *htt
|
||||
}
|
||||
profile, err = api.store(r).GetSSHAccessProfileForUser(user.ID, params["id"])
|
||||
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
|
||||
}
|
||||
if profile.ServerTargetType == "group" {
|
||||
|
||||
if profile.ServerTargetType == db.SSHAccessProfileServerTargetTypeGroup {
|
||||
items, err = api.store(r).ListSSHServersForGroup(profile.ServerGroupID)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
items = []models.SSHServer{profile.Server}
|
||||
}
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
|
||||
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
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, items)
|
||||
|
||||
// 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) {
|
||||
@@ -1133,7 +1191,7 @@ func (api *API) CreateSSHServerForSelf(w http.ResponseWriter, r *http.Request, _
|
||||
Tags: normalizeSSHBrokerTags(req.Tags),
|
||||
Enabled: req.Enabled,
|
||||
HostKeyPolicy: normalizeSSHServerHostKeyPolicy(req.HostKeyPolicy),
|
||||
OwnerScope: "user",
|
||||
OwnerScope: db.SSHOwnerScopeUser,
|
||||
OwnerUserID: user.ID,
|
||||
CreatedByKind: "user",
|
||||
CreatedBySubjectID: user.ID,
|
||||
@@ -1389,7 +1447,7 @@ func (api *API) CreateSSHAccessProfileForSelf(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
options = sshAccessProfileNormalizeOptions{
|
||||
OwnerScope: "user",
|
||||
OwnerScope: db.SSHOwnerScopeUser,
|
||||
OwnerUserID: user.ID,
|
||||
AllowUserEdit: true,
|
||||
RequireTargets: false,
|
||||
@@ -1431,7 +1489,7 @@ func (api *API) UpdateSSHAccessProfileForSelf(w http.ResponseWriter, r *http.Req
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if existing.OwnerScope != "user" || existing.OwnerUserID != user.ID {
|
||||
if existing.OwnerScope != db.SSHOwnerScopeUser || existing.OwnerUserID != user.ID {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -1441,7 +1499,7 @@ func (api *API) UpdateSSHAccessProfileForSelf(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
options = sshAccessProfileNormalizeOptions{
|
||||
OwnerScope: "user",
|
||||
OwnerScope: db.SSHOwnerScopeUser,
|
||||
OwnerUserID: user.ID,
|
||||
AllowUserEdit: true,
|
||||
RequireTargets: false,
|
||||
@@ -1480,7 +1538,7 @@ func (api *API) DeleteSSHAccessProfileForSelf(w http.ResponseWriter, r *http.Req
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if item.OwnerScope != "user" || item.OwnerUserID != user.ID {
|
||||
if item.OwnerScope != db.SSHOwnerScopeUser || item.OwnerUserID != user.ID {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
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)})
|
||||
return
|
||||
}
|
||||
if profile.ServerTargetType == "group" {
|
||||
if profile.ServerTargetType == db.SSHAccessProfileServerTargetTypeGroup {
|
||||
req.ServerID = strings.TrimSpace(req.ServerID)
|
||||
if req.ServerID == "" {
|
||||
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.ServerGroupID = strings.TrimSpace(req.ServerGroupID)
|
||||
if req.ServerTargetType == "" {
|
||||
req.ServerTargetType = "server"
|
||||
req.ServerTargetType = db.SSHAccessProfileServerTargetTypeServer
|
||||
}
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
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 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")
|
||||
}
|
||||
if req.ServerTargetType == "server" && 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.SSHAccessProfileServerTargetTypeServer && req.ServerID == "" { return item, errors.New("server_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.AuthMethod == "" {
|
||||
@@ -2095,10 +2153,10 @@ func (api *API) normalizeSSHAccessProfileRequest(r *http.Request, req sshAccessP
|
||||
return item, errors.New("ssh_credential_id is disabled")
|
||||
}
|
||||
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")
|
||||
}
|
||||
} 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")
|
||||
}
|
||||
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 {
|
||||
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
|
||||
group, err = api.store(r).GetSSHServerGroup(req.ServerGroupID)
|
||||
if err != nil {
|
||||
@@ -2300,7 +2358,7 @@ func normalizeSSHAccessProfileTargets(raw []sshAccessProfileTargetRequest, allow
|
||||
for i = 0; i < len(raw); i++ {
|
||||
targetType = strings.ToLower(strings.TrimSpace(raw[i].TargetType))
|
||||
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")
|
||||
}
|
||||
if targetID == "" {
|
||||
@@ -2327,7 +2385,7 @@ func validateSSHAccessProfileTargets(store *db.Store, targets []models.SSHAccess
|
||||
return nil
|
||||
}
|
||||
for i = 0; i < len(targets); i++ {
|
||||
if targets[i].TargetType == "user" {
|
||||
if targets[i].TargetType == db.SSHAccessProfileTargetTypeUser {
|
||||
_, err = store.GetUserByID(targets[i].TargetID)
|
||||
} else {
|
||||
_, err = store.GetUserGroup(targets[i].TargetID)
|
||||
|
||||
@@ -12,14 +12,12 @@ type Handler interface {
|
||||
}
|
||||
|
||||
type route struct {
|
||||
method string
|
||||
pattern string
|
||||
//handler HandlerFunc
|
||||
parts []string // pre-split pattern segments
|
||||
handler Handler
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -27,18 +25,21 @@ func (hf HandlerFunc) Handle(w http.ResponseWriter, req *http.Request, params Pa
|
||||
}
|
||||
|
||||
func NewRouter() *Router {
|
||||
return &Router{}
|
||||
return &Router{
|
||||
index: make(map[string]map[int][]route),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) AddHandler(method string, pattern string, handler Handler) {
|
||||
var parts []string
|
||||
var entry route
|
||||
|
||||
// TODO: can I split the pattern a tree structure for faster lookup later?
|
||||
// TODO: duplication check?
|
||||
entry.method = method
|
||||
entry.pattern = pattern
|
||||
entry.handler = handler
|
||||
r.routes = append(r.routes, entry)
|
||||
parts = split(pattern)
|
||||
entry = route{parts: parts, handler: handler}
|
||||
if r.index[method] == nil {
|
||||
r.index[method] = make(map[int][]route)
|
||||
}
|
||||
r.index[method][len(parts)] = append(r.index[method][len(parts)], entry)
|
||||
}
|
||||
|
||||
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) {
|
||||
var i int
|
||||
var pathParts []string
|
||||
var byDepth map[int][]route
|
||||
var routes []route
|
||||
var rt route
|
||||
var params Params
|
||||
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.Handle(w, req, params) // <-- handler is the Handler interface
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
func match(pattern, path string) (Params, bool) {
|
||||
var pParts []string
|
||||
var pathParts []string
|
||||
// match assumes pParts and pathParts are the same length (guaranteed by the index).
|
||||
func match(pParts, pathParts []string) (Params, bool) {
|
||||
var params Params
|
||||
var i int
|
||||
var p 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 params == nil { params = make(Params) }
|
||||
params[p[1:]] = seg
|
||||
continue
|
||||
}
|
||||
|
||||
if p != seg {
|
||||
return nil, false
|
||||
}
|
||||
if p != seg { return nil, false }
|
||||
}
|
||||
if params == nil { params = Params{} }
|
||||
return params, true
|
||||
}
|
||||
|
||||
func split(path string) []string {
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
return []string{}
|
||||
}
|
||||
if path == "" { return []string{} }
|
||||
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("PATCH", "/api/ssh/credentials/:id", api.UpdateSSHCredentialForSelf)
|
||||
router.Handle("DELETE", "/api/ssh/credentials/:id", api.DeleteSSHCredentialForSelf)
|
||||
|
||||
router.Handle("GET", "/api/ssh/access-profiles", api.ListSSHAccessProfilesForSelf)
|
||||
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/credential", api.GetSSHAccessProfileCredentialForSelf)
|
||||
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("DELETE", "/api/ssh/access-profiles/:id", api.DeleteSSHAccessProfileForSelf)
|
||||
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}`),
|
||||
getSSHAccessProfileCredentialForSelf: (id: string) => request<SSHCredential>(`/api/ssh/access-profiles/${id}/credential`),
|
||||
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) =>
|
||||
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' }),
|
||||
|
||||
@@ -7,5 +7,20 @@ const compactSx: SxProps<Theme> = { minWidth: 0, m: 0 }
|
||||
export default function CompactListItemText(props: ListItemTextProps) {
|
||||
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
|
||||
onClose: () => void
|
||||
showAdminFields?: boolean
|
||||
accessProfileID?: string | null
|
||||
}
|
||||
|
||||
function fmt(value: number): string {
|
||||
@@ -49,7 +50,12 @@ export default function SSHServerGroupDetailsDialog(props: SSHServerGroupDetails
|
||||
|
||||
setMembersLoading(true)
|
||||
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 }
|
||||
setMemberServers(Array.isArray(list) ? list : [])
|
||||
}).catch((err: unknown) => {
|
||||
@@ -62,7 +68,7 @@ export default function SSHServerGroupDetailsDialog(props: SSHServerGroupDetails
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [item])
|
||||
}, [item, props.accessProfileID])
|
||||
|
||||
return (
|
||||
<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 }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{key.name}({key.prefix})
|
||||
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||
</Typography>
|
||||
|
||||
@@ -292,7 +292,7 @@ export default function AdminPKIClientProfilesPage() {
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
</Typography>
|
||||
|
||||
@@ -455,7 +455,7 @@ export default function AdminSSHAccessProfilesPage() {
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.name}{' '}
|
||||
<Chip
|
||||
size="small"
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function AdminSSHCredentialsPage() {
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
</Typography>
|
||||
|
||||
@@ -490,7 +490,7 @@ export default function AdminSSHServersPage() {
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
</Typography>
|
||||
@@ -560,7 +560,7 @@ export default function AdminSSHServersPage() {
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||
</Typography>
|
||||
|
||||
@@ -302,7 +302,7 @@ export default function AdminSSHSessionsPage() {
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{sessionTitle(item)}{' '}
|
||||
<Chip
|
||||
label={item.status || 'unknown'}
|
||||
@@ -476,7 +476,7 @@ export default function AdminSSHSessionsPage() {
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{formatTransferOperation(item.operation)} · {item.server_name || item.server_id || '-'}
|
||||
{item.target_server_name ? ` → ${item.target_server_name}` : ''}{' '}
|
||||
<Chip
|
||||
|
||||
@@ -327,7 +327,7 @@ export default function AdminSSHUserCAPage() {
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
</Typography>
|
||||
|
||||
@@ -370,7 +370,7 @@ export default function AdminUserGroupsPage() {
|
||||
>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography noWrap>
|
||||
<Typography component="div">
|
||||
{group.name}
|
||||
{group.disabled? (<> <Chip size="small" label="Disabled" color="error" sx={{ mt: 0.25, flexShrink: 0 }} /></>): null}
|
||||
</Typography>
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function AdminUsersPage() {
|
||||
>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{u.display_name || u.username}({u.username})
|
||||
{u.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||
</Typography>
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function ApiKeysPage() {
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{key.name}({key.prefix})
|
||||
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||
</Typography>
|
||||
|
||||
@@ -425,7 +425,7 @@ export default function ClientCertificatesPage() {
|
||||
>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.common_name} ({item.cert_id})
|
||||
{' '}
|
||||
<Chip
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function SSHCredentialsPage() {
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
</Typography>
|
||||
|
||||
@@ -109,6 +109,7 @@ export default function SSHServersPage() {
|
||||
const [viewServerItem, setViewServerItem] = useState<SSHServer | null>(null)
|
||||
const [viewSharedServerItem, setViewSharedServerItem] = useState<SSHServer | null>(null)
|
||||
const [viewServerGroupItem, setViewServerGroupItem] = useState<SSHServerGroup | null>(null)
|
||||
const [accessProfileIdForServerGroup, setAccessProfileIdForServerGroup] = useState<string | null>(null)
|
||||
const [hostKeyServer, setHostKeyServer] = useState<SSHServer | null>(null)
|
||||
const [hostKeys, setHostKeys] = useState<SSHServerHostKey[]>([])
|
||||
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
|
||||
setViewServerGroupItem(item)
|
||||
setViewServerGroupItem(group)
|
||||
setAccessProfileIdForServerGroup(profile_id)
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
@@ -664,7 +666,7 @@ export default function SSHServersPage() {
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
<Typography component="div">
|
||||
{item.name}
|
||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||
</Typography>
|
||||
@@ -675,7 +677,7 @@ export default function SSHServersPage() {
|
||||
{item.server_target_type === 'group' ? (
|
||||
<Link
|
||||
underline="hover"
|
||||
onClick={() => void openServerGroupView(item.server_group)}
|
||||
onClick={() => void openServerGroupView(item.id, item.server_group)}
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
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' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
{item.name}
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||
</Typography>
|
||||
}
|
||||
@@ -764,9 +766,9 @@ export default function SSHServersPage() {
|
||||
{item.host}:{item.port}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
Tags: {(item.tags || []).join(', ') || '-'} · {item.id}
|
||||
Tags: {(item.tags || []).join(', ') || '-'}
|
||||
</Typography>
|
||||
{item.description ? (
|
||||
{item.description? (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
{item.description}
|
||||
</Typography>
|
||||
@@ -920,6 +922,7 @@ export default function SSHServersPage() {
|
||||
<SSHServerGroupDetailsDialog
|
||||
item={viewServerGroupItem}
|
||||
onClose={() => setViewServerGroupItem(null) }
|
||||
accessProfileID={accessProfileIdForServerGroup}
|
||||
/>
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
|
||||
Reference in New Issue
Block a user