Compare commits
8 Commits
fc4686720d
...
e35e38eb3b
| Author | SHA1 | Date | |
|---|---|---|---|
| e35e38eb3b | |||
| 7ca729a6b0 | |||
| 1a79a62068 | |||
| 37958d5b6a | |||
| 5fd651534e | |||
| 3361750f26 | |||
| d3809028e6 | |||
| 2dbf06fd5f |
@@ -73,6 +73,7 @@ func (h *gitPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
|
|||||||
var repoStorageID int64
|
var repoStorageID int64
|
||||||
var err error
|
var err error
|
||||||
var newPath string
|
var newPath string
|
||||||
|
|
||||||
currentStore = requestStore(r, h.store)
|
currentStore = requestStore(r, h.store)
|
||||||
path = strings.TrimPrefix(r.URL.Path, "/")
|
path = strings.TrimPrefix(r.URL.Path, "/")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
@@ -412,6 +413,8 @@ func main() {
|
|||||||
Uploads: uploadStore,
|
Uploads: uploadStore,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
SSHSessionRegistry: handlers.NewSSHSessionRegistry(),
|
SSHSessionRegistry: handlers.NewSSHSessionRegistry(),
|
||||||
|
SSHPromptedPasswordStore: handlers.NewSSHPromptedPasswordStore(),
|
||||||
|
SSHPreparedSessionStore: handlers.NewSSHPreparedSessionStore(),
|
||||||
}
|
}
|
||||||
rpmMirror.Start()
|
rpmMirror.Start()
|
||||||
|
|
||||||
@@ -423,6 +426,7 @@ func main() {
|
|||||||
|
|
||||||
var gitServer http.Handler
|
var gitServer http.Handler
|
||||||
var authFunc func(store *db.Store, username string, password string) (bool, error)
|
var authFunc func(store *db.Store, username string, password string) (bool, error)
|
||||||
|
|
||||||
authFunc = func(store *db.Store, username string, password string) (bool, error) {
|
authFunc = func(store *db.Store, username string, password string) (bool, error) {
|
||||||
var user models.User
|
var user models.User
|
||||||
var principal models.ServicePrincipal
|
var principal models.ServicePrincipal
|
||||||
@@ -601,6 +605,7 @@ func main() {
|
|||||||
router.Handle("GET", "/api/admin/ssh/access-profiles/:id", api.GetSSHAccessProfileAdmin)
|
router.Handle("GET", "/api/admin/ssh/access-profiles/:id", api.GetSSHAccessProfileAdmin)
|
||||||
router.Handle("PATCH", "/api/admin/ssh/access-profiles/:id", api.UpdateSSHAccessProfileAdmin)
|
router.Handle("PATCH", "/api/admin/ssh/access-profiles/:id", api.UpdateSSHAccessProfileAdmin)
|
||||||
router.Handle("DELETE", "/api/admin/ssh/access-profiles/:id", api.DeleteSSHAccessProfileAdmin)
|
router.Handle("DELETE", "/api/admin/ssh/access-profiles/:id", api.DeleteSSHAccessProfileAdmin)
|
||||||
|
router.Handle("GET", "/api/admin/ssh/sessions", api.ListSSHSessionsAdmin)
|
||||||
router.Handle("GET", "/api/ssh/user-cas", api.ListSSHUserCAsForSelf)
|
router.Handle("GET", "/api/ssh/user-cas", api.ListSSHUserCAsForSelf)
|
||||||
router.Handle("GET", "/api/ssh/user-cas/:id", api.GetSSHUserCAForSelf)
|
router.Handle("GET", "/api/ssh/user-cas/:id", api.GetSSHUserCAForSelf)
|
||||||
router.Handle("GET", "/api/ssh/user-cas/:id/public-key", api.DownloadSSHUserCAPublicKeyForSelf)
|
router.Handle("GET", "/api/ssh/user-cas/:id/public-key", api.DownloadSSHUserCAPublicKeyForSelf)
|
||||||
@@ -620,6 +625,7 @@ func main() {
|
|||||||
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)
|
||||||
router.Handle("GET", "/api/ssh/sessions", api.ListSSHSessionsForSelf)
|
router.Handle("GET", "/api/ssh/sessions", api.ListSSHSessionsForSelf)
|
||||||
|
router.Handle("GET", "/api/ssh/stream", api.StreamSSHWorkspaceForSelf)
|
||||||
router.Handle("GET", "/api/ssh/sessions/:id", api.GetSSHSessionForSelf)
|
router.Handle("GET", "/api/ssh/sessions/:id", api.GetSSHSessionForSelf)
|
||||||
router.Handle("POST", "/api/ssh/sessions/:id/disconnect", api.DisconnectSSHSessionForSelf)
|
router.Handle("POST", "/api/ssh/sessions/:id/disconnect", api.DisconnectSSHSessionForSelf)
|
||||||
router.Handle("GET", "/api/ssh/sessions/:id/stream", api.StreamSSHSessionForSelf)
|
router.Handle("GET", "/api/ssh/sessions/:id/stream", api.StreamSSHSessionForSelf)
|
||||||
@@ -1160,10 +1166,7 @@ func requestClientCertFingerprint(r *http.Request) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isClientCertAllowed(fp string, policy listenerAuthPolicy) bool {
|
func isClientCertAllowed(fp string, policy listenerAuthPolicy) bool {
|
||||||
if len(policy.CertAllowlist) == 0 {
|
if len(policy.CertAllowlist) == 0 { return true }
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return policy.CertAllowlist[fp]
|
return policy.CertAllowlist[fp]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1171,12 +1174,9 @@ func requestStore(r *http.Request, store *db.Store) *db.Store {
|
|||||||
var requestStore *db.Store
|
var requestStore *db.Store
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
if r == nil {
|
if r =! nil {
|
||||||
return store
|
|
||||||
}
|
|
||||||
requestStore, ok = middleware.StoreFromContext(r.Context())
|
requestStore, ok = middleware.StoreFromContext(r.Context())
|
||||||
if ok && requestStore != nil {
|
if ok && requestStore != nil { return requestStore }
|
||||||
return requestStore
|
|
||||||
}
|
}
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ func Open(driver, dsn string) (*Store, error) {
|
|||||||
goto oops
|
goto oops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//if drv == "sqlite" {
|
||||||
|
// db.SetMaxOpenConns(10)
|
||||||
|
// db.SetMaxIdleConns(1)
|
||||||
|
//}
|
||||||
|
|
||||||
err = db.Ping()
|
err = db.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
goto oops
|
goto oops
|
||||||
|
|||||||
+185
-125
@@ -3,6 +3,7 @@ package db
|
|||||||
import "database/sql"
|
import "database/sql"
|
||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
import "errors"
|
import "errors"
|
||||||
|
import "fmt"
|
||||||
import "sort"
|
import "sort"
|
||||||
import "strings"
|
import "strings"
|
||||||
import "time"
|
import "time"
|
||||||
@@ -144,18 +145,12 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
|
|
||||||
if strings.TrimSpace(item.ID) == "" {
|
if strings.TrimSpace(item.ID) == "" {
|
||||||
item.ID, err = util.NewID()
|
item.ID, err = util.NewID()
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if item.Port <= 0 {
|
|
||||||
item.Port = 22
|
|
||||||
}
|
}
|
||||||
|
if item.Port <= 0 { item.Port = 22 }
|
||||||
item.Tags = normalizeStringList(item.Tags)
|
item.Tags = normalizeStringList(item.Tags)
|
||||||
tagsJSON, err = encodeStringList(item.Tags)
|
tagsJSON, err = encodeStringList(item.Tags)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
now = time.Now().UTC().Unix()
|
now = time.Now().UTC().Unix()
|
||||||
item.CreatedAt = now
|
item.CreatedAt = now
|
||||||
item.UpdatedAt = now
|
item.UpdatedAt = now
|
||||||
@@ -187,26 +182,20 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
item.CreatedAt,
|
item.CreatedAt,
|
||||||
item.UpdatedAt,
|
item.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
return item, err
|
return item, err
|
||||||
}
|
|
||||||
return item, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateSSHServer(item models.SSHServer) (models.SSHServer, error) {
|
func (s *Store) UpdateSSHServer(item models.SSHServer) (models.SSHServer, error) {
|
||||||
var err error
|
var err error
|
||||||
var tagsJSON string
|
var tagsJSON string
|
||||||
|
|
||||||
if item.Port <= 0 {
|
if item.Port <= 0 { item.Port = 22 }
|
||||||
item.Port = 22
|
|
||||||
}
|
|
||||||
item.Tags = normalizeStringList(item.Tags)
|
item.Tags = normalizeStringList(item.Tags)
|
||||||
tagsJSON, err = encodeStringList(item.Tags)
|
tagsJSON, err = encodeStringList(item.Tags)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
item.UpdatedAt = time.Now().UTC().Unix()
|
item.UpdatedAt = time.Now().UTC().Unix()
|
||||||
|
|
||||||
|
// TODO: need to protect it with tx.
|
||||||
_, err = s.Exec(`UPDATE ssh_servers
|
_, err = s.Exec(`UPDATE ssh_servers
|
||||||
SET name = ?, host = ?, port = ?, description = ?, tags_json = ?, enabled = ?, updated_at = ?
|
SET name = ?, host = ?, port = ?, description = ?, tags_json = ?, enabled = ?, updated_at = ?
|
||||||
WHERE public_id = ?`,
|
WHERE public_id = ?`,
|
||||||
@@ -219,19 +208,13 @@ func (s *Store) UpdateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
|||||||
item.UpdatedAt,
|
item.UpdatedAt,
|
||||||
strings.TrimSpace(item.ID),
|
strings.TrimSpace(item.ID),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
return s.GetSSHServer(item.ID)
|
||||||
item, err = s.GetSSHServer(item.ID)
|
|
||||||
if err != nil {
|
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
return item, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) DeleteSSHServer(id string) error {
|
func (s *Store) DeleteSSHServer(id string) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
_, err = s.Exec(`DELETE FROM ssh_servers WHERE public_id = ?`, strings.TrimSpace(id))
|
_, err = s.Exec(`DELETE FROM ssh_servers WHERE public_id = ?`, strings.TrimSpace(id))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -285,9 +268,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
|||||||
FROM ssh_access_profiles p
|
FROM ssh_access_profiles p
|
||||||
JOIN ssh_servers s ON s.public_id = p.server_public_id
|
JOIN ssh_servers s ON s.public_id = p.server_public_id
|
||||||
ORDER BY s.name, p.name`)
|
ORDER BY s.name, p.name`)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
@@ -329,31 +310,24 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
|||||||
&item.Server.CreatedAt,
|
&item.Server.CreatedAt,
|
||||||
&item.Server.UpdatedAt,
|
&item.Server.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.SSHPrincipals, err = decodeStringList(principalsJSON)
|
item.SSHPrincipals, err = decodeStringList(principalsJSON)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON)
|
item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Server.Tags, err = decodeStringList(serverTagsJSON)
|
item.Server.Tags, err = decodeStringList(serverTagsJSON)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
|
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,38 +464,32 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
|||||||
}
|
}
|
||||||
if strings.TrimSpace(item.ID) == "" {
|
if strings.TrimSpace(item.ID) == "" {
|
||||||
item.ID, err = util.NewID()
|
item.ID, err = util.NewID()
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if item.DefaultValidSeconds <= 0 {
|
|
||||||
item.DefaultValidSeconds = 3600
|
|
||||||
}
|
|
||||||
if item.MaxValidSeconds <= 0 {
|
|
||||||
item.MaxValidSeconds = item.DefaultValidSeconds
|
|
||||||
}
|
}
|
||||||
|
if item.DefaultValidSeconds <= 0 { item.DefaultValidSeconds = 3600 }
|
||||||
|
if item.MaxValidSeconds <= 0 { item.MaxValidSeconds = item.DefaultValidSeconds }
|
||||||
|
|
||||||
item.SSHPrincipals = normalizeStringList(item.SSHPrincipals)
|
item.SSHPrincipals = normalizeStringList(item.SSHPrincipals)
|
||||||
item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs)
|
item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs)
|
||||||
principalsJSON, err = encodeStringList(item.SSHPrincipals)
|
principalsJSON, err = encodeStringList(item.SSHPrincipals)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs)
|
grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
now = time.Now().UTC().Unix()
|
now = time.Now().UTC().Unix()
|
||||||
item.CreatedAt = now
|
item.CreatedAt = now
|
||||||
item.UpdatedAt = now
|
item.UpdatedAt = now
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
item.SecretPayload = strings.TrimSpace(item.SecretPayload)
|
||||||
if strings.TrimSpace(item.SecretPayload) != "" {
|
item.SecretPassword = strings.TrimSpace(item.SecretPassword)
|
||||||
|
if item.SecretPayload != "" || item.SecretPassword != "" {
|
||||||
secret = models.SSHSecret{
|
secret = models.SSHSecret{
|
||||||
ID: item.SecretID,
|
ID: item.SecretID,
|
||||||
Kind: "private_key",
|
Kind: "private_key",
|
||||||
Payload: item.SecretPayload,
|
Payload: item.SecretPayload,
|
||||||
|
Password: item.SecretPassword,
|
||||||
CreatedByKind: item.CreatedByKind,
|
CreatedByKind: item.CreatedByKind,
|
||||||
CreatedBySubjectID: item.CreatedBySubjectID,
|
CreatedBySubjectID: item.CreatedBySubjectID,
|
||||||
CreatedBySubjectName: item.CreatedBySubjectName,
|
CreatedBySubjectName: item.CreatedBySubjectName,
|
||||||
@@ -629,32 +597,30 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
|||||||
if strings.TrimSpace(item.AuthMethod) == "" {
|
if strings.TrimSpace(item.AuthMethod) == "" {
|
||||||
return item, errors.New("auth_method is required")
|
return item, errors.New("auth_method is required")
|
||||||
}
|
}
|
||||||
if item.DefaultValidSeconds <= 0 {
|
|
||||||
item.DefaultValidSeconds = 3600
|
if item.DefaultValidSeconds <= 0 { item.DefaultValidSeconds = 3600 }
|
||||||
}
|
if item.MaxValidSeconds <= 0 { item.MaxValidSeconds = item.DefaultValidSeconds }
|
||||||
if item.MaxValidSeconds <= 0 {
|
|
||||||
item.MaxValidSeconds = item.DefaultValidSeconds
|
|
||||||
}
|
|
||||||
item.SSHPrincipals = normalizeStringList(item.SSHPrincipals)
|
item.SSHPrincipals = normalizeStringList(item.SSHPrincipals)
|
||||||
item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs)
|
item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs)
|
||||||
principalsJSON, err = encodeStringList(item.SSHPrincipals)
|
principalsJSON, err = encodeStringList(item.SSHPrincipals)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs)
|
grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
item.UpdatedAt = time.Now().UTC().Unix()
|
item.UpdatedAt = time.Now().UTC().Unix()
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
item.SecretPayload = strings.TrimSpace(item.SecretPayload)
|
||||||
if strings.TrimSpace(item.SecretPayload) != "" {
|
item.SecretPassword = strings.TrimSpace(item.SecretPassword)
|
||||||
|
if item.SecretPayload != "" || item.SecretPassword != "" {
|
||||||
secret = models.SSHSecret{
|
secret = models.SSHSecret{
|
||||||
ID: item.SecretID,
|
ID: item.SecretID,
|
||||||
Kind: "private_key",
|
Kind: "private_key",
|
||||||
Payload: item.SecretPayload,
|
Payload: item.SecretPayload,
|
||||||
|
Password: item.SecretPassword,
|
||||||
CreatedByKind: item.CreatedByKind,
|
CreatedByKind: item.CreatedByKind,
|
||||||
CreatedBySubjectID: item.CreatedBySubjectID,
|
CreatedBySubjectID: item.CreatedBySubjectID,
|
||||||
CreatedBySubjectName: item.CreatedBySubjectName,
|
CreatedBySubjectName: item.CreatedBySubjectName,
|
||||||
@@ -875,32 +841,26 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
|||||||
&item.Server.CreatedAt,
|
&item.Server.CreatedAt,
|
||||||
&item.Server.UpdatedAt,
|
&item.Server.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.SSHPrincipals, err = decodeStringList(principalsJSON)
|
item.SSHPrincipals, err = decodeStringList(principalsJSON)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON)
|
item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Server.Tags, err = decodeStringList(serverTagsJSON)
|
item.Server.Tags, err = decodeStringList(serverTagsJSON)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
|
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Editable = item.OwnerScope == "user" && item.OwnerUserID == trimmedUserID
|
item.Editable = item.OwnerScope == "user" && item.OwnerUserID == trimmedUserID
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,10 +931,8 @@ func (s *Store) GetSSHSecret(id string) (models.SSHSecret, error) {
|
|||||||
var item models.SSHSecret
|
var item models.SSHSecret
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
row = s.QueryRow(`SELECT public_id, kind, payload, metadata_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
|
row = s.QueryRow(`SELECT public_id, kind, payload, password, metadata_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_secrets WHERE public_id = ?`, strings.TrimSpace(id))
|
||||||
FROM ssh_secrets
|
err = row.Scan(&item.ID, &item.Kind, &item.Payload, &item.Password, &item.MetadataJSON, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||||
WHERE public_id = ?`, strings.TrimSpace(id))
|
|
||||||
err = row.Scan(&item.ID, &item.Kind, &item.Payload, &item.MetadataJSON, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return item, err
|
return item, err
|
||||||
}
|
}
|
||||||
@@ -1132,38 +1090,137 @@ func (s *Store) GetSSHSession(id string) (models.SSHSession, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListSSHSessionsForUser(userID string, limit int) ([]models.SSHSession, error) {
|
func (s *Store) ListSSHSessionsForUser(userID string, limit int) ([]models.SSHSession, error) {
|
||||||
|
var items []models.SSHSession
|
||||||
|
var hasMore bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
items, hasMore, err = s.ListSSHSessionsForUserFiltered(userID, limit, 0, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = hasMore
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListSSHSessionsForUserFiltered(userID string, limit int, offset int, query string, status string) ([]models.SSHSession, bool, error) {
|
||||||
var rows *sql.Rows
|
var rows *sql.Rows
|
||||||
var items []models.SSHSession
|
var items []models.SSHSession
|
||||||
var item models.SSHSession
|
var item models.SSHSession
|
||||||
|
var whereParts []string
|
||||||
|
var args []any
|
||||||
|
var sqlQuery string
|
||||||
|
var like string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 50
|
limit = 20
|
||||||
}
|
}
|
||||||
if limit > 500 {
|
if limit > 200 {
|
||||||
limit = 500
|
limit = 200
|
||||||
}
|
}
|
||||||
rows, err = s.Query(`SELECT public_id, profile_public_id, server_public_id, user_public_id, username, remote_username, auth_method, host, port, status, host_key_fingerprint, requested_term, requested_cols, requested_rows, started_at, connected_at, ended_at, remote_addr, user_agent, error
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
status = strings.ToLower(strings.TrimSpace(status))
|
||||||
|
whereParts = append(whereParts, "user_public_id = ?")
|
||||||
|
args = append(args, strings.TrimSpace(userID))
|
||||||
|
if query != "" {
|
||||||
|
like = "%" + query + "%"
|
||||||
|
whereParts = append(whereParts, "(public_id LIKE ? OR profile_public_id LIKE ? OR server_public_id LIKE ? OR user_public_id LIKE ? OR username LIKE ? OR remote_username LIKE ? OR host LIKE ? OR auth_method LIKE ? OR status LIKE ? OR host_key_fingerprint LIKE ? OR error LIKE ?)")
|
||||||
|
args = append(args, like, like, like, like, like, like, like, like, like, like, like)
|
||||||
|
}
|
||||||
|
if status != "" {
|
||||||
|
whereParts = append(whereParts, "status = ?")
|
||||||
|
args = append(args, status)
|
||||||
|
}
|
||||||
|
sqlQuery = fmt.Sprintf(`SELECT public_id, profile_public_id, server_public_id, user_public_id, username, remote_username, auth_method, host, port, status, host_key_fingerprint, requested_term, requested_cols, requested_rows, started_at, connected_at, ended_at, remote_addr, user_agent, error
|
||||||
FROM ssh_sessions
|
FROM ssh_sessions
|
||||||
WHERE user_public_id = ?
|
WHERE %s
|
||||||
ORDER BY started_at DESC
|
ORDER BY started_at DESC
|
||||||
LIMIT ?`, strings.TrimSpace(userID), limit)
|
LIMIT ? OFFSET ?`, strings.Join(whereParts, " AND "))
|
||||||
|
args = append(args, limit + 1, offset)
|
||||||
|
rows, err = s.Query(sqlQuery, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.Scan(&item.ID, &item.ProfileID, &item.ServerID, &item.UserID, &item.Username, &item.RemoteUsername, &item.AuthMethod, &item.Host, &item.Port, &item.Status, &item.HostKeyFingerprint, &item.RequestedTerm, &item.RequestedCols, &item.RequestedRows, &item.StartedAt, &item.ConnectedAt, &item.EndedAt, &item.RemoteAddr, &item.UserAgent, &item.Error)
|
err = rows.Scan(&item.ID, &item.ProfileID, &item.ServerID, &item.UserID, &item.Username, &item.RemoteUsername, &item.AuthMethod, &item.Host, &item.Port, &item.Status, &item.HostKeyFingerprint, &item.RequestedTerm, &item.RequestedCols, &item.RequestedRows, &item.StartedAt, &item.ConnectedAt, &item.EndedAt, &item.RemoteAddr, &item.UserAgent, &item.Error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
return items, nil
|
if len(items) > limit {
|
||||||
|
items = items[:limit]
|
||||||
|
return items, true, nil
|
||||||
|
}
|
||||||
|
return items, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListSSHSessionsFiltered(limit int, offset int, query string, status string) ([]models.SSHSession, bool, error) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
var items []models.SSHSession
|
||||||
|
var item models.SSHSession
|
||||||
|
var whereParts []string
|
||||||
|
var args []any
|
||||||
|
var sqlQuery string
|
||||||
|
var like string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 200 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
status = strings.ToLower(strings.TrimSpace(status))
|
||||||
|
if query != "" {
|
||||||
|
like = "%" + query + "%"
|
||||||
|
whereParts = append(whereParts, "(public_id LIKE ? OR profile_public_id LIKE ? OR server_public_id LIKE ? OR user_public_id LIKE ? OR username LIKE ? OR remote_username LIKE ? OR host LIKE ? OR auth_method LIKE ? OR status LIKE ? OR host_key_fingerprint LIKE ? OR error LIKE ?)")
|
||||||
|
args = append(args, like, like, like, like, like, like, like, like, like, like, like)
|
||||||
|
}
|
||||||
|
if status != "" {
|
||||||
|
whereParts = append(whereParts, "status = ?")
|
||||||
|
args = append(args, status)
|
||||||
|
}
|
||||||
|
sqlQuery = `SELECT public_id, profile_public_id, server_public_id, user_public_id, username, remote_username, auth_method, host, port, status, host_key_fingerprint, requested_term, requested_cols, requested_rows, started_at, connected_at, ended_at, remote_addr, user_agent, error
|
||||||
|
FROM ssh_sessions`
|
||||||
|
if len(whereParts) > 0 {
|
||||||
|
sqlQuery += "\n\t\tWHERE " + strings.Join(whereParts, " AND ")
|
||||||
|
}
|
||||||
|
sqlQuery += "\n\t\tORDER BY started_at DESC\n\t\tLIMIT ? OFFSET ?"
|
||||||
|
args = append(args, limit + 1, offset)
|
||||||
|
rows, err = s.Query(sqlQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
err = rows.Scan(&item.ID, &item.ProfileID, &item.ServerID, &item.UserID, &item.Username, &item.RemoteUsername, &item.AuthMethod, &item.Host, &item.Port, &item.Status, &item.HostKeyFingerprint, &item.RequestedTerm, &item.RequestedCols, &item.RequestedRows, &item.StartedAt, &item.ConnectedAt, &item.EndedAt, &item.RemoteAddr, &item.UserAgent, &item.Error)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if len(items) > limit {
|
||||||
|
items = items[:limit]
|
||||||
|
return items, true, nil
|
||||||
|
}
|
||||||
|
return items, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateSSHSessionStatus(id string, status string, hostKeyFingerprint string, connectedAt int64, endedAt int64, errorText string) error {
|
func (s *Store) UpdateSSHSessionStatus(id string, status string, hostKeyFingerprint string, connectedAt int64, endedAt int64, errorText string) error {
|
||||||
@@ -1199,16 +1256,18 @@ func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, err
|
|||||||
public_id,
|
public_id,
|
||||||
kind,
|
kind,
|
||||||
payload,
|
payload,
|
||||||
|
password,
|
||||||
metadata_json,
|
metadata_json,
|
||||||
created_by_kind,
|
created_by_kind,
|
||||||
created_by_subject_id,
|
created_by_subject_id,
|
||||||
created_by_subject_name,
|
created_by_subject_name,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
item.ID,
|
item.ID,
|
||||||
strings.TrimSpace(item.Kind),
|
strings.TrimSpace(item.Kind),
|
||||||
item.Payload,
|
item.Payload,
|
||||||
|
item.Password,
|
||||||
normalizeJSONText(item.MetadataJSON, "{}"),
|
normalizeJSONText(item.MetadataJSON, "{}"),
|
||||||
strings.TrimSpace(item.CreatedByKind),
|
strings.TrimSpace(item.CreatedByKind),
|
||||||
strings.TrimSpace(item.CreatedBySubjectID),
|
strings.TrimSpace(item.CreatedBySubjectID),
|
||||||
@@ -1216,29 +1275,30 @@ func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, err
|
|||||||
item.CreatedAt,
|
item.CreatedAt,
|
||||||
item.UpdatedAt,
|
item.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
return item, err
|
return item, err
|
||||||
}
|
|
||||||
return item, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, error) {
|
func updateSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
item.UpdatedAt = time.Now().UTC().Unix()
|
item.UpdatedAt = time.Now().UTC().Unix()
|
||||||
_, err = tx.Exec(`UPDATE ssh_secrets
|
_, err = tx.Exec(`UPDATE ssh_secrets SET kind = ?, payload = ?, metadata_json = ?, updated_at = ? WHERE public_id = ?`,
|
||||||
SET kind = ?, payload = ?, metadata_json = ?, updated_at = ?
|
|
||||||
WHERE public_id = ?`,
|
|
||||||
strings.TrimSpace(item.Kind),
|
strings.TrimSpace(item.Kind),
|
||||||
item.Payload,
|
item.Payload,
|
||||||
normalizeJSONText(item.MetadataJSON, "{}"),
|
normalizeJSONText(item.MetadataJSON, "{}"),
|
||||||
item.UpdatedAt,
|
item.UpdatedAt,
|
||||||
strings.TrimSpace(item.ID),
|
strings.TrimSpace(item.ID),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err == nil && item.Password != "" {
|
||||||
return item, err
|
// TODO: how can i update the password without security breach?
|
||||||
|
// currently, i perform the password update only if the given password is not blank
|
||||||
|
_, err = tx.Exec(`UPDATE ssh_secrets SET password = ?, updated_at = ? WHERE public_id = ?`,
|
||||||
|
item.Password,
|
||||||
|
item.UpdatedAt,
|
||||||
|
strings.TrimSpace(item.ID),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return item, nil
|
return item, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertSSHAccessProfileTargetTx(tx *sql.Tx, profileID string, targetType string, targetID string, createdAt int64) error {
|
func insertSSHAccessProfileTargetTx(tx *sql.Tx, profileID string, targetType string, targetID string, createdAt int64) error {
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ type API struct {
|
|||||||
Uploads storage.FileStore
|
Uploads storage.FileStore
|
||||||
Logger *util.Logger
|
Logger *util.Logger
|
||||||
SSHSessionRegistry *SSHSessionRegistry
|
SSHSessionRegistry *SSHSessionRegistry
|
||||||
|
SSHPromptedPasswordStore *SSHPromptedPasswordStore
|
||||||
|
SSHPreparedSessionStore *SSHPreparedSessionStore
|
||||||
OnTLSListenersChanged func()
|
OnTLSListenersChanged func()
|
||||||
OnTLSListenerRuntimeStatus func() map[string]int
|
OnTLSListenerRuntimeStatus func() map[string]int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1039,9 +1039,8 @@ func (api *API) ServePKICRL(w http.ResponseWriter, r *http.Request) {
|
|||||||
var err error
|
var err error
|
||||||
path = strings.TrimPrefix(r.URL.Path, "/pki/crl/")
|
path = strings.TrimPrefix(r.URL.Path, "/pki/crl/")
|
||||||
base = path
|
base = path
|
||||||
if strings.HasSuffix(base, ".pem") {
|
|
||||||
base = strings.TrimSuffix(base, ".pem")
|
base = strings.TrimSuffix(base, ".pem")
|
||||||
}
|
|
||||||
caID = strings.TrimSpace(base)
|
caID = strings.TrimSpace(base)
|
||||||
if caID == "" {
|
if caID == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
import "io"
|
import "io"
|
||||||
|
import "strings"
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
import "golang.org/x/crypto/ssh"
|
import "golang.org/x/crypto/ssh"
|
||||||
@@ -22,6 +23,16 @@ type SSHSessionRegistry struct {
|
|||||||
items map[string]*sshActiveSession
|
items map[string]*sshActiveSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SSHPromptedPasswordStore struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
items map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SSHPreparedSessionStore struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
items map[string]sshSessionPrepared
|
||||||
|
}
|
||||||
|
|
||||||
func NewSSHSessionRegistry() *SSHSessionRegistry {
|
func NewSSHSessionRegistry() *SSHSessionRegistry {
|
||||||
var registry *SSHSessionRegistry
|
var registry *SSHSessionRegistry
|
||||||
|
|
||||||
@@ -31,6 +42,129 @@ func NewSSHSessionRegistry() *SSHSessionRegistry {
|
|||||||
return registry
|
return registry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewSSHPromptedPasswordStore() *SSHPromptedPasswordStore {
|
||||||
|
var store *SSHPromptedPasswordStore
|
||||||
|
|
||||||
|
store = &SSHPromptedPasswordStore{
|
||||||
|
items: map[string]string{},
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSSHPreparedSessionStore() *SSHPreparedSessionStore {
|
||||||
|
var store *SSHPreparedSessionStore
|
||||||
|
|
||||||
|
store = &SSHPreparedSessionStore{
|
||||||
|
items: map[string]sshSessionPrepared{},
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHPromptedPasswordStore) Put(id string, password string) {
|
||||||
|
var trimmedID string
|
||||||
|
|
||||||
|
if s == nil { return }
|
||||||
|
trimmedID = strings.TrimSpace(id)
|
||||||
|
if trimmedID == "" { return }
|
||||||
|
s.mu.Lock()
|
||||||
|
s.items[trimmedID] = password
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHPromptedPasswordStore) Take(id string) string {
|
||||||
|
var trimmedID string
|
||||||
|
var password string
|
||||||
|
|
||||||
|
if s == nil { return "" }
|
||||||
|
trimmedID = strings.TrimSpace(id)
|
||||||
|
if trimmedID == "" { return "" }
|
||||||
|
s.mu.Lock()
|
||||||
|
password = s.items[trimmedID]
|
||||||
|
delete(s.items, trimmedID)
|
||||||
|
s.mu.Unlock()
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHPromptedPasswordStore) Delete(id string) {
|
||||||
|
var trimmedID string
|
||||||
|
|
||||||
|
if s == nil { return}
|
||||||
|
trimmedID = strings.TrimSpace(id)
|
||||||
|
if trimmedID == "" { return }
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.items, trimmedID)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHPreparedSessionStore) Put(id string, item sshSessionPrepared) {
|
||||||
|
var trimmedID string
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trimmedID = strings.TrimSpace(id)
|
||||||
|
if trimmedID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
s.items[trimmedID] = item
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHPreparedSessionStore) Get(id string) (sshSessionPrepared, bool) {
|
||||||
|
var trimmedID string
|
||||||
|
var item sshSessionPrepared
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return item, false
|
||||||
|
}
|
||||||
|
trimmedID = strings.TrimSpace(id)
|
||||||
|
if trimmedID == "" {
|
||||||
|
return item, false
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
item, ok = s.items[trimmedID]
|
||||||
|
s.mu.Unlock()
|
||||||
|
return item, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHPreparedSessionStore) Take(id string) (sshSessionPrepared, bool) {
|
||||||
|
var trimmedID string
|
||||||
|
var item sshSessionPrepared
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return item, false
|
||||||
|
}
|
||||||
|
trimmedID = strings.TrimSpace(id)
|
||||||
|
if trimmedID == "" {
|
||||||
|
return item, false
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
item, ok = s.items[trimmedID]
|
||||||
|
if ok {
|
||||||
|
delete(s.items, trimmedID)
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
return item, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHPreparedSessionStore) Delete(id string) {
|
||||||
|
var trimmedID string
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trimmedID = strings.TrimSpace(id)
|
||||||
|
if trimmedID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.items, trimmedID)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *SSHSessionRegistry) Register(id string, item *sshActiveSession) error {
|
func (r *SSHSessionRegistry) Register(id string, item *sshActiveSession) error {
|
||||||
if r == nil || item == nil {
|
if r == nil || item == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -201,13 +201,9 @@ func RegisterAfterCommit(ctx context.Context, fn func()) {
|
|||||||
var holder *afterCommitHolder
|
var holder *afterCommitHolder
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
if ctx == nil || fn == nil {
|
if ctx == nil || fn == nil { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
holder, ok = ctx.Value(afterCommitKey).(*afterCommitHolder)
|
holder, ok = ctx.Value(afterCommitKey).(*afterCommitHolder)
|
||||||
if !ok || holder == nil {
|
if !ok || holder == nil { return}
|
||||||
return
|
|
||||||
}
|
|
||||||
holder.Fns = append(holder.Fns, fn)
|
holder.Fns = append(holder.Fns, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,19 +213,14 @@ func runAfterCommit(ctx context.Context) {
|
|||||||
var fns []func()
|
var fns []func()
|
||||||
var i int
|
var i int
|
||||||
|
|
||||||
if ctx == nil {
|
if ctx == nil { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
holder, ok = ctx.Value(afterCommitKey).(*afterCommitHolder)
|
holder, ok = ctx.Value(afterCommitKey).(*afterCommitHolder)
|
||||||
if !ok || holder == nil || len(holder.Fns) == 0 {
|
if !ok || holder == nil || len(holder.Fns) == 0 { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
fns = append(fns, holder.Fns...)
|
fns = append(fns, holder.Fns...)
|
||||||
holder.Fns = nil
|
holder.Fns = nil
|
||||||
for i = 0; i < len(fns); i++ {
|
for i = 0; i < len(fns); i++ {
|
||||||
if fns[i] != nil {
|
if fns[i] != nil { fns[i]()}
|
||||||
fns[i]()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -539,6 +539,7 @@ type SSHSecret struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
Payload string `json:"-"`
|
Payload string `json:"-"`
|
||||||
|
Password string `json:"-"`
|
||||||
MetadataJSON string `json:"-"`
|
MetadataJSON string `json:"-"`
|
||||||
CreatedByKind string `json:"created_by_kind"`
|
CreatedByKind string `json:"created_by_kind"`
|
||||||
CreatedBySubjectID string `json:"created_by_subject_id"`
|
CreatedBySubjectID string `json:"created_by_subject_id"`
|
||||||
@@ -561,6 +562,7 @@ type SSHAccessProfile struct {
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
SecretID string `json:"-"`
|
SecretID string `json:"-"`
|
||||||
SecretPayload string `json:"-"`
|
SecretPayload string `json:"-"`
|
||||||
|
SecretPassword string `json:"="`
|
||||||
AuthPublicKey string `json:"auth_public_key"`
|
AuthPublicKey string `json:"auth_public_key"`
|
||||||
AuthPublicKeyFingerprint string `json:"auth_public_key_fingerprint"`
|
AuthPublicKeyFingerprint string `json:"auth_public_key_fingerprint"`
|
||||||
SSHUserCAID string `json:"ssh_user_ca_id"`
|
SSHUserCAID string `json:"ssh_user_ca_id"`
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS ssh_secrets (
|
|||||||
public_id TEXT NOT NULL UNIQUE,
|
public_id TEXT NOT NULL UNIQUE,
|
||||||
kind TEXT NOT NULL,
|
kind TEXT NOT NULL,
|
||||||
payload TEXT NOT NULL,
|
payload TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
metadata_json TEXT NOT NULL DEFAULT '{}',
|
metadata_json TEXT NOT NULL DEFAULT '{}',
|
||||||
created_by_kind TEXT NOT NULL DEFAULT 'user',
|
created_by_kind TEXT NOT NULL DEFAULT 'user',
|
||||||
created_by_subject_id TEXT NOT NULL DEFAULT '',
|
created_by_subject_id TEXT NOT NULL DEFAULT '',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
|
|||||||
+33
-9
@@ -686,7 +686,7 @@ export interface SSHAccessProfile {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
remote_username: string
|
remote_username: string
|
||||||
auth_method: 'managed_ssh_cert' | 'stored_private_key'
|
auth_method: 'managed_ssh_cert' | 'prompted_password' | 'stored_password' | 'stored_private_key'
|
||||||
owner_scope: 'admin_shared' | 'user'
|
owner_scope: 'admin_shared' | 'user'
|
||||||
owner_user_id: string
|
owner_user_id: string
|
||||||
allow_user_edit: boolean
|
allow_user_edit: boolean
|
||||||
@@ -715,7 +715,7 @@ export interface SSHSession {
|
|||||||
user_id: string
|
user_id: string
|
||||||
username: string
|
username: string
|
||||||
remote_username: string
|
remote_username: string
|
||||||
auth_method: 'managed_ssh_cert' | 'stored_private_key'
|
auth_method: 'managed_ssh_cert' | 'prompted_password' | 'stored_password' | 'stored_private_key'
|
||||||
host: string
|
host: string
|
||||||
port: number
|
port: number
|
||||||
status: string
|
status: string
|
||||||
@@ -731,6 +731,13 @@ export interface SSHSession {
|
|||||||
error: string
|
error: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SSHSessionListResponse {
|
||||||
|
items: SSHSession[]
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
has_more: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface SSHSessionConnectResponse {
|
export interface SSHSessionConnectResponse {
|
||||||
session_id: string
|
session_id: string
|
||||||
status: string
|
status: string
|
||||||
@@ -1154,15 +1161,16 @@ export const api = {
|
|||||||
request<SSHServerHostKey>(`/api/ssh/servers/${id}/host-keys`, { method: 'POST', body: JSON.stringify({ public_key }) }),
|
request<SSHServerHostKey>(`/api/ssh/servers/${id}/host-keys`, { method: 'POST', body: JSON.stringify({ public_key }) }),
|
||||||
deleteSSHServerHostKeyForSelf: (id: string, hostKeyID: string) =>
|
deleteSSHServerHostKeyForSelf: (id: string, hostKeyID: string) =>
|
||||||
request<void>(`/api/ssh/servers/${id}/host-keys/${hostKeyID}`, { method: 'DELETE' }),
|
request<void>(`/api/ssh/servers/${id}/host-keys/${hostKeyID}`, { method: 'DELETE' }),
|
||||||
listSSHAccessProfilesForSelf: () => request<SSHAccessProfile[]>('/api/ssh/access-profiles'),
|
listSSHAccessProfilesForSelf: (options?: RequestInit) => request<SSHAccessProfile[]>('/api/ssh/access-profiles', options),
|
||||||
createSSHAccessProfileForSelf: (payload: {
|
createSSHAccessProfileForSelf: (payload: {
|
||||||
server_id: string
|
server_id: string
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
remote_username: string
|
remote_username: string
|
||||||
auth_method: 'managed_ssh_cert' | 'stored_private_key'
|
auth_method: 'managed_ssh_cert' | 'prompted_password' | 'stored_password' | 'stored_private_key'
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
private_key_pem?: string
|
private_key_pem?: string
|
||||||
|
passowrd_text?: string
|
||||||
ssh_user_ca_id?: string
|
ssh_user_ca_id?: string
|
||||||
ssh_principal_mode?: 'explicit' | 'grant'
|
ssh_principal_mode?: 'explicit' | 'grant'
|
||||||
ssh_principals?: string[]
|
ssh_principals?: string[]
|
||||||
@@ -1171,15 +1179,16 @@ export const api = {
|
|||||||
max_valid_seconds: number
|
max_valid_seconds: number
|
||||||
}) =>
|
}) =>
|
||||||
request<SSHAccessProfile>('/api/ssh/access-profiles', { method: 'POST', body: JSON.stringify(payload) }),
|
request<SSHAccessProfile>('/api/ssh/access-profiles', { method: 'POST', body: JSON.stringify(payload) }),
|
||||||
getSSHAccessProfileForSelf: (id: string) => request<SSHAccessProfile>(`/api/ssh/access-profiles/${id}`),
|
getSSHAccessProfileForSelf: (id: string, options?: RequestInit) => request<SSHAccessProfile>(`/api/ssh/access-profiles/${id}`),
|
||||||
updateSSHAccessProfileForSelf: (id: string, payload: {
|
updateSSHAccessProfileForSelf: (id: string, payload: {
|
||||||
server_id: string
|
server_id: string
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
remote_username: string
|
remote_username: string
|
||||||
auth_method: 'managed_ssh_cert' | 'stored_private_key'
|
auth_method: 'managed_ssh_cert' | 'prompted_password' | 'stored_password' | 'stored_private_key'
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
private_key_pem?: string
|
private_key_pem?: string
|
||||||
|
password_text?: string
|
||||||
ssh_user_ca_id?: string
|
ssh_user_ca_id?: string
|
||||||
ssh_principal_mode?: 'explicit' | 'grant'
|
ssh_principal_mode?: 'explicit' | 'grant'
|
||||||
ssh_principals?: string[]
|
ssh_principals?: string[]
|
||||||
@@ -1189,10 +1198,25 @@ export const api = {
|
|||||||
}) =>
|
}) =>
|
||||||
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' }),
|
||||||
connectSSHAccessProfileForSelf: (id: string, payload?: { cols?: number; rows?: number; term?: string }) =>
|
connectSSHAccessProfileForSelf: (id: string, payload?: { cols?: number; rows?: number; term?: string; password?: string }) =>
|
||||||
request<SSHSessionConnectResponse>(`/api/ssh/access-profiles/${id}/connect`, { method: 'POST', body: JSON.stringify(payload || {}) }),
|
request<SSHSessionConnectResponse>(`/api/ssh/access-profiles/${id}/connect`, { method: 'POST', body: JSON.stringify(payload || {}) }),
|
||||||
listSSHSessionsForSelf: () => request<SSHSession[]>('/api/ssh/sessions'),
|
listSSHSessionsForSelf: (params?: { limit?: number; offset?: number; q?: string; status?: string }, options?: RequestInit) => {
|
||||||
getSSHSessionForSelf: (id: string) => request<SSHSession>(`/api/ssh/sessions/${id}`),
|
const search = new URLSearchParams()
|
||||||
|
if (params?.limit && params.limit > 0) search.set('limit', String(params.limit))
|
||||||
|
if (params?.offset && params.offset > 0) search.set('offset', String(params.offset))
|
||||||
|
if (params?.q) search.set('q', params.q)
|
||||||
|
if (params?.status) search.set('status', params.status)
|
||||||
|
return request<SSHSessionListResponse>(`/api/ssh/sessions${search.toString() ? `?${search.toString()}` : ''}`, options)
|
||||||
|
},
|
||||||
|
listSSHSessionsAdmin: (params?: { limit?: number; offset?: number; q?: string; status?: string }, options?: RequestInit) => {
|
||||||
|
const search = new URLSearchParams()
|
||||||
|
if (params?.limit && params.limit > 0) search.set('limit', String(params.limit))
|
||||||
|
if (params?.offset && params.offset > 0) search.set('offset', String(params.offset))
|
||||||
|
if (params?.q) search.set('q', params.q)
|
||||||
|
if (params?.status) search.set('status', params.status)
|
||||||
|
return request<SSHSessionListResponse>(`/api/admin/ssh/sessions${search.toString() ? `?${search.toString()}` : ''}`, options)
|
||||||
|
},
|
||||||
|
getSSHSessionForSelf: (id: string, options?: RequestInit) => request<SSHSession>(`/api/ssh/sessions/${id}`, options),
|
||||||
disconnectSSHSessionForSelf: (id: string) => request<void>(`/api/ssh/sessions/${id}/disconnect`, { method: 'POST' }),
|
disconnectSSHSessionForSelf: (id: string) => request<void>(`/api/ssh/sessions/${id}/disconnect`, { method: 'POST' }),
|
||||||
inspectSSHCertificate: (certificate: string) =>
|
inspectSSHCertificate: (certificate: string) =>
|
||||||
request<{ dump: string }>('/api/ssh/cert/inspect', { method: 'POST', body: JSON.stringify({ certificate }) }),
|
request<{ dump: string }>('/api/ssh/cert/inspect', { method: 'POST', body: JSON.stringify({ certificate }) }),
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default function Layout() {
|
|||||||
adminItems.push({ label: 'Client Cert Profiles', path: '/admin/pki/client-profiles', icon: <VerifiedUserIcon fontSize="small" /> })
|
adminItems.push({ label: 'Client Cert Profiles', path: '/admin/pki/client-profiles', icon: <VerifiedUserIcon fontSize="small" /> })
|
||||||
adminItems.push({ label: 'SSH Servers', path: '/admin/ssh-servers', icon: <DnsIcon fontSize="small" /> })
|
adminItems.push({ label: 'SSH Servers', path: '/admin/ssh-servers', icon: <DnsIcon fontSize="small" /> })
|
||||||
adminItems.push({ label: 'SSH Access Profiles', path: '/admin/ssh-access-profiles', icon: <TerminalIcon fontSize="small" /> })
|
adminItems.push({ label: 'SSH Access Profiles', path: '/admin/ssh-access-profiles', icon: <TerminalIcon fontSize="small" /> })
|
||||||
|
adminItems.push({ label: 'SSH Sessions', path: '/admin/ssh-sessions', icon: <HistoryIcon fontSize="small" /> })
|
||||||
adminItems.push({ label: 'Admin SSH CA', path: '/admin/ssh-ca', icon: <BadgeOutlinedIcon fontSize="small" /> })
|
adminItems.push({ label: 'Admin SSH CA', path: '/admin/ssh-ca', icon: <BadgeOutlinedIcon fontSize="small" /> })
|
||||||
adminItems.push({ label: 'SSH Sign History', path: '/admin/ssh-sign-history', icon: <HistoryIcon fontSize="small" /> })
|
adminItems.push({ label: 'SSH Sign History', path: '/admin/ssh-sign-history', icon: <HistoryIcon fontSize="small" /> })
|
||||||
adminItems.push({ label: 'SSH Principal Grants', path: '/admin/ssh-principal-grants', icon: <ManageAccountsIcon fontSize="small" /> })
|
adminItems.push({ label: 'SSH Principal Grants', path: '/admin/ssh-principal-grants', icon: <ManageAccountsIcon fontSize="small" /> })
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import AdminPKIClientProfilesPage from '../pages/AdminPKIClientProfilesPage'
|
|||||||
import AdminSSHUserCAPage from '../pages/AdminSSHUserCAPage'
|
import AdminSSHUserCAPage from '../pages/AdminSSHUserCAPage'
|
||||||
import AdminSSHServersPage from '../pages/AdminSSHServersPage'
|
import AdminSSHServersPage from '../pages/AdminSSHServersPage'
|
||||||
import AdminSSHAccessProfilesPage from '../pages/AdminSSHAccessProfilesPage'
|
import AdminSSHAccessProfilesPage from '../pages/AdminSSHAccessProfilesPage'
|
||||||
|
import AdminSSHSessionsPage from '../pages/AdminSSHSessionsPage'
|
||||||
import AdminSSHSignHistoryPage from '../pages/AdminSSHSignHistoryPage'
|
import AdminSSHSignHistoryPage from '../pages/AdminSSHSignHistoryPage'
|
||||||
import AdminSSHPrincipalGrantsPage from '../pages/AdminSSHPrincipalGrantsPage'
|
import AdminSSHPrincipalGrantsPage from '../pages/AdminSSHPrincipalGrantsPage'
|
||||||
import AdminTLSAuthPoliciesPage from '../pages/AdminTLSAuthPoliciesPage'
|
import AdminTLSAuthPoliciesPage from '../pages/AdminTLSAuthPoliciesPage'
|
||||||
@@ -99,6 +100,7 @@ export const routes: RouteObject[] = [
|
|||||||
{ path: 'admin/pki/client-profiles', element: <AdminPKIClientProfilesPage /> },
|
{ path: 'admin/pki/client-profiles', element: <AdminPKIClientProfilesPage /> },
|
||||||
{ path: 'admin/ssh-servers', element: <AdminSSHServersPage /> },
|
{ path: 'admin/ssh-servers', element: <AdminSSHServersPage /> },
|
||||||
{ path: 'admin/ssh-access-profiles', element: <AdminSSHAccessProfilesPage /> },
|
{ path: 'admin/ssh-access-profiles', element: <AdminSSHAccessProfilesPage /> },
|
||||||
|
{ path: 'admin/ssh-sessions', element: <AdminSSHSessionsPage /> },
|
||||||
{ path: 'admin/ssh-ca', element: <AdminSSHUserCAPage /> },
|
{ path: 'admin/ssh-ca', element: <AdminSSHUserCAPage /> },
|
||||||
{ path: 'admin/ssh-sign-history', element: <AdminSSHSignHistoryPage /> },
|
{ path: 'admin/ssh-sign-history', element: <AdminSSHSignHistoryPage /> },
|
||||||
{ path: 'admin/ssh-principal-grants', element: <AdminSSHPrincipalGrantsPage /> },
|
{ path: 'admin/ssh-principal-grants', element: <AdminSSHPrincipalGrantsPage /> },
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ export default function FormDialogContent(props: DialogContentProps) {
|
|||||||
return (
|
return (
|
||||||
<DialogContent
|
<DialogContent
|
||||||
{...props}
|
{...props}
|
||||||
sx={[
|
sx={[ // props.sx may exist. this overrides it
|
||||||
{
|
{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: 1.5,
|
gap: 1.5,
|
||||||
pt: '8px !important'
|
pt: '8px !important'
|
||||||
},
|
},
|
||||||
props.sx
|
// revive the overridden props.sx
|
||||||
|
// Array.isArray() check in case it's already an array.
|
||||||
|
...(Array.isArray(props.sx)? props.sx: (props.sx? [props.sx] : []))
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import Alert from '@mui/material/Alert'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import SectionCard from '../components/SectionCard'
|
||||||
|
import { api, SSHSession, SSHSessionListResponse } from '../api'
|
||||||
|
|
||||||
|
function fmt(value: number): string {
|
||||||
|
if (!value || value <= 0) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
return new Date(value * 1000).toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionTitle(item: SSHSession): string {
|
||||||
|
return `${item.remote_username}@${item.host}:${item.port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminSSHSessionsPage() {
|
||||||
|
const [items, setItems] = useState<SSHSession[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [status, setStatus] = useState('')
|
||||||
|
const [limit, setLimit] = useState(25)
|
||||||
|
const [offset, setOffset] = useState(0)
|
||||||
|
const [hasMore, setHasMore] = useState(false)
|
||||||
|
|
||||||
|
const load = useCallback(async (signal?: AbortSignal) => {
|
||||||
|
let response: SSHSessionListResponse
|
||||||
|
let nextQuery: string
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
nextQuery = query.trim()
|
||||||
|
response = await api.listSSHSessionsAdmin({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
q: nextQuery || undefined,
|
||||||
|
status: status || undefined
|
||||||
|
}, signal ? { signal } : undefined)
|
||||||
|
setItems(Array.isArray(response.items) ? response.items : [])
|
||||||
|
setHasMore(Boolean(response.has_more))
|
||||||
|
} catch (err) {
|
||||||
|
if (signal?.aborted) { return }
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load SSH session history')
|
||||||
|
setItems([])
|
||||||
|
setHasMore(false)
|
||||||
|
} finally {
|
||||||
|
if (!signal?.aborted) setLoading(false)
|
||||||
|
}
|
||||||
|
}, [limit, offset, query, status])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ac: AbortController = new AbortController()
|
||||||
|
|
||||||
|
void load(ac.signal)
|
||||||
|
return () => {
|
||||||
|
ac.abort()
|
||||||
|
}
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'grid', gap: 1 }}>
|
||||||
|
<Typography variant="h5">Admin: SSH Sessions</Typography>
|
||||||
|
{error ? <Alert severity="error">{error}</Alert> : null}
|
||||||
|
<SectionCard
|
||||||
|
title="SSH Session History"
|
||||||
|
subtitle="All SSH sessions across users."
|
||||||
|
actions={(
|
||||||
|
<Button variant="outlined" size="small" onClick={() => void load()} disabled={loading}>
|
||||||
|
{loading ? 'Refreshing...' : 'Refresh'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label="Search"
|
||||||
|
value={query}
|
||||||
|
onChange={(event) => {
|
||||||
|
setQuery(event.target.value)
|
||||||
|
setOffset(0)
|
||||||
|
}}
|
||||||
|
sx={{ minWidth: 240, maxWidth: 360 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
label="Status"
|
||||||
|
value={status}
|
||||||
|
onChange={(event) => {
|
||||||
|
setStatus(event.target.value)
|
||||||
|
setOffset(0)
|
||||||
|
}}
|
||||||
|
sx={{ minWidth: 150 }}
|
||||||
|
>
|
||||||
|
<MenuItem value="">(all)</MenuItem>
|
||||||
|
<MenuItem value="pending">pending</MenuItem>
|
||||||
|
<MenuItem value="connecting">connecting</MenuItem>
|
||||||
|
<MenuItem value="connected">connected</MenuItem>
|
||||||
|
<MenuItem value="closed">closed</MenuItem>
|
||||||
|
<MenuItem value="error">error</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
label="Page Size"
|
||||||
|
value={String(limit)}
|
||||||
|
onChange={(event) => {
|
||||||
|
setLimit(Number(event.target.value) || 25)
|
||||||
|
setOffset(0)
|
||||||
|
}}
|
||||||
|
sx={{ minWidth: 130 }}
|
||||||
|
>
|
||||||
|
<MenuItem value="10">10</MenuItem>
|
||||||
|
<MenuItem value="25">25</MenuItem>
|
||||||
|
<MenuItem value="50">50</MenuItem>
|
||||||
|
<MenuItem value="100">100</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Box>
|
||||||
|
{loading && !items.length ? (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{!loading && !items.length ? (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
No SSH session history found.
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{items.length ? (
|
||||||
|
<Box sx={{ display: 'grid', gap: 0 }}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Box
|
||||||
|
key={item.id}
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gap: 0.5,
|
||||||
|
py: 1,
|
||||||
|
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
<Box sx={{ minWidth: 0, display: 'grid', gap: 0.25 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ wordBreak: 'break-word' }}>
|
||||||
|
{sessionTitle(item)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
|
User: {item.username} · Auth: {item.auth_method} · Started: {fmt(item.started_at)} · Ended: {fmt(item.ended_at)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
|
Session: {item.id} · Profile: {item.profile_id} · Server: {item.server_id}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
|
Remote: {item.remote_addr || '-'} · Host key: {item.host_key_fingerprint || '-'}
|
||||||
|
</Typography>
|
||||||
|
{item.error ? (
|
||||||
|
<Typography variant="caption" color="error" sx={{ wordBreak: 'break-word' }}>
|
||||||
|
{item.error}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
<Chip
|
||||||
|
label={item.status || 'unknown'}
|
||||||
|
size="small"
|
||||||
|
color={item.status === 'connected' ? 'success' : item.status === 'error' ? 'error' : 'default'}
|
||||||
|
variant={item.status === 'connected' ? 'filled' : 'outlined'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Showing {items.length ? offset + 1 : 0}-{offset + items.length}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button variant="outlined" size="small" onClick={() => setOffset((prev) => Math.max(0, prev - limit))} disabled={loading || offset <= 0}>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" size="small" onClick={() => setOffset((prev) => prev + limit)} disabled={loading || !hasMore}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SectionCard>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,9 +24,10 @@ type SSHAccessProfileForm = {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
remote_username: string
|
remote_username: string
|
||||||
auth_method: 'managed_ssh_cert' | 'stored_private_key'
|
auth_method: 'managed_ssh_cert' | 'prompted_password' | 'stored_password' | 'stored_private_key'
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
private_key_pem: string
|
private_key_pem: string
|
||||||
|
password_text: string
|
||||||
ssh_user_ca_id: string
|
ssh_user_ca_id: string
|
||||||
ssh_principal_mode: 'explicit' | 'grant'
|
ssh_principal_mode: 'explicit' | 'grant'
|
||||||
ssh_principals_text: string
|
ssh_principals_text: string
|
||||||
@@ -52,6 +53,7 @@ const emptyForm = (): SSHAccessProfileForm => ({
|
|||||||
auth_method: 'managed_ssh_cert',
|
auth_method: 'managed_ssh_cert',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
private_key_pem: '',
|
private_key_pem: '',
|
||||||
|
password_text: '',
|
||||||
ssh_user_ca_id: '',
|
ssh_user_ca_id: '',
|
||||||
ssh_principal_mode: 'explicit',
|
ssh_principal_mode: 'explicit',
|
||||||
ssh_principals_text: '',
|
ssh_principals_text: '',
|
||||||
@@ -79,6 +81,9 @@ export default function SSHServersPage() {
|
|||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [viewItem, setViewItem] = useState<SSHAccessProfile | null>(null)
|
const [viewItem, setViewItem] = useState<SSHAccessProfile | null>(null)
|
||||||
const [connectingID, setConnectingID] = useState<string | null>(null)
|
const [connectingID, setConnectingID] = useState<string | null>(null)
|
||||||
|
const [connectPasswordItem, setConnectPasswordItem] = useState<SSHAccessProfile | null>(null)
|
||||||
|
const [connectPassword, setConnectPassword] = useState('')
|
||||||
|
const [connectPasswordError, setConnectPasswordError] = useState<string | null>(null)
|
||||||
const [dialogOpen, setDialogOpen] = useState(false)
|
const [dialogOpen, setDialogOpen] = useState(false)
|
||||||
const [editingID, setEditingID] = useState<string | null>(null)
|
const [editingID, setEditingID] = useState<string | null>(null)
|
||||||
const [form, setForm] = useState<SSHAccessProfileForm>(emptyForm())
|
const [form, setForm] = useState<SSHAccessProfileForm>(emptyForm())
|
||||||
@@ -194,6 +199,7 @@ export default function SSHServersPage() {
|
|||||||
auth_method: item.auth_method,
|
auth_method: item.auth_method,
|
||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
private_key_pem: '',
|
private_key_pem: '',
|
||||||
|
password_text: '',
|
||||||
ssh_user_ca_id: item.ssh_user_ca_id || '',
|
ssh_user_ca_id: item.ssh_user_ca_id || '',
|
||||||
ssh_principal_mode: item.ssh_principal_mode || 'explicit',
|
ssh_principal_mode: item.ssh_principal_mode || 'explicit',
|
||||||
ssh_principals_text: (item.ssh_principals || []).join(', '),
|
ssh_principals_text: (item.ssh_principals || []).join(', '),
|
||||||
@@ -255,11 +261,20 @@ export default function SSHServersPage() {
|
|||||||
setHostKeySaving(false)
|
setHostKeySaving(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConnect = async (item: SSHAccessProfile) => {
|
const closeConnectPasswordPrompt = () => {
|
||||||
|
if (connectingID) return
|
||||||
|
setConnectPasswordItem(null)
|
||||||
|
setConnectPassword('')
|
||||||
|
setConnectPasswordError(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectToProfile = async (item: SSHAccessProfile, password?: string) => {
|
||||||
let cols = 120
|
let cols = 120
|
||||||
let rows = 36
|
let rows = 36
|
||||||
|
let message: string
|
||||||
|
|
||||||
setError(null)
|
setError(null)
|
||||||
|
setConnectPasswordError(null)
|
||||||
setConnectingID(item.id)
|
setConnectingID(item.id)
|
||||||
try {
|
try {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -269,16 +284,33 @@ export default function SSHServersPage() {
|
|||||||
const session = await api.connectSSHAccessProfileForSelf(item.id, {
|
const session = await api.connectSSHAccessProfileForSelf(item.id, {
|
||||||
cols,
|
cols,
|
||||||
rows,
|
rows,
|
||||||
term: 'xterm-256color'
|
term: 'xterm-256color',
|
||||||
|
password
|
||||||
})
|
})
|
||||||
|
closeConnectPasswordPrompt()
|
||||||
navigate(`/ssh-sessions/${session.session_id}`)
|
navigate(`/ssh-sessions/${session.session_id}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to connect')
|
message = err instanceof Error ? err.message : 'Failed to connect'
|
||||||
|
if (password !== undefined) {
|
||||||
|
setConnectPasswordError(message)
|
||||||
|
} else {
|
||||||
|
setError(message)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setConnectingID(null)
|
setConnectingID(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleConnect = async (item: SSHAccessProfile) => {
|
||||||
|
if (item.auth_method === 'prompted_password') {
|
||||||
|
setConnectPasswordItem(item)
|
||||||
|
setConnectPassword('')
|
||||||
|
setConnectPasswordError(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await connectToProfile(item)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
server_id: form.server_id,
|
server_id: form.server_id,
|
||||||
@@ -288,6 +320,7 @@ export default function SSHServersPage() {
|
|||||||
auth_method: form.auth_method,
|
auth_method: form.auth_method,
|
||||||
enabled: form.enabled,
|
enabled: form.enabled,
|
||||||
private_key_pem: form.private_key_pem.trim() || undefined,
|
private_key_pem: form.private_key_pem.trim() || undefined,
|
||||||
|
password_text: form.password_text.trim() || undefined,
|
||||||
ssh_user_ca_id: form.auth_method === 'managed_ssh_cert' ? (form.ssh_user_ca_id || undefined) : undefined,
|
ssh_user_ca_id: form.auth_method === 'managed_ssh_cert' ? (form.ssh_user_ca_id || undefined) : undefined,
|
||||||
ssh_principal_mode: form.auth_method === 'managed_ssh_cert' ? form.ssh_principal_mode : undefined,
|
ssh_principal_mode: form.auth_method === 'managed_ssh_cert' ? form.ssh_principal_mode : undefined,
|
||||||
ssh_principals: form.auth_method === 'managed_ssh_cert' && form.ssh_principal_mode === 'explicit' ? parseNames(form.ssh_principals_text) : [],
|
ssh_principals: form.auth_method === 'managed_ssh_cert' && form.ssh_principal_mode === 'explicit' ? parseNames(form.ssh_principals_text) : [],
|
||||||
@@ -552,7 +585,12 @@ export default function SSHServersPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'grid', gap: 1 }}>
|
<Box sx={{ display: 'grid', gap: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||||
<Typography variant="h5">SSH Servers</Typography>
|
<Typography variant="h5">SSH Servers</Typography>
|
||||||
|
<Button variant="outlined" onClick={() => navigate('/ssh-sessions', { state: { historyOpen: true } })}>
|
||||||
|
Session History
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
{error ? <Alert severity="error">{error}</Alert> : null}
|
{error ? <Alert severity="error">{error}</Alert> : null}
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title={
|
title={
|
||||||
@@ -665,6 +703,8 @@ export default function SSHServersPage() {
|
|||||||
onChange={(event) => setForm((prev) => ({ ...prev, auth_method: event.target.value as SSHAccessProfileForm['auth_method'] }))}
|
onChange={(event) => setForm((prev) => ({ ...prev, auth_method: event.target.value as SSHAccessProfileForm['auth_method'] }))}
|
||||||
>
|
>
|
||||||
<MenuItem value="managed_ssh_cert">managed_ssh_cert</MenuItem>
|
<MenuItem value="managed_ssh_cert">managed_ssh_cert</MenuItem>
|
||||||
|
<MenuItem value="prompted_password">prompted_password</MenuItem>
|
||||||
|
<MenuItem value="stored_password">stored_password</MenuItem>
|
||||||
<MenuItem value="stored_private_key">stored_private_key</MenuItem>
|
<MenuItem value="stored_private_key">stored_private_key</MenuItem>
|
||||||
</SelectField>
|
</SelectField>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
@@ -700,7 +740,7 @@ export default function SSHServersPage() {
|
|||||||
helperText="Comma-separated principal names."
|
helperText="Comma-separated principal names."
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Autocomplete
|
<Autocomplete<SSHPrincipalGrant, true, false, false>
|
||||||
multiple
|
multiple
|
||||||
options={grants}
|
options={grants}
|
||||||
value={selectedGrantOptions}
|
value={selectedGrantOptions}
|
||||||
@@ -730,7 +770,7 @@ export default function SSHServersPage() {
|
|||||||
helperText={editingID ? 'Leave blank to keep the existing managed key.' : 'Leave blank to auto-generate an ed25519 key.'}
|
helperText={editingID ? 'Leave blank to keep the existing managed key.' : 'Leave blank to auto-generate an ed25519 key.'}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : form.auth_method == 'stored_private_key'? (
|
||||||
<TextField
|
<TextField
|
||||||
label="Private Key PEM"
|
label="Private Key PEM"
|
||||||
multiline
|
multiline
|
||||||
@@ -739,7 +779,15 @@ export default function SSHServersPage() {
|
|||||||
onChange={(event) => setForm((prev) => ({ ...prev, private_key_pem: event.target.value }))}
|
onChange={(event) => setForm((prev) => ({ ...prev, private_key_pem: event.target.value }))}
|
||||||
helperText={editingID ? 'Leave blank to keep the existing key.' : 'Required for stored_private_key.'}
|
helperText={editingID ? 'Leave blank to keep the existing key.' : 'Required for stored_private_key.'}
|
||||||
/>
|
/>
|
||||||
)}
|
) : form.auth_method == 'stored_password'? (
|
||||||
|
<TextField
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={form.password_text}
|
||||||
|
onChange={(event) => setForm((prev) => ({ ...prev, password_text: event.target.value }))}
|
||||||
|
helperText={editingID ? 'Leave blank to keep the existing password.' : 'Required for stored_password.'}
|
||||||
|
/>
|
||||||
|
) : null }
|
||||||
</FormDialogContent>
|
</FormDialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setDialogOpen(false)} disabled={saving}>
|
<Button onClick={() => setDialogOpen(false)} disabled={saving}>
|
||||||
@@ -831,6 +879,35 @@ export default function SSHServersPage() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={Boolean(connectPasswordItem)} onClose={closeConnectPasswordPrompt} maxWidth="xs" fullWidth>
|
||||||
|
<DialogTitle>Enter SSH Password</DialogTitle>
|
||||||
|
<FormDialogContent>
|
||||||
|
{connectPasswordError ? <Alert severity="error">{connectPasswordError}</Alert> : null}
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{connectPasswordItem ? `${connectPasswordItem.remote_username}@${connectPasswordItem.server?.host || connectPasswordItem.server_id}:${connectPasswordItem.server?.port || 22}` : 'Enter the password for this SSH access profile.'}
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={connectPassword}
|
||||||
|
onChange={(event) => setConnectPassword(event.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</FormDialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeConnectPasswordPrompt} disabled={Boolean(connectingID)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => connectPasswordItem ? void connectToProfile(connectPasswordItem, connectPassword) : undefined}
|
||||||
|
disabled={Boolean(connectingID) || connectPassword === ''}
|
||||||
|
>
|
||||||
|
{connectingID ? 'Connecting...' : 'Connect'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={Boolean(hostKeyServer)} onClose={closeHostKeys} maxWidth="md" fullWidth>
|
<Dialog open={Boolean(hostKeyServer)} onClose={closeHostKeys} maxWidth="md" fullWidth>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Box sx={{ display: 'grid', gap: 1 }}>
|
<Box sx={{ display: 'grid', gap: 1 }}>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user