Compare commits
8 Commits
| 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 == "" {
|
||||||
@@ -409,9 +410,11 @@ func main() {
|
|||||||
RpmMeta: rpmMeta,
|
RpmMeta: rpmMeta,
|
||||||
RpmMirror: rpmMirror,
|
RpmMirror: rpmMirror,
|
||||||
DockerBase: dockerBase,
|
DockerBase: dockerBase,
|
||||||
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())
|
||||||
}
|
if ok && requestStore != nil { return requestStore }
|
||||||
requestStore, ok = middleware.StoreFromContext(r.Context())
|
|
||||||
if ok && requestStore != nil {
|
|
||||||
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
|
||||||
|
|||||||
+187
-127
@@ -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"
|
||||||
|
|||||||
+1347
-1323
File diff suppressed because it is too large
Load Diff
@@ -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 /> },
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ import DialogContent from '@mui/material/DialogContent'
|
|||||||
import { DialogContentProps } from '@mui/material/DialogContent'
|
import { DialogContentProps } from '@mui/material/DialogContent'
|
||||||
|
|
||||||
export default function FormDialogContent(props: DialogContentProps) {
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user