Compare commits
4 Commits
352c92c638
...
228a74a695
| Author | SHA1 | Date | |
|---|---|---|---|
| 228a74a695 | |||
| 23ea03aa67 | |||
| f7d03485e4 | |||
| 0285cb9fa0 |
+28
-21
@@ -40,7 +40,7 @@ func (s *Store) GetACMEProfile(id string) (models.ACMEProfile, error) {
|
||||
var item models.ACMEProfile
|
||||
row = s.QueryRow(`SELECT public_id, name, directory_url, email, account_url, account_key_pem, solver_type, acme_dns_api_url, acme_dns_user, acme_dns_key, acme_dns_subdomain, acme_dns_full_domain, enabled, last_error, created_at, updated_at
|
||||
FROM acme_profiles
|
||||
WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
WHERE o.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(&item.ID, &item.Name, &item.DirectoryURL, &item.Email, &item.AccountURL, &item.AccountKeyPEM, &item.SolverType, &item.ACMEDNSAPIURL, &item.ACMEDNSUser, &item.ACMEDNSKey, &item.ACMEDNSSubdomain, &item.ACMEDNSFullDomain, &item.Enabled, &item.LastError, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -118,14 +118,18 @@ func (s *Store) ListACMEOrders(profileID string) ([]models.ACMEOrder, error) {
|
||||
var item models.ACMEOrder
|
||||
var challengesJSON string
|
||||
if strings.TrimSpace(profileID) == "" {
|
||||
rows, err = s.Query(`SELECT public_id, profile_public_id, common_name, san_dns, order_url, finalize_url, status, last_error, cert_public_id, challenges_json, created_at, updated_at
|
||||
FROM acme_orders
|
||||
ORDER BY created_at DESC`)
|
||||
rows, err = s.Query(`SELECT o.public_id, COALESCE(p.public_id, ''), o.common_name, o.san_dns, o.order_url, o.finalize_url, o.status, o.last_error, COALESCE(c.public_id, ''), o.challenges_json, o.created_at, o.updated_at
|
||||
FROM acme_orders o
|
||||
LEFT JOIN acme_profiles p ON p.id = o.profile_id
|
||||
LEFT JOIN pki_certs c ON c.id = o.cert_id
|
||||
ORDER BY o.created_at DESC`)
|
||||
} else {
|
||||
rows, err = s.Query(`SELECT public_id, profile_public_id, common_name, san_dns, order_url, finalize_url, status, last_error, cert_public_id, challenges_json, created_at, updated_at
|
||||
FROM acme_orders
|
||||
WHERE profile_public_id = ?
|
||||
ORDER BY created_at DESC`, strings.TrimSpace(profileID))
|
||||
rows, err = s.Query(`SELECT o.public_id, COALESCE(p.public_id, ''), o.common_name, o.san_dns, o.order_url, o.finalize_url, o.status, o.last_error, COALESCE(c.public_id, ''), o.challenges_json, o.created_at, o.updated_at
|
||||
FROM acme_orders o
|
||||
LEFT JOIN acme_profiles p ON p.id = o.profile_id
|
||||
LEFT JOIN pki_certs c ON c.id = o.cert_id
|
||||
WHERE p.public_id = ?
|
||||
ORDER BY o.created_at DESC`, strings.TrimSpace(profileID))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -154,9 +158,11 @@ func (s *Store) GetACMEOrder(id string) (models.ACMEOrder, error) {
|
||||
var err error
|
||||
var item models.ACMEOrder
|
||||
var challengesJSON string
|
||||
row = s.QueryRow(`SELECT public_id, profile_public_id, common_name, san_dns, order_url, finalize_url, status, last_error, cert_public_id, challenges_json, csr_pem, key_pem, created_at, updated_at
|
||||
FROM acme_orders
|
||||
WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
row = s.QueryRow(`SELECT o.public_id, COALESCE(p.public_id, ''), o.common_name, o.san_dns, o.order_url, o.finalize_url, o.status, o.last_error, COALESCE(c.public_id, ''), o.challenges_json, o.csr_pem, o.key_pem, o.created_at, o.updated_at
|
||||
FROM acme_orders o
|
||||
LEFT JOIN acme_profiles p ON p.id = o.profile_id
|
||||
LEFT JOIN pki_certs c ON c.id = o.cert_id
|
||||
WHERE o.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(&item.ID, &item.ProfileID, &item.CommonName, &item.SANDNS, &item.OrderURL, &item.FinalizeURL, &item.Status, &item.LastError, &item.CertID, &challengesJSON, &item.CSRPEM, &item.KeyPEM, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -185,9 +191,9 @@ func (s *Store) CreateACMEOrder(item models.ACMEOrder) (models.ACMEOrder, error)
|
||||
now = time.Now().UTC().Unix()
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
_, err = s.Exec(`INSERT INTO acme_orders (public_id, profile_public_id, common_name, san_dns, order_url, finalize_url, status, last_error, cert_public_id, challenges_json, csr_pem, key_pem, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.ID, item.ProfileID, item.CommonName, item.SANDNS, item.OrderURL, item.FinalizeURL, item.Status, item.LastError, item.CertID, challengesJSON, item.CSRPEM, item.KeyPEM, item.CreatedAt, item.UpdatedAt)
|
||||
_, err = s.Exec(`INSERT INTO acme_orders (public_id, profile_id, common_name, san_dns, order_url, finalize_url, status, last_error, cert_id, challenges_json, csr_pem, key_pem, created_at, updated_at)
|
||||
VALUES (?, (SELECT id FROM acme_profiles WHERE public_id = ?), ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM pki_certs WHERE public_id = ?) END, ?, ?, ?, ?, ?)`,
|
||||
item.ID, item.ProfileID, item.CommonName, item.SANDNS, item.OrderURL, item.FinalizeURL, item.Status, item.LastError, strings.TrimSpace(item.CertID), strings.TrimSpace(item.CertID), challengesJSON, item.CSRPEM, item.KeyPEM, item.CreatedAt, item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
@@ -203,9 +209,9 @@ func (s *Store) UpdateACMEOrder(item models.ACMEOrder) (models.ACMEOrder, error)
|
||||
}
|
||||
item.UpdatedAt = time.Now().UTC().Unix()
|
||||
_, err = s.Exec(`UPDATE acme_orders
|
||||
SET status = ?, last_error = ?, cert_public_id = ?, challenges_json = ?, updated_at = ?
|
||||
SET status = ?, last_error = ?, cert_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM pki_certs WHERE public_id = ?) END, challenges_json = ?, updated_at = ?
|
||||
WHERE public_id = ?`,
|
||||
item.Status, item.LastError, item.CertID, challengesJSON, item.UpdatedAt, item.ID)
|
||||
item.Status, item.LastError, strings.TrimSpace(item.CertID), strings.TrimSpace(item.CertID), challengesJSON, item.UpdatedAt, item.ID)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
@@ -226,10 +232,11 @@ func (s *Store) GetLatestACMEOrderByCertID(certID string) (models.ACMEOrder, err
|
||||
var row *sql.Row
|
||||
var item models.ACMEOrder
|
||||
var err error
|
||||
row = s.QueryRow(`SELECT public_id, profile_public_id, common_name, san_dns
|
||||
FROM acme_orders
|
||||
WHERE cert_public_id = ?
|
||||
ORDER BY updated_at DESC, created_at DESC
|
||||
row = s.QueryRow(`SELECT o.public_id, COALESCE(p.public_id, ''), o.common_name, o.san_dns
|
||||
FROM acme_orders o
|
||||
LEFT JOIN acme_profiles p ON p.id = o.profile_id
|
||||
WHERE o.cert_id = (SELECT id FROM pki_certs WHERE public_id = ?)
|
||||
ORDER BY o.updated_at DESC, o.created_at DESC
|
||||
LIMIT 1`, strings.TrimSpace(certID))
|
||||
err = row.Scan(&item.ID, &item.ProfileID, &item.CommonName, &item.SANDNS)
|
||||
if err != nil {
|
||||
@@ -245,7 +252,7 @@ func (s *Store) HasACMEOrderForCert(certID string) (bool, error) {
|
||||
|
||||
row = s.QueryRow(`SELECT 1
|
||||
FROM acme_orders
|
||||
WHERE cert_public_id = ?
|
||||
WHERE cert_id = (SELECT id FROM pki_certs WHERE public_id = ?)
|
||||
LIMIT 1`, strings.TrimSpace(certID))
|
||||
err = row.Scan(&found)
|
||||
if err == sql.ErrNoRows {
|
||||
|
||||
@@ -130,17 +130,17 @@ func (s *Store) DeleteUserGroup(groupID string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'group' AND subject_public_id = ?`, strings.TrimSpace(groupID))
|
||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'group' AND subject_id = (SELECT id FROM user_groups WHERE public_id = ?)`, strings.TrimSpace(groupID))
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM ssh_principal_grant_targets WHERE target_type = 'group' AND target_public_id = ?`, strings.TrimSpace(groupID))
|
||||
_, err = tx.Exec(`DELETE FROM ssh_principal_grant_targets WHERE target_type = 'group' AND target_id = (SELECT id FROM user_groups WHERE public_id = ?)`, strings.TrimSpace(groupID))
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM subject_permissions WHERE subject_type = 'group' AND subject_public_id = ?`, strings.TrimSpace(groupID))
|
||||
_, err = tx.Exec(`DELETE FROM subject_permissions WHERE subject_type = 'group' AND subject_id = (SELECT id FROM user_groups WHERE public_id = ?)`, strings.TrimSpace(groupID))
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
@@ -243,7 +243,7 @@ func (s *Store) ListProjectGroupRoles(projectID string) ([]models.ProjectGroupRo
|
||||
rows, err = s.Query(`SELECT p.public_id, g.public_id, b.role, b.created_at
|
||||
FROM project_role_bindings b
|
||||
JOIN projects p ON p.id = b.project_id
|
||||
JOIN user_groups g ON g.public_id = b.subject_public_id
|
||||
JOIN user_groups g ON g.id = b.subject_id
|
||||
WHERE p.public_id = ? AND b.subject_type = 'group'
|
||||
ORDER BY g.name`, strings.TrimSpace(projectID))
|
||||
if err != nil {
|
||||
@@ -271,11 +271,11 @@ func (s *Store) UpsertProjectGroupRole(projectID string, groupID string, role st
|
||||
var rowsAffected int64
|
||||
var err error
|
||||
now = time.Now().UTC().Unix()
|
||||
result, err = s.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_public_id, role, created_at, updated_at)
|
||||
SELECT (SELECT id FROM projects WHERE public_id = ?), 'group', g.public_id, ?, ?, ?
|
||||
result, err = s.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at)
|
||||
SELECT (SELECT id FROM projects WHERE public_id = ?), 'group', g.id, ?, ?, ?
|
||||
FROM user_groups g
|
||||
WHERE g.public_id = ?
|
||||
ON CONFLICT(project_id, subject_type, subject_public_id) DO UPDATE SET role = excluded.role, updated_at = excluded.updated_at`,
|
||||
ON CONFLICT(project_id, subject_type, subject_id) DO UPDATE SET role = excluded.role, updated_at = excluded.updated_at`,
|
||||
strings.TrimSpace(projectID), strings.TrimSpace(role), now, now, strings.TrimSpace(groupID))
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -299,7 +299,7 @@ func (s *Store) RemoveProjectGroupRole(projectID string, groupID string) error {
|
||||
_, err = s.Exec(`DELETE FROM project_role_bindings
|
||||
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
AND subject_type = 'group'
|
||||
AND subject_public_id = ?`,
|
||||
AND subject_id = (SELECT id FROM user_groups WHERE public_id = ?)`,
|
||||
strings.TrimSpace(projectID),
|
||||
strings.TrimSpace(groupID),
|
||||
)
|
||||
@@ -315,10 +315,10 @@ func (s *Store) GetProjectRoleForUser(projectID string, userID string) (string,
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
AND (
|
||||
(b.subject_type = 'user' AND b.subject_public_id = ?)
|
||||
(b.subject_type = 'user' AND b.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR
|
||||
(b.subject_type = 'group' AND b.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
(b.subject_type = 'group' AND b.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
@@ -368,7 +368,7 @@ func (s *Store) GetProjectRoleForPrincipal(projectID string, principalID string)
|
||||
FROM project_role_bindings
|
||||
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
AND subject_type = 'principal'
|
||||
AND subject_public_id = ?`,
|
||||
AND subject_id = (SELECT id FROM service_principals WHERE public_id = ?)`,
|
||||
strings.TrimSpace(projectID),
|
||||
strings.TrimSpace(principalID),
|
||||
)
|
||||
|
||||
@@ -38,8 +38,11 @@ func (s *Store) ListPKIClientProfiles(targetType string, targetID string) ([]mod
|
||||
JOIN pki_cas ca ON ca.id = p.ca_id
|
||||
LEFT JOIN users u ON u.id = p.created_by_user_id
|
||||
JOIN pki_client_profile_targets t ON t.profile_id = p.id
|
||||
WHERE t.target_type = ? AND t.target_public_id = ?
|
||||
ORDER BY p.name`, targetType, targetID)
|
||||
LEFT JOIN users tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||
LEFT JOIN user_groups tg ON t.target_type = 'group' AND tg.id = t.target_id
|
||||
WHERE t.target_type = ?
|
||||
AND ((t.target_type = 'user' AND tu.public_id = ?) OR (t.target_type = 'group' AND tg.public_id = ?))
|
||||
ORDER BY p.name`, targetType, targetID, targetID)
|
||||
} else {
|
||||
rows, err = s.Query(`SELECT
|
||||
p.public_id,
|
||||
@@ -130,13 +133,14 @@ func (s *Store) ListActivePKIClientProfilesForUser(userID string) ([]models.PKIC
|
||||
JOIN pki_cas ca ON ca.id = p.ca_id
|
||||
LEFT JOIN users u ON u.id = p.created_by_user_id
|
||||
JOIN pki_client_profile_targets t ON t.profile_id = p.id
|
||||
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.public_id = t.target_public_id
|
||||
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.id = t.target_id
|
||||
LEFT JOIN user_group_members gm ON t.target_type = 'group' AND gm.group_id = ug.id
|
||||
LEFT JOIN users gu ON gm.user_id = gu.id
|
||||
LEFT JOIN users tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||
WHERE p.enabled = 1
|
||||
AND ca.status = 'active'
|
||||
AND (
|
||||
(t.target_type = 'user' AND t.target_public_id = ?)
|
||||
(t.target_type = 'user' AND tu.public_id = ?)
|
||||
OR
|
||||
(t.target_type = 'group' AND ug.disabled = 0 AND (ug.scope = 'all_users' OR gu.public_id = ?))
|
||||
)
|
||||
@@ -264,14 +268,15 @@ func (s *Store) GetActivePKIClientProfileForUser(profileID string, userID string
|
||||
JOIN pki_cas ca ON ca.id = p.ca_id
|
||||
LEFT JOIN users u ON u.id = p.created_by_user_id
|
||||
JOIN pki_client_profile_targets t ON t.profile_id = p.id
|
||||
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.public_id = t.target_public_id
|
||||
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.id = t.target_id
|
||||
LEFT JOIN user_group_members gm ON t.target_type = 'group' AND gm.group_id = ug.id
|
||||
LEFT JOIN users gu ON gm.user_id = gu.id
|
||||
LEFT JOIN users tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||
WHERE p.public_id = ?
|
||||
AND p.enabled = 1
|
||||
AND ca.status = 'active'
|
||||
AND (
|
||||
(t.target_type = 'user' AND t.target_public_id = ?)
|
||||
(t.target_type = 'user' AND tu.public_id = ?)
|
||||
OR
|
||||
(t.target_type = 'group' AND ug.disabled = 0 AND (ug.scope = 'all_users' OR gu.public_id = ?))
|
||||
)`,
|
||||
@@ -503,10 +508,10 @@ func (s *Store) ListPKIClientIssuancesForUser(userID string) ([]models.PKIClient
|
||||
|
||||
rows, err = s.Query(`SELECT
|
||||
i.public_id,
|
||||
i.cert_public_id,
|
||||
i.user_public_id,
|
||||
COALESCE(c.public_id, ''),
|
||||
COALESCE(u.public_id, ''),
|
||||
i.username,
|
||||
i.profile_public_id,
|
||||
COALESCE(p.public_id, ''),
|
||||
i.profile_name,
|
||||
i.serial_hex,
|
||||
i.common_name,
|
||||
@@ -520,8 +525,10 @@ func (s *Store) ListPKIClientIssuancesForUser(userID string) ([]models.PKIClient
|
||||
COALESCE(c.revocation_reason, ''),
|
||||
i.created_at
|
||||
FROM pki_client_issuances i
|
||||
LEFT JOIN pki_certs c ON c.public_id = i.cert_public_id
|
||||
WHERE i.user_public_id = ?
|
||||
LEFT JOIN pki_certs c ON c.id = i.cert_id
|
||||
LEFT JOIN users u ON u.id = i.user_id
|
||||
LEFT JOIN pki_client_profiles p ON p.id = i.profile_id
|
||||
WHERE u.public_id = ?
|
||||
ORDER BY i.created_at DESC`, strings.TrimSpace(userID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -573,10 +580,10 @@ func (s *Store) CreatePKIClientIssuance(item models.PKIClientIssuance) (models.P
|
||||
}
|
||||
_, err = s.Exec(`INSERT INTO pki_client_issuances (
|
||||
public_id,
|
||||
cert_public_id,
|
||||
user_public_id,
|
||||
cert_id,
|
||||
user_id,
|
||||
username,
|
||||
profile_public_id,
|
||||
profile_id,
|
||||
profile_name,
|
||||
serial_hex,
|
||||
common_name,
|
||||
@@ -586,7 +593,7 @@ func (s *Store) CreatePKIClientIssuance(item models.PKIClientIssuance) (models.P
|
||||
not_before,
|
||||
not_after,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, (SELECT id FROM pki_certs WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, (SELECT id FROM pki_client_profiles WHERE public_id = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
strings.TrimSpace(item.ID),
|
||||
strings.TrimSpace(item.CertID),
|
||||
strings.TrimSpace(item.UserID),
|
||||
@@ -614,7 +621,8 @@ func (s *Store) UserOwnsPKIClientCert(userID string, certID string) (bool, error
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM pki_client_issuances
|
||||
WHERE user_public_id = ? AND cert_public_id = ?`,
|
||||
WHERE user_id = (SELECT id FROM users WHERE public_id = ?)
|
||||
AND cert_id = (SELECT id FROM pki_certs WHERE public_id = ?)`,
|
||||
strings.TrimSpace(userID),
|
||||
strings.TrimSpace(certID),
|
||||
)
|
||||
@@ -634,10 +642,10 @@ func (s *Store) listPKIClientProfileTargets(profileID string) ([]models.PKIClien
|
||||
rows, err = s.Query(`SELECT
|
||||
p.public_id,
|
||||
t.target_type,
|
||||
t.target_public_id,
|
||||
CASE WHEN t.target_type = 'user' THEN COALESCE(u.public_id, '') ELSE COALESCE(g.public_id, '') END,
|
||||
CASE
|
||||
WHEN t.target_type = 'user' THEN COALESCE(CASE WHEN u.display_name != '' THEN u.display_name || ' (' || u.username || ')' ELSE u.username END, t.target_public_id)
|
||||
ELSE COALESCE(g.name, t.target_public_id)
|
||||
WHEN t.target_type = 'user' THEN COALESCE(CASE WHEN u.display_name != '' THEN u.display_name || ' (' || u.username || ')' ELSE u.username END, '')
|
||||
ELSE COALESCE(g.name, '')
|
||||
END,
|
||||
CASE
|
||||
WHEN t.target_type = 'user' THEN CASE WHEN u.public_id IS NOT NULL AND u.disabled = 0 THEN 1 ELSE 0 END
|
||||
@@ -646,10 +654,10 @@ func (s *Store) listPKIClientProfileTargets(profileID string) ([]models.PKIClien
|
||||
t.created_at
|
||||
FROM pki_client_profile_targets t
|
||||
JOIN pki_client_profiles p ON p.id = t.profile_id
|
||||
LEFT JOIN users u ON t.target_type = 'user' AND u.public_id = t.target_public_id
|
||||
LEFT JOIN user_groups g ON t.target_type = 'group' AND g.public_id = t.target_public_id
|
||||
LEFT JOIN users u ON t.target_type = 'user' AND u.id = t.target_id
|
||||
LEFT JOIN user_groups g ON t.target_type = 'group' AND g.id = t.target_id
|
||||
WHERE p.public_id = ?
|
||||
ORDER BY t.target_type, t.target_public_id`, strings.TrimSpace(profileID))
|
||||
ORDER BY t.target_type, target_id`, strings.TrimSpace(profileID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -689,16 +697,23 @@ func insertPKIClientProfileTargetTx(tx *sql.Tx, profileID string, targetType str
|
||||
_, err = tx.Exec(`INSERT INTO pki_client_profile_targets (
|
||||
profile_id,
|
||||
target_type,
|
||||
target_public_id,
|
||||
target_id,
|
||||
created_at
|
||||
) VALUES (
|
||||
(SELECT id FROM pki_client_profiles WHERE public_id = ?),
|
||||
?,
|
||||
?,
|
||||
CASE
|
||||
WHEN ? = 'user' THEN (SELECT id FROM users WHERE public_id = ?)
|
||||
WHEN ? = 'group' THEN (SELECT id FROM user_groups WHERE public_id = ?)
|
||||
ELSE NULL
|
||||
END,
|
||||
?
|
||||
)`,
|
||||
strings.TrimSpace(profileID),
|
||||
targetType,
|
||||
targetType,
|
||||
targetID,
|
||||
targetType,
|
||||
targetID,
|
||||
createdAt,
|
||||
)
|
||||
|
||||
@@ -80,12 +80,12 @@ func (s *Store) DeleteServicePrincipal(id string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'principal' AND subject_public_id = ?`, id)
|
||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'principal' AND subject_id = (SELECT id FROM service_principals WHERE public_id = ?)`, id)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM subject_permissions WHERE subject_type = 'principal' AND subject_public_id = ?`, id)
|
||||
_, err = tx.Exec(`DELETE FROM subject_permissions WHERE subject_type = 'principal' AND subject_id = (SELECT id FROM service_principals WHERE public_id = ?)`, id)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
@@ -182,7 +182,7 @@ func (s *Store) ListPrincipalProjectRoles(principalID string) ([]models.Principa
|
||||
FROM project_role_bindings b
|
||||
JOIN projects p ON p.id = b.project_id
|
||||
WHERE b.subject_type = 'principal'
|
||||
AND b.subject_public_id = ?
|
||||
AND b.subject_id = (SELECT id FROM service_principals WHERE public_id = ?)
|
||||
ORDER BY p.public_id`, principalID, principalID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -207,9 +207,9 @@ func (s *Store) UpsertPrincipalProjectRole(item models.PrincipalProjectRole) (mo
|
||||
var err error
|
||||
now = time.Now().UTC().Unix()
|
||||
item.CreatedAt = now
|
||||
_, err = s.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_public_id, role, created_at, updated_at)
|
||||
VALUES ((SELECT id FROM projects WHERE public_id = ?), 'principal', ?, ?, ?, ?)
|
||||
ON CONFLICT(project_id, subject_type, subject_public_id) DO UPDATE SET role = excluded.role, updated_at = excluded.updated_at`,
|
||||
_, err = s.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at)
|
||||
VALUES ((SELECT id FROM projects WHERE public_id = ?), 'principal', (SELECT id FROM service_principals WHERE public_id = ?), ?, ?, ?)
|
||||
ON CONFLICT(project_id, subject_type, subject_id) DO UPDATE SET role = excluded.role, updated_at = excluded.updated_at`,
|
||||
item.ProjectID, item.PrincipalID, item.Role, item.CreatedAt, item.CreatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -222,7 +222,7 @@ func (s *Store) DeletePrincipalProjectRole(principalID string, projectID string)
|
||||
_, err = s.Exec(`DELETE FROM project_role_bindings
|
||||
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
AND subject_type = 'principal'
|
||||
AND subject_public_id = ?`, projectID, principalID)
|
||||
AND subject_id = (SELECT id FROM service_principals WHERE public_id = ?)`, projectID, principalID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,10 @@ func (s *Store) ListSSHServers() ([]models.SSHServer, error) {
|
||||
var item models.SSHServer
|
||||
var tagsJSON string
|
||||
|
||||
rows, err = s.Query(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
|
||||
FROM ssh_servers
|
||||
ORDER BY name, host, port`)
|
||||
rows, err = s.Query(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, srv.created_by_subject_id, srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
FROM ssh_servers srv
|
||||
LEFT JOIN users ou ON ou.id = srv.owner_user_id
|
||||
ORDER BY srv.name, srv.host, srv.port`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,9 +68,10 @@ func (s *Store) GetSSHServer(id string) (models.SSHServer, error) {
|
||||
var tagsJSON string
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
|
||||
FROM ssh_servers
|
||||
WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
row = s.QueryRow(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, srv.created_by_subject_id, srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
FROM ssh_servers srv
|
||||
LEFT JOIN users ou ON ou.id = srv.owner_user_id
|
||||
WHERE srv.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.OwnerScope, &item.OwnerUserID, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -90,10 +92,11 @@ func (s *Store) ListSSHServersForUser(userID string) ([]models.SSHServer, error)
|
||||
var trimmedUserID string
|
||||
|
||||
trimmedUserID = strings.TrimSpace(userID)
|
||||
rows, err = s.Query(`SELECT public_id, name, host, port, description, tags_json, enabled, host_key_policy, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
|
||||
FROM ssh_servers
|
||||
WHERE owner_scope = 'user' AND owner_user_public_id = ?
|
||||
ORDER BY name, host, port`, trimmedUserID)
|
||||
rows, err = s.Query(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, srv.created_by_subject_id, srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
FROM ssh_servers srv
|
||||
LEFT JOIN users ou ON ou.id = srv.owner_user_id
|
||||
WHERE srv.owner_scope = 'user' AND ou.public_id = ?
|
||||
ORDER BY srv.name, srv.host, srv.port`, trimmedUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -191,13 +194,13 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
||||
enabled,
|
||||
host_key_policy,
|
||||
owner_scope,
|
||||
owner_user_public_id,
|
||||
owner_user_id,
|
||||
created_by_kind,
|
||||
created_by_subject_id,
|
||||
created_by_subject_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, ?, ?, ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.Name),
|
||||
strings.TrimSpace(item.Host),
|
||||
@@ -208,6 +211,7 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
||||
item.HostKeyPolicy,
|
||||
strings.TrimSpace(item.OwnerScope),
|
||||
strings.TrimSpace(item.OwnerUserID),
|
||||
strings.TrimSpace(item.OwnerUserID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID),
|
||||
strings.TrimSpace(item.CreatedBySubjectName),
|
||||
@@ -254,19 +258,19 @@ func (s *Store) DeleteSSHServer(id string) error {
|
||||
var trimmedID string
|
||||
|
||||
trimmedID = strings.TrimSpace(id)
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_access_profiles WHERE server_public_id = ?`, trimmedID)
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_access_profiles WHERE server_id = (SELECT id FROM ssh_servers WHERE public_id = ?)`, trimmedID)
|
||||
err = row.Scan(&count)
|
||||
if err != nil { return err }
|
||||
if count > 0 {
|
||||
return &SSHServerDeleteReferenceError{Kind: "SSH access profile", Count: count}
|
||||
}
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_server_group_members WHERE server_public_id = ?`, trimmedID)
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_server_group_members WHERE server_id = (SELECT id FROM ssh_servers WHERE public_id = ?)`, trimmedID)
|
||||
err = row.Scan(&count)
|
||||
if err != nil { return err }
|
||||
if count > 0 {
|
||||
return &SSHServerDeleteReferenceError{Kind: "SSH server group membership", Count: count}
|
||||
}
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_sessions WHERE server_public_id = ?`, trimmedID)
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_sessions WHERE server_id = (SELECT id FROM ssh_servers WHERE public_id = ?)`, trimmedID)
|
||||
err = row.Scan(&count)
|
||||
if err != nil { return err }
|
||||
if count > 0 {
|
||||
@@ -286,9 +290,9 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
||||
|
||||
rows, err = s.Query(`SELECT
|
||||
p.public_id,
|
||||
p.server_public_id,
|
||||
COALESCE(s.public_id, ''),
|
||||
COALESCE(p.server_target_type, 'server'),
|
||||
COALESCE(p.server_group_public_id, ''),
|
||||
COALESCE(g.public_id, ''),
|
||||
COALESCE(g.name, ''),
|
||||
COALESCE(g.description, ''),
|
||||
COALESCE(g.enabled, 0),
|
||||
@@ -303,15 +307,15 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
||||
p.auth_method,
|
||||
p.second_factor_mode,
|
||||
p.owner_scope,
|
||||
p.owner_user_public_id,
|
||||
COALESCE(pou.public_id, ''),
|
||||
p.allow_user_edit,
|
||||
p.enabled,
|
||||
COALESCE(p.secret_public_id, ''),
|
||||
COALESCE(p.ssh_credential_public_id, ''),
|
||||
COALESCE(sec.public_id, ''),
|
||||
COALESCE(c.public_id, ''),
|
||||
COALESCE(c.name, ''),
|
||||
p.auth_public_key,
|
||||
p.auth_public_key_fingerprint,
|
||||
COALESCE(p.ssh_user_ca_public_id, ''),
|
||||
COALESCE(ca.public_id, ''),
|
||||
p.ssh_principal_grant_ids_json,
|
||||
p.default_cert_valid_seconds,
|
||||
p.max_cert_valid_seconds,
|
||||
@@ -331,16 +335,20 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
||||
s.enabled,
|
||||
s.host_key_policy,
|
||||
s.owner_scope,
|
||||
s.owner_user_public_id,
|
||||
COALESCE(sou.public_id, ''),
|
||||
s.created_by_kind,
|
||||
s.created_by_subject_id,
|
||||
s.created_by_subject_name,
|
||||
s.created_at,
|
||||
s.updated_at
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_servers s ON s.public_id = p.server_public_id
|
||||
LEFT JOIN ssh_server_groups g ON g.public_id = p.server_group_public_id
|
||||
LEFT JOIN ssh_credentials c ON c.public_id = p.ssh_credential_public_id
|
||||
JOIN ssh_servers s ON s.id = p.server_id
|
||||
LEFT JOIN users sou ON sou.id = s.owner_user_id
|
||||
LEFT JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||
LEFT JOIN users pou ON pou.id = p.owner_user_id
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = p.ssh_user_ca_id
|
||||
ORDER BY s.name, p.name`)
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
@@ -430,9 +438,9 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
||||
|
||||
row = s.QueryRow(`SELECT
|
||||
p.public_id,
|
||||
p.server_public_id,
|
||||
COALESCE(s.public_id, ''),
|
||||
COALESCE(p.server_target_type, 'server'),
|
||||
COALESCE(p.server_group_public_id, ''),
|
||||
COALESCE(g.public_id, ''),
|
||||
COALESCE(g.name, ''),
|
||||
COALESCE(g.description, ''),
|
||||
COALESCE(g.enabled, 0),
|
||||
@@ -447,15 +455,15 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
||||
p.auth_method,
|
||||
p.second_factor_mode,
|
||||
p.owner_scope,
|
||||
p.owner_user_public_id,
|
||||
COALESCE(pou.public_id, ''),
|
||||
p.allow_user_edit,
|
||||
p.enabled,
|
||||
COALESCE(p.secret_public_id, ''),
|
||||
COALESCE(p.ssh_credential_public_id, ''),
|
||||
COALESCE(sec.public_id, ''),
|
||||
COALESCE(c.public_id, ''),
|
||||
COALESCE(c.name, ''),
|
||||
p.auth_public_key,
|
||||
p.auth_public_key_fingerprint,
|
||||
COALESCE(p.ssh_user_ca_public_id, ''),
|
||||
COALESCE(ca.public_id, ''),
|
||||
p.ssh_principal_grant_ids_json,
|
||||
p.default_cert_valid_seconds,
|
||||
p.max_cert_valid_seconds,
|
||||
@@ -475,16 +483,20 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
||||
s.enabled,
|
||||
s.host_key_policy,
|
||||
s.owner_scope,
|
||||
s.owner_user_public_id,
|
||||
COALESCE(sou.public_id, ''),
|
||||
s.created_by_kind,
|
||||
s.created_by_subject_id,
|
||||
s.created_by_subject_name,
|
||||
s.created_at,
|
||||
s.updated_at
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_servers s ON s.public_id = p.server_public_id
|
||||
LEFT JOIN ssh_server_groups g ON g.public_id = p.server_group_public_id
|
||||
LEFT JOIN ssh_credentials c ON c.public_id = p.ssh_credential_public_id
|
||||
JOIN ssh_servers s ON s.id = p.server_id
|
||||
LEFT JOIN users sou ON sou.id = s.owner_user_id
|
||||
LEFT JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||
LEFT JOIN users pou ON pou.id = p.owner_user_id
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = p.ssh_user_ca_id
|
||||
WHERE p.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(
|
||||
&item.ID,
|
||||
@@ -634,23 +646,23 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO ssh_access_profiles (
|
||||
public_id,
|
||||
server_public_id,
|
||||
server_id,
|
||||
server_target_type,
|
||||
server_group_public_id,
|
||||
server_group_id,
|
||||
name,
|
||||
description,
|
||||
remote_username,
|
||||
auth_method,
|
||||
second_factor_mode,
|
||||
owner_scope,
|
||||
owner_user_public_id,
|
||||
owner_user_id,
|
||||
allow_user_edit,
|
||||
enabled,
|
||||
secret_public_id,
|
||||
ssh_credential_public_id,
|
||||
secret_id,
|
||||
ssh_credential_id,
|
||||
auth_public_key,
|
||||
auth_public_key_fingerprint,
|
||||
ssh_user_ca_public_id,
|
||||
ssh_user_ca_id,
|
||||
ssh_principal_grant_ids_json,
|
||||
default_cert_valid_seconds,
|
||||
max_cert_valid_seconds,
|
||||
@@ -661,10 +673,11 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
created_by_subject_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, (SELECT id FROM ssh_servers WHERE public_id = ?), ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_server_groups WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_secrets WHERE public_id = ?) END, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_credentials WHERE public_id = ?) END, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_user_cas WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.ServerID),
|
||||
strings.TrimSpace(item.ServerTargetType),
|
||||
strings.TrimSpace(item.ServerGroupID),
|
||||
emptyStringToNil(item.ServerGroupID),
|
||||
strings.TrimSpace(item.Name),
|
||||
strings.TrimSpace(item.Description),
|
||||
@@ -673,12 +686,16 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
strings.TrimSpace(item.SecondFactorMode),
|
||||
strings.TrimSpace(item.OwnerScope),
|
||||
strings.TrimSpace(item.OwnerUserID),
|
||||
strings.TrimSpace(item.OwnerUserID),
|
||||
item.AllowUserEdit,
|
||||
item.Enabled,
|
||||
strings.TrimSpace(item.SecretID),
|
||||
emptyStringToNil(item.SecretID),
|
||||
strings.TrimSpace(item.SSHCredentialID),
|
||||
emptyStringToNil(item.SSHCredentialID),
|
||||
strings.TrimSpace(item.AuthPublicKey),
|
||||
strings.TrimSpace(item.AuthPublicKeyFingerprint),
|
||||
strings.TrimSpace(item.SSHUserCAID),
|
||||
emptyStringToNil(item.SSHUserCAID),
|
||||
grantIDsJSON,
|
||||
item.DefaultCertValidSeconds,
|
||||
@@ -788,10 +805,11 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
item.SecretID = secret.ID
|
||||
}
|
||||
_, err = tx.Exec(`UPDATE ssh_access_profiles
|
||||
SET server_public_id = ?, server_target_type = ?, server_group_public_id = ?, name = ?, description = ?, remote_username = ?, auth_method = ?, second_factor_mode = ?, owner_scope = ?, owner_user_public_id = ?, allow_user_edit = ?, enabled = ?, secret_public_id = ?, ssh_credential_public_id = ?, auth_public_key = ?, auth_public_key_fingerprint = ?, ssh_user_ca_public_id = ?, ssh_principal_grant_ids_json = ?, default_cert_valid_seconds = ?, max_cert_valid_seconds = ?, valid_after = ?, valid_before = ?, updated_at = ?
|
||||
SET server_id = (SELECT id FROM ssh_servers WHERE public_id = ?), server_target_type = ?, server_group_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_server_groups WHERE public_id = ?) END, name = ?, description = ?, remote_username = ?, auth_method = ?, second_factor_mode = ?, owner_scope = ?, owner_user_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, allow_user_edit = ?, enabled = ?, secret_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_secrets WHERE public_id = ?) END, ssh_credential_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_credentials WHERE public_id = ?) END, auth_public_key = ?, auth_public_key_fingerprint = ?, ssh_user_ca_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_user_cas WHERE public_id = ?) END, ssh_principal_grant_ids_json = ?, default_cert_valid_seconds = ?, max_cert_valid_seconds = ?, valid_after = ?, valid_before = ?, updated_at = ?
|
||||
WHERE public_id = ?`,
|
||||
strings.TrimSpace(item.ServerID),
|
||||
strings.TrimSpace(item.ServerTargetType),
|
||||
strings.TrimSpace(item.ServerGroupID),
|
||||
emptyStringToNil(item.ServerGroupID),
|
||||
strings.TrimSpace(item.Name),
|
||||
strings.TrimSpace(item.Description),
|
||||
@@ -800,12 +818,16 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
strings.TrimSpace(item.SecondFactorMode),
|
||||
strings.TrimSpace(item.OwnerScope),
|
||||
strings.TrimSpace(item.OwnerUserID),
|
||||
strings.TrimSpace(item.OwnerUserID),
|
||||
item.AllowUserEdit,
|
||||
item.Enabled,
|
||||
strings.TrimSpace(item.SecretID),
|
||||
emptyStringToNil(item.SecretID),
|
||||
strings.TrimSpace(item.SSHCredentialID),
|
||||
emptyStringToNil(item.SSHCredentialID),
|
||||
strings.TrimSpace(item.AuthPublicKey),
|
||||
strings.TrimSpace(item.AuthPublicKeyFingerprint),
|
||||
strings.TrimSpace(item.SSHUserCAID),
|
||||
emptyStringToNil(item.SSHUserCAID),
|
||||
grantIDsJSON,
|
||||
item.DefaultCertValidSeconds,
|
||||
@@ -819,7 +841,7 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
rollbackIfOwned(tx, owned)
|
||||
return item, err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM ssh_access_profile_targets WHERE profile_public_id = ?`, strings.TrimSpace(item.ID))
|
||||
_, err = tx.Exec(`DELETE FROM ssh_access_profile_targets WHERE profile_id = (SELECT id FROM ssh_access_profiles WHERE public_id = ?)`, strings.TrimSpace(item.ID))
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return item, err
|
||||
@@ -855,7 +877,11 @@ func (s *Store) DeleteSSHAccessProfile(id string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row = tx.QueryRow(`SELECT secret_public_id, ssh_credential_public_id FROM ssh_access_profiles WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
row = tx.QueryRow(`SELECT COALESCE(sec.public_id, ''), COALESCE(c.public_id, '')
|
||||
FROM ssh_access_profiles p
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
WHERE p.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(&secretID, &credentialID)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
@@ -892,9 +918,9 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
trimmedUserID = strings.TrimSpace(userID)
|
||||
rows, err = s.Query(`SELECT DISTINCT
|
||||
p.public_id,
|
||||
p.server_public_id,
|
||||
COALESCE(s.public_id, ''),
|
||||
COALESCE(p.server_target_type, 'server'),
|
||||
COALESCE(p.server_group_public_id, ''),
|
||||
COALESCE(g.public_id, ''),
|
||||
COALESCE(g.name, ''),
|
||||
COALESCE(g.description, ''),
|
||||
COALESCE(g.enabled, 0),
|
||||
@@ -909,15 +935,15 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
p.auth_method,
|
||||
p.second_factor_mode,
|
||||
p.owner_scope,
|
||||
p.owner_user_public_id,
|
||||
COALESCE(pou.public_id, ''),
|
||||
p.allow_user_edit,
|
||||
p.enabled,
|
||||
COALESCE(p.secret_public_id, ''),
|
||||
COALESCE(p.ssh_credential_public_id, ''),
|
||||
COALESCE(sec.public_id, ''),
|
||||
COALESCE(c.public_id, ''),
|
||||
COALESCE(c.name, ''),
|
||||
p.auth_public_key,
|
||||
p.auth_public_key_fingerprint,
|
||||
COALESCE(p.ssh_user_ca_public_id, ''),
|
||||
COALESCE(ca.public_id, ''),
|
||||
p.ssh_principal_grant_ids_json,
|
||||
p.default_cert_valid_seconds,
|
||||
p.max_cert_valid_seconds,
|
||||
@@ -937,22 +963,27 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
s.enabled,
|
||||
s.host_key_policy,
|
||||
s.owner_scope,
|
||||
s.owner_user_public_id,
|
||||
COALESCE(sou.public_id, ''),
|
||||
s.created_by_kind,
|
||||
s.created_by_subject_id,
|
||||
s.created_by_subject_name,
|
||||
s.created_at,
|
||||
s.updated_at
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_servers s ON s.public_id = p.server_public_id
|
||||
LEFT JOIN ssh_server_groups g ON g.public_id = p.server_group_public_id
|
||||
LEFT JOIN ssh_credentials c ON c.public_id = p.ssh_credential_public_id
|
||||
LEFT JOIN ssh_access_profile_targets t ON t.profile_public_id = p.public_id
|
||||
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.public_id = t.target_public_id
|
||||
JOIN ssh_servers s ON s.id = p.server_id
|
||||
LEFT JOIN users sou ON sou.id = s.owner_user_id
|
||||
LEFT JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||
LEFT JOIN users pou ON pou.id = p.owner_user_id
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = p.ssh_user_ca_id
|
||||
LEFT JOIN ssh_access_profile_targets t ON t.profile_id = p.id
|
||||
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.id = t.target_id
|
||||
LEFT JOIN user_group_members gm ON t.target_type = 'group' AND gm.group_id = ug.id
|
||||
LEFT JOIN users gu ON gm.user_id = gu.id
|
||||
LEFT JOIN users tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||
WHERE (
|
||||
(p.owner_scope = 'user' AND p.owner_user_public_id = ?)
|
||||
(p.owner_scope = 'user' AND pou.public_id = ?)
|
||||
OR
|
||||
(
|
||||
p.owner_scope = 'admin_shared'
|
||||
@@ -961,7 +992,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
AND (p.valid_after = 0 OR p.valid_after <= strftime('%s','now'))
|
||||
AND (p.valid_before = 0 OR p.valid_before >= strftime('%s','now'))
|
||||
AND (
|
||||
(t.target_type = 'user' AND t.target_public_id = ?)
|
||||
(t.target_type = 'user' AND tu.public_id = ?)
|
||||
OR
|
||||
(t.target_type = 'group' AND ug.disabled = 0 AND (ug.scope = 'all_users' OR gu.public_id = ?))
|
||||
)
|
||||
@@ -1078,10 +1109,10 @@ func (s *Store) listSSHAccessProfileTargets(profileID string) ([]models.SSHAcces
|
||||
rows, err = s.Query(`SELECT
|
||||
p.public_id,
|
||||
t.target_type,
|
||||
t.target_public_id,
|
||||
CASE WHEN t.target_type = 'user' THEN COALESCE(u.public_id, '') ELSE COALESCE(g.public_id, '') END,
|
||||
CASE
|
||||
WHEN t.target_type = 'user' THEN COALESCE(CASE WHEN u.display_name != '' THEN u.display_name || ' (' || u.username || ')' ELSE u.username END, t.target_public_id)
|
||||
ELSE COALESCE(g.name, t.target_public_id)
|
||||
WHEN t.target_type = 'user' THEN COALESCE(CASE WHEN u.display_name != '' THEN u.display_name || ' (' || u.username || ')' ELSE u.username END, '')
|
||||
ELSE COALESCE(g.name, '')
|
||||
END,
|
||||
CASE
|
||||
WHEN t.target_type = 'user' THEN CASE WHEN u.public_id IS NOT NULL AND u.disabled = 0 THEN 1 ELSE 0 END
|
||||
@@ -1089,11 +1120,11 @@ func (s *Store) listSSHAccessProfileTargets(profileID string) ([]models.SSHAcces
|
||||
END,
|
||||
t.created_at
|
||||
FROM ssh_access_profile_targets t
|
||||
JOIN ssh_access_profiles p ON p.public_id = t.profile_public_id
|
||||
LEFT JOIN users u ON t.target_type = 'user' AND u.public_id = t.target_public_id
|
||||
LEFT JOIN user_groups g ON t.target_type = 'group' AND g.public_id = t.target_public_id
|
||||
JOIN ssh_access_profiles p ON p.id = t.profile_id
|
||||
LEFT JOIN users u ON t.target_type = 'user' AND u.id = t.target_id
|
||||
LEFT JOIN user_groups g ON t.target_type = 'group' AND g.id = t.target_id
|
||||
WHERE p.public_id = ?
|
||||
ORDER BY t.target_type, t.target_public_id`, strings.TrimSpace(profileID))
|
||||
ORDER BY t.target_type, target_id`, strings.TrimSpace(profileID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1132,9 +1163,10 @@ func (s *Store) ListSSHServerHostKeys(serverID string) ([]models.SSHServerHostKe
|
||||
var item models.SSHServerHostKey
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT public_id, server_public_id, algorithm, public_key, fingerprint, created_at
|
||||
FROM ssh_server_host_keys
|
||||
WHERE server_public_id = ?
|
||||
rows, err = s.Query(`SELECT hk.public_id, s.public_id, hk.algorithm, hk.public_key, hk.fingerprint, hk.created_at
|
||||
FROM ssh_server_host_keys hk
|
||||
JOIN ssh_servers s ON s.id = hk.server_id
|
||||
WHERE s.public_id = ?
|
||||
ORDER BY created_at, fingerprint`, strings.TrimSpace(serverID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1166,8 +1198,8 @@ func (s *Store) CreateSSHServerHostKey(item models.SSHServerHostKey) (models.SSH
|
||||
}
|
||||
now = time.Now().UTC().Unix()
|
||||
item.CreatedAt = now
|
||||
_, err = s.Exec(`INSERT INTO ssh_server_host_keys (public_id, server_public_id, algorithm, public_key, fingerprint, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
_, err = s.Exec(`INSERT INTO ssh_server_host_keys (public_id, server_id, algorithm, public_key, fingerprint, created_at)
|
||||
VALUES (?, (SELECT id FROM ssh_servers WHERE public_id = ?), ?, ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.ServerID),
|
||||
strings.TrimSpace(item.Algorithm),
|
||||
@@ -1184,7 +1216,7 @@ func (s *Store) CreateSSHServerHostKey(item models.SSHServerHostKey) (models.SSH
|
||||
func (s *Store) DeleteSSHServerHostKey(serverID string, hostKeyID string) error {
|
||||
var err error
|
||||
|
||||
_, err = s.Exec(`DELETE FROM ssh_server_host_keys WHERE server_public_id = ? AND public_id = ?`, strings.TrimSpace(serverID), strings.TrimSpace(hostKeyID))
|
||||
_, err = s.Exec(`DELETE FROM ssh_server_host_keys WHERE server_id = (SELECT id FROM ssh_servers WHERE public_id = ?) AND public_id = ?`, strings.TrimSpace(serverID), strings.TrimSpace(hostKeyID))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1214,9 +1246,9 @@ func (s *Store) CreateSSHSession(item models.SSHSession) (models.SSHSession, err
|
||||
item.StartedAt = now
|
||||
_, err = s.Exec(`INSERT INTO ssh_sessions (
|
||||
public_id,
|
||||
profile_public_id,
|
||||
server_public_id,
|
||||
user_public_id,
|
||||
profile_id,
|
||||
server_id,
|
||||
user_id,
|
||||
username,
|
||||
remote_username,
|
||||
auth_method,
|
||||
@@ -1234,7 +1266,7 @@ func (s *Store) CreateSSHSession(item models.SSHSession) (models.SSHSession, err
|
||||
remote_addr,
|
||||
user_agent,
|
||||
error
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, (SELECT id FROM ssh_access_profiles WHERE public_id = ?), (SELECT id FROM ssh_servers WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.ProfileID),
|
||||
strings.TrimSpace(item.ServerID),
|
||||
@@ -1283,7 +1315,7 @@ func (s *Store) CountPendingSSHSessionsForUser(userID string) (int, error) {
|
||||
|
||||
row = s.QueryRow(`SELECT COUNT(*)
|
||||
FROM ssh_sessions
|
||||
WHERE user_public_id = ? AND status = 'pending'`, strings.TrimSpace(userID))
|
||||
WHERE user_id = (SELECT id FROM users WHERE public_id = ?) AND status = 'pending'`, strings.TrimSpace(userID))
|
||||
err = row.Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -1296,9 +1328,11 @@ func (s *Store) GetSSHSession(id string) (models.SSHSession, error) {
|
||||
var item models.SSHSession
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT ss.public_id, ss.profile_public_id, ss.server_public_id, COALESCE(s.name, ''), ss.user_public_id, ss.username, ss.remote_username, ss.auth_method, ss.second_factor_mode, ss.host, ss.port, ss.status, ss.host_key_fingerprint, ss.requested_term, ss.requested_cols, ss.requested_rows, ss.started_at, ss.connected_at, ss.ended_at, ss.remote_addr, ss.user_agent, ss.error
|
||||
row = s.QueryRow(`SELECT ss.public_id, COALESCE(p.public_id, ''), COALESCE(s.public_id, ''), COALESCE(s.name, ''), COALESCE(u.public_id, ''), ss.username, ss.remote_username, ss.auth_method, ss.second_factor_mode, ss.host, ss.port, ss.status, ss.host_key_fingerprint, ss.requested_term, ss.requested_cols, ss.requested_rows, ss.started_at, ss.connected_at, ss.ended_at, ss.remote_addr, ss.user_agent, ss.error
|
||||
FROM ssh_sessions ss
|
||||
LEFT JOIN ssh_servers s ON s.public_id = ss.server_public_id
|
||||
LEFT JOIN ssh_access_profiles p ON p.id = ss.profile_id
|
||||
LEFT JOIN ssh_servers s ON s.id = ss.server_id
|
||||
LEFT JOIN users u ON u.id = ss.user_id
|
||||
WHERE ss.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(&item.ID, &item.ProfileID, &item.ServerID, &item.ServerName, &item.UserID, &item.Username, &item.RemoteUsername, &item.AuthMethod, &item.SecondFactorMode, &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 {
|
||||
@@ -1341,20 +1375,22 @@ func (s *Store) ListSSHSessionsForUserFiltered(userID string, limit int, offset
|
||||
}
|
||||
query = strings.TrimSpace(query)
|
||||
status = strings.ToLower(strings.TrimSpace(status))
|
||||
whereParts = append(whereParts, "ss.user_public_id = ?")
|
||||
whereParts = append(whereParts, "u.public_id = ?")
|
||||
args = append(args, strings.TrimSpace(userID))
|
||||
if query != "" {
|
||||
like = "%" + query + "%"
|
||||
whereParts = append(whereParts, "(ss.public_id LIKE ? OR ss.profile_public_id LIKE ? OR ss.server_public_id LIKE ? OR COALESCE(s.name, '') LIKE ? OR ss.user_public_id LIKE ? OR ss.username LIKE ? OR ss.remote_username LIKE ? OR ss.host LIKE ? OR ss.auth_method LIKE ? OR ss.status LIKE ? OR ss.host_key_fingerprint LIKE ? OR ss.error LIKE ?)")
|
||||
whereParts = append(whereParts, "(ss.public_id LIKE ? OR COALESCE(p.public_id, '') LIKE ? OR COALESCE(s.public_id, '') LIKE ? OR COALESCE(s.name, '') LIKE ? OR COALESCE(u.public_id, '') LIKE ? OR ss.username LIKE ? OR ss.remote_username LIKE ? OR ss.host LIKE ? OR ss.auth_method LIKE ? OR ss.status LIKE ? OR ss.host_key_fingerprint LIKE ? OR ss.error LIKE ?)")
|
||||
args = append(args, like, like, like, like, like, like, like, like, like, like, like, like)
|
||||
}
|
||||
if status != "" {
|
||||
whereParts = append(whereParts, "ss.status = ?")
|
||||
args = append(args, status)
|
||||
}
|
||||
sqlQuery = fmt.Sprintf(`SELECT ss.public_id, ss.profile_public_id, ss.server_public_id, COALESCE(s.name, ''), ss.user_public_id, ss.username, ss.remote_username, ss.auth_method, ss.second_factor_mode, ss.host, ss.port, ss.status, ss.host_key_fingerprint, ss.requested_term, ss.requested_cols, ss.requested_rows, ss.started_at, ss.connected_at, ss.ended_at, ss.remote_addr, ss.user_agent, ss.error
|
||||
sqlQuery = fmt.Sprintf(`SELECT ss.public_id, COALESCE(p.public_id, ''), COALESCE(s.public_id, ''), COALESCE(s.name, ''), COALESCE(u.public_id, ''), ss.username, ss.remote_username, ss.auth_method, ss.second_factor_mode, ss.host, ss.port, ss.status, ss.host_key_fingerprint, ss.requested_term, ss.requested_cols, ss.requested_rows, ss.started_at, ss.connected_at, ss.ended_at, ss.remote_addr, ss.user_agent, ss.error
|
||||
FROM ssh_sessions ss
|
||||
LEFT JOIN ssh_servers s ON s.public_id = ss.server_public_id
|
||||
LEFT JOIN ssh_access_profiles p ON p.id = ss.profile_id
|
||||
LEFT JOIN ssh_servers s ON s.id = ss.server_id
|
||||
LEFT JOIN users u ON u.id = ss.user_id
|
||||
WHERE %s
|
||||
ORDER BY ss.started_at DESC
|
||||
LIMIT ? OFFSET ?`, strings.Join(whereParts, " AND "))
|
||||
@@ -1405,16 +1441,18 @@ func (s *Store) ListSSHSessionsFiltered(limit int, offset int, query string, sta
|
||||
status = strings.ToLower(strings.TrimSpace(status))
|
||||
if query != "" {
|
||||
like = "%" + query + "%"
|
||||
whereParts = append(whereParts, "(ss.public_id LIKE ? OR ss.profile_public_id LIKE ? OR ss.server_public_id LIKE ? OR COALESCE(s.name, '') LIKE ? OR ss.user_public_id LIKE ? OR ss.username LIKE ? OR ss.remote_username LIKE ? OR ss.host LIKE ? OR ss.auth_method LIKE ? OR ss.status LIKE ? OR ss.host_key_fingerprint LIKE ? OR ss.error LIKE ?)")
|
||||
whereParts = append(whereParts, "(ss.public_id LIKE ? OR COALESCE(p.public_id, '') LIKE ? OR COALESCE(s.public_id, '') LIKE ? OR COALESCE(s.name, '') LIKE ? OR COALESCE(u.public_id, '') LIKE ? OR ss.username LIKE ? OR ss.remote_username LIKE ? OR ss.host LIKE ? OR ss.auth_method LIKE ? OR ss.status LIKE ? OR ss.host_key_fingerprint LIKE ? OR ss.error LIKE ?)")
|
||||
args = append(args, like, like, like, like, like, like, like, like, like, like, like, like)
|
||||
}
|
||||
if status != "" {
|
||||
whereParts = append(whereParts, "ss.status = ?")
|
||||
args = append(args, status)
|
||||
}
|
||||
sqlQuery = `SELECT ss.public_id, ss.profile_public_id, ss.server_public_id, COALESCE(s.name, ''), ss.user_public_id, ss.username, ss.remote_username, ss.auth_method, ss.second_factor_mode, ss.host, ss.port, ss.status, ss.host_key_fingerprint, ss.requested_term, ss.requested_cols, ss.requested_rows, ss.started_at, ss.connected_at, ss.ended_at, ss.remote_addr, ss.user_agent, ss.error
|
||||
sqlQuery = `SELECT ss.public_id, COALESCE(p.public_id, ''), COALESCE(s.public_id, ''), COALESCE(s.name, ''), COALESCE(u.public_id, ''), ss.username, ss.remote_username, ss.auth_method, ss.second_factor_mode, ss.host, ss.port, ss.status, ss.host_key_fingerprint, ss.requested_term, ss.requested_cols, ss.requested_rows, ss.started_at, ss.connected_at, ss.ended_at, ss.remote_addr, ss.user_agent, ss.error
|
||||
FROM ssh_sessions ss
|
||||
LEFT JOIN ssh_servers s ON s.public_id = ss.server_public_id`
|
||||
LEFT JOIN ssh_access_profiles p ON p.id = ss.profile_id
|
||||
LEFT JOIN ssh_servers s ON s.id = ss.server_id
|
||||
LEFT JOIN users u ON u.id = ss.user_id`
|
||||
if len(whereParts) > 0 {
|
||||
sqlQuery += "\n\t\tWHERE " + strings.Join(whereParts, " AND ")
|
||||
}
|
||||
@@ -1459,6 +1497,257 @@ func (s *Store) UpdateSSHSessionStatus(id string, status string, hostKeyFingerpr
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) CreateSSHFileTransfer(item models.SSHFileTransfer) (models.SSHFileTransfer, error) {
|
||||
var pathsJSON string
|
||||
var now int64
|
||||
var err error
|
||||
|
||||
if strings.TrimSpace(item.ID) == "" {
|
||||
item.ID, err = util.NewID()
|
||||
if err != nil { return item, err }
|
||||
}
|
||||
pathsJSON, err = encodeStringList(item.Paths)
|
||||
if err != nil { return item, err }
|
||||
now = time.Now().UTC().Unix()
|
||||
if item.StartedAt <= 0 {
|
||||
item.StartedAt = now
|
||||
}
|
||||
if strings.TrimSpace(item.Status) == "" {
|
||||
item.Status = "running"
|
||||
}
|
||||
_, err = s.Exec(`INSERT INTO ssh_file_transfers (
|
||||
public_id,
|
||||
session_id,
|
||||
user_id,
|
||||
username,
|
||||
profile_id,
|
||||
server_id,
|
||||
server_name,
|
||||
remote_username,
|
||||
operation,
|
||||
source_session_id,
|
||||
target_session_id,
|
||||
target_server_id,
|
||||
target_server_name,
|
||||
target_dir,
|
||||
paths_json,
|
||||
bytes_transferred,
|
||||
status,
|
||||
error,
|
||||
started_at,
|
||||
finished_at,
|
||||
duration_ms,
|
||||
remote_addr,
|
||||
user_agent
|
||||
) VALUES (?, (SELECT id FROM ssh_sessions WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, (SELECT id FROM ssh_access_profiles WHERE public_id = ?), (SELECT id FROM ssh_servers WHERE public_id = ?), ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_sessions WHERE public_id = ?) END, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_sessions WHERE public_id = ?) END, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_servers WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
strings.TrimSpace(item.ID),
|
||||
strings.TrimSpace(item.SessionID),
|
||||
strings.TrimSpace(item.UserID),
|
||||
strings.TrimSpace(item.Username),
|
||||
strings.TrimSpace(item.ProfileID),
|
||||
strings.TrimSpace(item.ServerID),
|
||||
strings.TrimSpace(item.ServerName),
|
||||
strings.TrimSpace(item.RemoteUsername),
|
||||
strings.TrimSpace(item.Operation),
|
||||
strings.TrimSpace(item.SourceSessionID),
|
||||
strings.TrimSpace(item.SourceSessionID),
|
||||
strings.TrimSpace(item.TargetSessionID),
|
||||
strings.TrimSpace(item.TargetSessionID),
|
||||
strings.TrimSpace(item.TargetServerID),
|
||||
strings.TrimSpace(item.TargetServerID),
|
||||
strings.TrimSpace(item.TargetServerName),
|
||||
strings.TrimSpace(item.TargetDir),
|
||||
pathsJSON,
|
||||
item.BytesTransferred,
|
||||
strings.TrimSpace(item.Status),
|
||||
strings.TrimSpace(item.Error),
|
||||
item.StartedAt,
|
||||
item.FinishedAt,
|
||||
item.DurationMS,
|
||||
strings.TrimSpace(item.RemoteAddr),
|
||||
strings.TrimSpace(item.UserAgent),
|
||||
)
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Store) FinishSSHFileTransfer(id string, status string, paths []string, bytesTransferred int64, errText string) error {
|
||||
var row *sql.Row
|
||||
var startedAt int64
|
||||
var finishedAt int64
|
||||
var durationMS int64
|
||||
var pathsJSON string
|
||||
var err error
|
||||
|
||||
pathsJSON, err = encodeStringList(paths)
|
||||
if err != nil { return err }
|
||||
finishedAt = time.Now().UTC().Unix()
|
||||
row = s.QueryRow(`SELECT started_at FROM ssh_file_transfers WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(&startedAt)
|
||||
if err != nil { return err }
|
||||
durationMS = 0
|
||||
if startedAt > 0 && finishedAt >= startedAt {
|
||||
durationMS = (finishedAt - startedAt) * 1000
|
||||
}
|
||||
_, err = s.Exec(`UPDATE ssh_file_transfers
|
||||
SET status = ?, paths_json = ?, bytes_transferred = ?, error = ?, finished_at = ?, duration_ms = ?
|
||||
WHERE public_id = ?`,
|
||||
strings.TrimSpace(status),
|
||||
pathsJSON,
|
||||
bytesTransferred,
|
||||
strings.TrimSpace(errText),
|
||||
finishedAt,
|
||||
durationMS,
|
||||
strings.TrimSpace(id),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func scanSSHFileTransferRows(rows *sql.Rows, limit int) ([]models.SSHFileTransfer, bool, error) {
|
||||
var items []models.SSHFileTransfer
|
||||
var item models.SSHFileTransfer
|
||||
var pathsJSON string
|
||||
var err error
|
||||
|
||||
for rows.Next() {
|
||||
err = rows.Scan(
|
||||
&item.ID,
|
||||
&item.SessionID,
|
||||
&item.UserID,
|
||||
&item.Username,
|
||||
&item.ProfileID,
|
||||
&item.ServerID,
|
||||
&item.ServerName,
|
||||
&item.RemoteUsername,
|
||||
&item.Operation,
|
||||
&item.SourceSessionID,
|
||||
&item.TargetSessionID,
|
||||
&item.TargetServerID,
|
||||
&item.TargetServerName,
|
||||
&item.TargetDir,
|
||||
&pathsJSON,
|
||||
&item.BytesTransferred,
|
||||
&item.Status,
|
||||
&item.Error,
|
||||
&item.StartedAt,
|
||||
&item.FinishedAt,
|
||||
&item.DurationMS,
|
||||
&item.RemoteAddr,
|
||||
&item.UserAgent,
|
||||
)
|
||||
if err != nil { return nil, false, err }
|
||||
item.Paths, err = decodeStringList(pathsJSON)
|
||||
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) ListSSHFileTransfersForUserFiltered(userID string, limit int, offset int, query string, status string, sessionID string) ([]models.SSHFileTransfer, bool, error) {
|
||||
var rows *sql.Rows
|
||||
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))
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
whereParts = append(whereParts, "u.public_id = ?")
|
||||
args = append(args, strings.TrimSpace(userID))
|
||||
if sessionID != "" {
|
||||
whereParts = append(whereParts, "sess.public_id = ?")
|
||||
args = append(args, sessionID)
|
||||
}
|
||||
if query != "" {
|
||||
like = "%" + query + "%"
|
||||
whereParts = append(whereParts, "(ft.public_id LIKE ? OR COALESCE(sess.public_id, '') LIKE ? OR COALESCE(p.public_id, '') LIKE ? OR COALESCE(s.public_id, '') LIKE ? OR ft.server_name LIKE ? OR ft.username LIKE ? OR ft.remote_username LIKE ? OR ft.operation LIKE ? OR COALESCE(tsess.public_id, '') LIKE ? OR COALESCE(ts.public_id, '') LIKE ? OR ft.target_server_name LIKE ? OR ft.target_dir LIKE ? OR ft.paths_json LIKE ? OR ft.status LIKE ? OR ft.error LIKE ?)")
|
||||
args = append(args, like, like, like, like, like, like, like, like, like, like, like, like, like, like, like)
|
||||
}
|
||||
if status != "" {
|
||||
whereParts = append(whereParts, "ft.status = ?")
|
||||
args = append(args, status)
|
||||
}
|
||||
sqlQuery = fmt.Sprintf(`SELECT ft.public_id, COALESCE(sess.public_id, ''), COALESCE(u.public_id, ''), ft.username, COALESCE(p.public_id, ''), COALESCE(s.public_id, ''), ft.server_name, ft.remote_username, ft.operation, COALESCE(ssess.public_id, ''), COALESCE(tsess.public_id, ''), COALESCE(ts.public_id, ''), ft.target_server_name, ft.target_dir, ft.paths_json, ft.bytes_transferred, ft.status, ft.error, ft.started_at, ft.finished_at, ft.duration_ms, ft.remote_addr, ft.user_agent
|
||||
FROM ssh_file_transfers ft
|
||||
LEFT JOIN ssh_sessions sess ON sess.id = ft.session_id
|
||||
LEFT JOIN users u ON u.id = ft.user_id
|
||||
LEFT JOIN ssh_access_profiles p ON p.id = ft.profile_id
|
||||
LEFT JOIN ssh_servers s ON s.id = ft.server_id
|
||||
LEFT JOIN ssh_sessions ssess ON ssess.id = ft.source_session_id
|
||||
LEFT JOIN ssh_sessions tsess ON tsess.id = ft.target_session_id
|
||||
LEFT JOIN ssh_servers ts ON ts.id = ft.target_server_id
|
||||
WHERE %s
|
||||
ORDER BY ft.started_at DESC, ft.id DESC
|
||||
LIMIT ? OFFSET ?`, strings.Join(whereParts, " AND "))
|
||||
args = append(args, limit + 1, offset)
|
||||
rows, err = s.Query(sqlQuery, args...)
|
||||
if err != nil { return nil, false, err }
|
||||
defer rows.Close()
|
||||
return scanSSHFileTransferRows(rows, limit)
|
||||
}
|
||||
|
||||
func (s *Store) ListSSHFileTransfersFiltered(limit int, offset int, query string, status string, sessionID string, userID string) ([]models.SSHFileTransfer, bool, error) {
|
||||
var rows *sql.Rows
|
||||
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))
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
userID = strings.TrimSpace(userID)
|
||||
if sessionID != "" {
|
||||
whereParts = append(whereParts, "sess.public_id = ?")
|
||||
args = append(args, sessionID)
|
||||
}
|
||||
if userID != "" {
|
||||
whereParts = append(whereParts, "u.public_id = ?")
|
||||
args = append(args, userID)
|
||||
}
|
||||
if query != "" {
|
||||
like = "%" + query + "%"
|
||||
whereParts = append(whereParts, "(ft.public_id LIKE ? OR COALESCE(sess.public_id, '') LIKE ? OR COALESCE(u.public_id, '') LIKE ? OR ft.username LIKE ? OR COALESCE(p.public_id, '') LIKE ? OR COALESCE(s.public_id, '') LIKE ? OR ft.server_name LIKE ? OR ft.remote_username LIKE ? OR ft.operation LIKE ? OR COALESCE(tsess.public_id, '') LIKE ? OR COALESCE(ts.public_id, '') LIKE ? OR ft.target_server_name LIKE ? OR ft.target_dir LIKE ? OR ft.paths_json LIKE ? OR ft.status LIKE ? OR ft.error LIKE ?)")
|
||||
args = append(args, like, like, like, like, like, like, like, like, like, like, like, like, like, like, like, like)
|
||||
}
|
||||
if status != "" {
|
||||
whereParts = append(whereParts, "ft.status = ?")
|
||||
args = append(args, status)
|
||||
}
|
||||
sqlQuery = `SELECT ft.public_id, COALESCE(sess.public_id, ''), COALESCE(u.public_id, ''), ft.username, COALESCE(p.public_id, ''), COALESCE(s.public_id, ''), ft.server_name, ft.remote_username, ft.operation, COALESCE(ssess.public_id, ''), COALESCE(tsess.public_id, ''), COALESCE(ts.public_id, ''), ft.target_server_name, ft.target_dir, ft.paths_json, ft.bytes_transferred, ft.status, ft.error, ft.started_at, ft.finished_at, ft.duration_ms, ft.remote_addr, ft.user_agent
|
||||
FROM ssh_file_transfers ft
|
||||
LEFT JOIN ssh_sessions sess ON sess.id = ft.session_id
|
||||
LEFT JOIN users u ON u.id = ft.user_id
|
||||
LEFT JOIN ssh_access_profiles p ON p.id = ft.profile_id
|
||||
LEFT JOIN ssh_servers s ON s.id = ft.server_id
|
||||
LEFT JOIN ssh_sessions ssess ON ssess.id = ft.source_session_id
|
||||
LEFT JOIN ssh_sessions tsess ON tsess.id = ft.target_session_id
|
||||
LEFT JOIN ssh_servers ts ON ts.id = ft.target_server_id`
|
||||
if len(whereParts) > 0 {
|
||||
sqlQuery += "\n\t\tWHERE " + strings.Join(whereParts, " AND ")
|
||||
}
|
||||
sqlQuery += "\n\t\tORDER BY ft.started_at DESC, ft.id 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()
|
||||
return scanSSHFileTransferRows(rows, limit)
|
||||
}
|
||||
|
||||
func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, error) {
|
||||
var err error
|
||||
var now int64
|
||||
@@ -1478,8 +1767,6 @@ func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, err
|
||||
payload,
|
||||
password,
|
||||
metadata_json,
|
||||
owner_scope,
|
||||
owner_user_public_id,
|
||||
created_by_kind,
|
||||
created_by_subject_id,
|
||||
created_by_subject_name,
|
||||
@@ -1541,14 +1828,17 @@ func insertSSHAccessProfileTargetTx(tx *sql.Tx, profileID string, targetType str
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO ssh_access_profile_targets (
|
||||
public_id,
|
||||
profile_public_id,
|
||||
profile_id,
|
||||
target_type,
|
||||
target_public_id,
|
||||
target_id,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, (SELECT id FROM ssh_access_profiles WHERE public_id = ?), ?, CASE WHEN ? = 'user' THEN (SELECT id FROM users WHERE public_id = ?) WHEN ? = 'group' THEN (SELECT id FROM user_groups WHERE public_id = ?) ELSE NULL END, ?)`,
|
||||
id,
|
||||
strings.TrimSpace(profileID),
|
||||
targetType,
|
||||
targetType,
|
||||
targetID,
|
||||
targetType,
|
||||
targetID,
|
||||
createdAt,
|
||||
)
|
||||
|
||||
@@ -31,8 +31,10 @@ func (s *Store) ListSSHCredentials() ([]models.SSHCredential, error) {
|
||||
var item models.SSHCredential
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT public_id, name, description, type, secret_public_id, public_key, fingerprint, enabled, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
|
||||
FROM ssh_credentials
|
||||
rows, err = s.Query(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.created_at, c.updated_at
|
||||
FROM ssh_credentials c
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = c.secret_id
|
||||
LEFT JOIN users u ON u.id = c.owner_user_id
|
||||
WHERE owner_scope = 'admin'
|
||||
ORDER BY name`)
|
||||
if err != nil { return nil, err }
|
||||
@@ -53,10 +55,12 @@ func (s *Store) ListSSHCredentialsForUser(userID string) ([]models.SSHCredential
|
||||
var item models.SSHCredential
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT public_id, name, description, type, secret_public_id, public_key, fingerprint, enabled, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
|
||||
FROM ssh_credentials
|
||||
WHERE owner_scope = 'user' AND owner_user_public_id = ?
|
||||
ORDER BY name`, strings.TrimSpace(userID))
|
||||
rows, err = s.Query(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.created_at, c.updated_at
|
||||
FROM ssh_credentials c
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = c.secret_id
|
||||
LEFT JOIN users u ON u.id = c.owner_user_id
|
||||
WHERE c.owner_scope = 'user' AND u.public_id = ?
|
||||
ORDER BY c.name`, strings.TrimSpace(userID))
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
@@ -74,9 +78,11 @@ func (s *Store) GetSSHCredential(id string) (models.SSHCredential, error) {
|
||||
var item models.SSHCredential
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT public_id, name, description, type, secret_public_id, public_key, fingerprint, enabled, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
|
||||
FROM ssh_credentials
|
||||
WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
row = s.QueryRow(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.created_at, c.updated_at
|
||||
FROM ssh_credentials c
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = c.secret_id
|
||||
LEFT JOIN users u ON u.id = c.owner_user_id
|
||||
WHERE c.public_id = ?`, strings.TrimSpace(id))
|
||||
err = scanSSHCredential(row, &item)
|
||||
return item, err
|
||||
}
|
||||
@@ -86,9 +92,11 @@ func (s *Store) GetSSHCredentialForUser(userID string, id string) (models.SSHCre
|
||||
var item models.SSHCredential
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT public_id, name, description, type, secret_public_id, public_key, fingerprint, enabled, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at
|
||||
FROM ssh_credentials
|
||||
WHERE public_id = ? AND owner_scope = 'user' AND owner_user_public_id = ?`, strings.TrimSpace(id), strings.TrimSpace(userID))
|
||||
row = s.QueryRow(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.created_at, c.updated_at
|
||||
FROM ssh_credentials c
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = c.secret_id
|
||||
LEFT JOIN users u ON u.id = c.owner_user_id
|
||||
WHERE c.public_id = ? AND c.owner_scope = 'user' AND u.public_id = ?`, strings.TrimSpace(id), strings.TrimSpace(userID))
|
||||
err = scanSSHCredential(row, &item)
|
||||
return item, err
|
||||
}
|
||||
@@ -123,8 +131,8 @@ func (s *Store) CreateSSHCredential(item models.SSHCredential, secret models.SSH
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
item.SecretID = secret.ID
|
||||
_, err = tx.Exec(`INSERT INTO ssh_credentials (public_id, name, description, type, secret_public_id, public_key, fingerprint, enabled, owner_scope, owner_user_public_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
_, err = tx.Exec(`INSERT INTO ssh_credentials (public_id, name, description, type, secret_id, public_key, fingerprint, enabled, owner_scope, owner_user_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, (SELECT id FROM ssh_secrets WHERE public_id = ?), ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.Name),
|
||||
strings.TrimSpace(item.Description),
|
||||
@@ -174,7 +182,10 @@ func (s *Store) DeleteSSHCredential(id string) error {
|
||||
var err error
|
||||
|
||||
id = strings.TrimSpace(id)
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_access_profiles WHERE ssh_credential_public_id = ?`, id)
|
||||
row = s.QueryRow(`SELECT COUNT(*)
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
WHERE c.public_id = ?`, id)
|
||||
err = row.Scan(&count)
|
||||
if err != nil { return err }
|
||||
if count > 0 { return errors.New("ssh credential is used by access profiles") }
|
||||
|
||||
@@ -29,14 +29,18 @@ func (s *Store) ListSSHPrincipalGrants(targetType string, targetID string) ([]mo
|
||||
g.used_count,
|
||||
g.disabled,
|
||||
g.reason,
|
||||
COALESCE(g.created_by_user_public_id, ''),
|
||||
COALESCE(cu.public_id, ''),
|
||||
g.created_at,
|
||||
g.updated_at,
|
||||
g.last_used_at
|
||||
FROM ssh_principal_grants g
|
||||
JOIN ssh_principal_grant_targets t ON t.grant_public_id = g.public_id
|
||||
WHERE t.target_type = ? AND t.target_public_id = ?
|
||||
ORDER BY g.principal, g.created_at`, targetType, targetID)
|
||||
LEFT JOIN users cu ON cu.id = g.created_by_user_id
|
||||
JOIN ssh_principal_grant_targets t ON t.grant_id = g.id
|
||||
LEFT JOIN users tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||
LEFT JOIN user_groups tg ON t.target_type = 'group' AND tg.id = t.target_id
|
||||
WHERE t.target_type = ?
|
||||
AND ((t.target_type = 'user' AND tu.public_id = ?) OR (t.target_type = 'group' AND tg.public_id = ?))
|
||||
ORDER BY g.principal, g.created_at`, targetType, targetID, targetID)
|
||||
} else {
|
||||
rows, err = s.Query(`SELECT
|
||||
g.public_id,
|
||||
@@ -49,11 +53,12 @@ func (s *Store) ListSSHPrincipalGrants(targetType string, targetID string) ([]mo
|
||||
g.used_count,
|
||||
g.disabled,
|
||||
g.reason,
|
||||
COALESCE(g.created_by_user_public_id, ''),
|
||||
COALESCE(cu.public_id, ''),
|
||||
g.created_at,
|
||||
g.updated_at,
|
||||
g.last_used_at
|
||||
FROM ssh_principal_grants g
|
||||
LEFT JOIN users cu ON cu.id = g.created_by_user_id
|
||||
ORDER BY g.principal, g.created_at`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -114,21 +119,23 @@ func (s *Store) ListActiveSSHPrincipalGrantsForUser(userID string, now int64) ([
|
||||
g.used_count,
|
||||
g.disabled,
|
||||
g.reason,
|
||||
COALESCE(g.created_by_user_public_id, ''),
|
||||
COALESCE(cu.public_id, ''),
|
||||
g.created_at,
|
||||
g.updated_at,
|
||||
g.last_used_at
|
||||
FROM ssh_principal_grants g
|
||||
JOIN ssh_principal_grant_targets t ON t.grant_public_id = g.public_id
|
||||
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.public_id = t.target_public_id
|
||||
LEFT JOIN users cu ON cu.id = g.created_by_user_id
|
||||
JOIN ssh_principal_grant_targets t ON t.grant_id = g.id
|
||||
LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.id = t.target_id
|
||||
LEFT JOIN user_group_members gm ON t.target_type = 'group' AND gm.group_id = ug.id
|
||||
LEFT JOIN users gu ON gm.user_id = gu.id
|
||||
LEFT JOIN users tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||
WHERE g.disabled = 0
|
||||
AND (g.valid_after = 0 OR g.valid_after <= ?)
|
||||
AND (g.valid_before = 0 OR g.valid_before >= ?)
|
||||
AND (g.max_uses = 0 OR g.used_count < g.max_uses)
|
||||
AND (
|
||||
(t.target_type = 'user' AND t.target_public_id = ?)
|
||||
(t.target_type = 'user' AND tu.public_id = ?)
|
||||
OR
|
||||
(t.target_type = 'group' AND ug.disabled = 0 AND (ug.scope = 'all_users' OR gu.public_id = ?))
|
||||
)
|
||||
@@ -195,11 +202,12 @@ func (s *Store) GetSSHPrincipalGrant(id string) (models.SSHPrincipalGrant, error
|
||||
g.used_count,
|
||||
g.disabled,
|
||||
g.reason,
|
||||
COALESCE(g.created_by_user_public_id, ''),
|
||||
COALESCE(cu.public_id, ''),
|
||||
g.created_at,
|
||||
g.updated_at,
|
||||
g.last_used_at
|
||||
FROM ssh_principal_grants g
|
||||
LEFT JOIN users cu ON cu.id = g.created_by_user_id
|
||||
WHERE g.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(
|
||||
&item.ID,
|
||||
@@ -283,11 +291,11 @@ func (s *Store) CreateSSHPrincipalGrant(item models.SSHPrincipalGrant) (models.S
|
||||
used_count,
|
||||
disabled,
|
||||
reason,
|
||||
created_by_user_public_id,
|
||||
created_by_user_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
last_used_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, ?, ?, ?)`,
|
||||
strings.TrimSpace(item.ID),
|
||||
strings.TrimSpace(item.Name),
|
||||
principalsJSON,
|
||||
@@ -298,7 +306,8 @@ func (s *Store) CreateSSHPrincipalGrant(item models.SSHPrincipalGrant) (models.S
|
||||
item.UsedCount,
|
||||
item.Disabled,
|
||||
strings.TrimSpace(item.Reason),
|
||||
nullableTrimmedString(item.CreatedByUserID),
|
||||
strings.TrimSpace(item.CreatedByUserID),
|
||||
strings.TrimSpace(item.CreatedByUserID),
|
||||
item.CreatedAt,
|
||||
item.UpdatedAt,
|
||||
item.LastUsedAt,
|
||||
@@ -388,7 +397,7 @@ func (s *Store) UpdateSSHPrincipalGrant(item models.SSHPrincipalGrant) (models.S
|
||||
rollbackIfOwned(tx, owned)
|
||||
return item, err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM ssh_principal_grant_targets WHERE grant_public_id = ?`, strings.TrimSpace(item.ID))
|
||||
_, err = tx.Exec(`DELETE FROM ssh_principal_grant_targets WHERE grant_id = (SELECT id FROM ssh_principal_grants WHERE public_id = ?)`, strings.TrimSpace(item.ID))
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return item, err
|
||||
@@ -482,9 +491,9 @@ func (s *Store) listSSHPrincipalGrantTargets(grantID string) ([]models.SSHPrinci
|
||||
var items []models.SSHPrincipalGrantTarget
|
||||
var item models.SSHPrincipalGrantTarget
|
||||
rows, err = s.Query(`SELECT
|
||||
t.grant_public_id,
|
||||
gnt.public_id,
|
||||
t.target_type,
|
||||
t.target_public_id,
|
||||
CASE WHEN t.target_type = 'user' THEN COALESCE(u.public_id, '') ELSE COALESCE(g.public_id, '') END,
|
||||
CASE
|
||||
WHEN t.target_type = 'user' THEN COALESCE(u.username, '')
|
||||
WHEN t.target_type = 'group' THEN COALESCE(g.name, '')
|
||||
@@ -497,10 +506,11 @@ func (s *Store) listSSHPrincipalGrantTargets(grantID string) ([]models.SSHPrinci
|
||||
END AS target_active,
|
||||
t.created_at
|
||||
FROM ssh_principal_grant_targets t
|
||||
LEFT JOIN users u ON t.target_type = 'user' AND u.public_id = t.target_public_id
|
||||
LEFT JOIN user_groups g ON t.target_type = 'group' AND g.public_id = t.target_public_id
|
||||
WHERE t.grant_public_id = ?
|
||||
ORDER BY t.target_type, target_name, t.target_public_id`,
|
||||
JOIN ssh_principal_grants gnt ON gnt.id = t.grant_id
|
||||
LEFT JOIN users u ON t.target_type = 'user' AND u.id = t.target_id
|
||||
LEFT JOIN user_groups g ON t.target_type = 'group' AND g.id = t.target_id
|
||||
WHERE gnt.public_id = ?
|
||||
ORDER BY t.target_type, target_name, target_id`,
|
||||
strings.TrimSpace(grantID),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -541,8 +551,8 @@ func insertSSHPrincipalGrantTargetTx(tx *sql.Tx, grantID string, targetType stri
|
||||
return errors.New("target id is required")
|
||||
}
|
||||
if normalizedType == "user" {
|
||||
result, err = tx.Exec(`INSERT INTO ssh_principal_grant_targets (grant_public_id, target_type, target_public_id, created_at)
|
||||
SELECT ?, 'user', u.public_id, ?
|
||||
result, err = tx.Exec(`INSERT INTO ssh_principal_grant_targets (grant_id, target_type, target_id, created_at)
|
||||
SELECT (SELECT id FROM ssh_principal_grants WHERE public_id = ?), 'user', u.id, ?
|
||||
FROM users u
|
||||
WHERE u.public_id = ?`,
|
||||
strings.TrimSpace(grantID),
|
||||
@@ -550,8 +560,8 @@ func insertSSHPrincipalGrantTargetTx(tx *sql.Tx, grantID string, targetType stri
|
||||
strings.TrimSpace(targetID),
|
||||
)
|
||||
} else {
|
||||
result, err = tx.Exec(`INSERT INTO ssh_principal_grant_targets (grant_public_id, target_type, target_public_id, created_at)
|
||||
SELECT ?, 'group', g.public_id, ?
|
||||
result, err = tx.Exec(`INSERT INTO ssh_principal_grant_targets (grant_id, target_type, target_id, created_at)
|
||||
SELECT (SELECT id FROM ssh_principal_grants WHERE public_id = ?), 'group', g.id, ?
|
||||
FROM user_groups g
|
||||
WHERE g.public_id = ?`,
|
||||
strings.TrimSpace(grantID),
|
||||
|
||||
@@ -18,7 +18,12 @@ func (s *Store) listSSHServerGroupMemberIDs(groupID string) ([]string, error) {
|
||||
var id string
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT server_public_id FROM ssh_server_group_members WHERE group_public_id = ? ORDER BY server_public_id`, strings.TrimSpace(groupID))
|
||||
rows, err = s.Query(`SELECT s.public_id
|
||||
FROM ssh_server_group_members gm
|
||||
JOIN ssh_servers s ON s.id = gm.server_id
|
||||
JOIN ssh_server_groups g ON g.id = gm.group_id
|
||||
WHERE g.public_id = ?
|
||||
ORDER BY s.public_id`, strings.TrimSpace(groupID))
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
@@ -72,10 +77,12 @@ func (s *Store) ListSSHServersForGroup(groupID string) ([]models.SSHServer, erro
|
||||
var tagsJSON string
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.host_key_policy, s.owner_scope, s.owner_user_public_id, s.created_by_kind, s.created_by_subject_id, s.created_by_subject_name, s.created_at, s.updated_at
|
||||
rows, err = s.Query(`SELECT s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.host_key_policy, s.owner_scope, COALESCE(u.public_id, ''), s.created_by_kind, s.created_by_subject_id, s.created_by_subject_name, s.created_at, s.updated_at
|
||||
FROM ssh_server_group_members gm
|
||||
JOIN ssh_servers s ON s.public_id = gm.server_public_id
|
||||
WHERE gm.group_public_id = ?
|
||||
JOIN ssh_servers s ON s.id = gm.server_id
|
||||
JOIN ssh_server_groups g ON g.id = gm.group_id
|
||||
LEFT JOIN users u ON u.id = s.owner_user_id
|
||||
WHERE g.public_id = ?
|
||||
ORDER BY s.name`, strings.TrimSpace(groupID))
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
@@ -137,7 +144,10 @@ func (s *Store) DeleteSSHServerGroup(id string) error {
|
||||
var err error
|
||||
|
||||
id = strings.TrimSpace(id)
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_access_profiles WHERE server_group_public_id = ?`, id)
|
||||
row = s.QueryRow(`SELECT COUNT(*)
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||
WHERE g.public_id = ?`, id)
|
||||
err = row.Scan(&count)
|
||||
if err != nil { return err }
|
||||
if count > 0 { return errors.New("ssh server group is used by access profiles") }
|
||||
@@ -150,14 +160,20 @@ func (s *Store) AddSSHServerGroupMember(groupID string, serverID string) error {
|
||||
var err error
|
||||
|
||||
now = time.Now().UTC().Unix()
|
||||
_, err = s.Exec(`INSERT OR IGNORE INTO ssh_server_group_members (group_public_id, server_public_id, created_at) VALUES (?, ?, ?)`, strings.TrimSpace(groupID), strings.TrimSpace(serverID), now)
|
||||
_, err = s.Exec(`INSERT OR IGNORE INTO ssh_server_group_members (group_id, server_id, created_at)
|
||||
SELECT g.id, srv.id, ?
|
||||
FROM ssh_server_groups g
|
||||
JOIN ssh_servers srv ON srv.public_id = ?
|
||||
WHERE g.public_id = ?`, now, strings.TrimSpace(serverID), strings.TrimSpace(groupID))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSSHServerGroupMember(groupID string, serverID string) error {
|
||||
var err error
|
||||
|
||||
_, err = s.Exec(`DELETE FROM ssh_server_group_members WHERE group_public_id = ? AND server_public_id = ?`, strings.TrimSpace(groupID), strings.TrimSpace(serverID))
|
||||
_, err = s.Exec(`DELETE FROM ssh_server_group_members
|
||||
WHERE group_id = (SELECT id FROM ssh_server_groups WHERE public_id = ?)
|
||||
AND server_id = (SELECT id FROM ssh_servers WHERE public_id = ?)`, strings.TrimSpace(groupID), strings.TrimSpace(serverID))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -166,7 +182,9 @@ func (s *Store) SSHServerBelongsToGroup(groupID string, serverID string) (bool,
|
||||
var count int
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_server_group_members WHERE group_public_id = ? AND server_public_id = ?`, strings.TrimSpace(groupID), strings.TrimSpace(serverID))
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM ssh_server_group_members
|
||||
WHERE group_id = (SELECT id FROM ssh_server_groups WHERE public_id = ?)
|
||||
AND server_id = (SELECT id FROM ssh_servers WHERE public_id = ?)`, strings.TrimSpace(groupID), strings.TrimSpace(serverID))
|
||||
err = row.Scan(&count)
|
||||
if err != nil { return false, err }
|
||||
return count > 0, nil
|
||||
|
||||
@@ -215,8 +215,8 @@ func (s *Store) CreateSSHUserCAIssuance(item models.SSHUserCAIssuance) (models.S
|
||||
}
|
||||
_, err = s.Exec(`INSERT INTO ssh_user_ca_issuances (
|
||||
public_id,
|
||||
ca_public_id,
|
||||
issuer_user_public_id,
|
||||
ca_id,
|
||||
issuer_user_id,
|
||||
issuer_username,
|
||||
issuer_kind,
|
||||
source_public_key,
|
||||
@@ -232,10 +232,11 @@ func (s *Store) CreateSSHUserCAIssuance(item models.SSHUserCAIssuance) (models.S
|
||||
remote_addr,
|
||||
user_agent,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, (SELECT id FROM ssh_user_cas WHERE public_id = ?), CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
strings.TrimSpace(item.ID),
|
||||
strings.TrimSpace(item.CAID),
|
||||
nullableTrimmedString(item.IssuerUserID),
|
||||
strings.TrimSpace(item.IssuerUserID),
|
||||
strings.TrimSpace(item.IssuerUserID),
|
||||
strings.TrimSpace(item.IssuerUsername),
|
||||
strings.TrimSpace(item.IssuerKind),
|
||||
strings.TrimSpace(item.SourcePublicKey),
|
||||
@@ -268,27 +269,29 @@ func (s *Store) ListSSHUserCAIssuancesByCA(caID string, limit int) ([]models.SSH
|
||||
var grantNamesJSON string
|
||||
limit = normalizeSSHUserCAIssuanceLimit(limit)
|
||||
rows, err = s.Query(`SELECT
|
||||
public_id,
|
||||
ca_public_id,
|
||||
COALESCE(issuer_user_public_id, ''),
|
||||
issuer_username,
|
||||
issuer_kind,
|
||||
source_public_key,
|
||||
source_public_key_fingerprint,
|
||||
certificate,
|
||||
key_id,
|
||||
principals_json,
|
||||
grant_ids_json,
|
||||
grant_names_json,
|
||||
valid_after,
|
||||
valid_before,
|
||||
serial,
|
||||
remote_addr,
|
||||
user_agent,
|
||||
created_at
|
||||
FROM ssh_user_ca_issuances
|
||||
WHERE ca_public_id = ?
|
||||
ORDER BY created_at DESC
|
||||
i.public_id,
|
||||
COALESCE(ca.public_id, ''),
|
||||
COALESCE(u.public_id, ''),
|
||||
i.issuer_username,
|
||||
i.issuer_kind,
|
||||
i.source_public_key,
|
||||
i.source_public_key_fingerprint,
|
||||
i.certificate,
|
||||
i.key_id,
|
||||
i.principals_json,
|
||||
i.grant_ids_json,
|
||||
i.grant_names_json,
|
||||
i.valid_after,
|
||||
i.valid_before,
|
||||
i.serial,
|
||||
i.remote_addr,
|
||||
i.user_agent,
|
||||
i.created_at
|
||||
FROM ssh_user_ca_issuances i
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = i.ca_id
|
||||
LEFT JOIN users u ON u.id = i.issuer_user_id
|
||||
WHERE ca.public_id = ?
|
||||
ORDER BY i.created_at DESC
|
||||
LIMIT ?`,
|
||||
strings.TrimSpace(caID),
|
||||
limit,
|
||||
@@ -354,52 +357,56 @@ func (s *Store) ListSSHUserCAIssuances(limit int, caID string) ([]models.SSHUser
|
||||
caID = strings.TrimSpace(caID)
|
||||
if caID == "" {
|
||||
rows, err = s.Query(`SELECT
|
||||
public_id,
|
||||
ca_public_id,
|
||||
COALESCE(issuer_user_public_id, ''),
|
||||
issuer_username,
|
||||
issuer_kind,
|
||||
source_public_key,
|
||||
source_public_key_fingerprint,
|
||||
certificate,
|
||||
key_id,
|
||||
principals_json,
|
||||
grant_ids_json,
|
||||
grant_names_json,
|
||||
valid_after,
|
||||
valid_before,
|
||||
serial,
|
||||
remote_addr,
|
||||
user_agent,
|
||||
created_at
|
||||
FROM ssh_user_ca_issuances
|
||||
ORDER BY created_at DESC
|
||||
i.public_id,
|
||||
COALESCE(ca.public_id, ''),
|
||||
COALESCE(u.public_id, ''),
|
||||
i.issuer_username,
|
||||
i.issuer_kind,
|
||||
i.source_public_key,
|
||||
i.source_public_key_fingerprint,
|
||||
i.certificate,
|
||||
i.key_id,
|
||||
i.principals_json,
|
||||
i.grant_ids_json,
|
||||
i.grant_names_json,
|
||||
i.valid_after,
|
||||
i.valid_before,
|
||||
i.serial,
|
||||
i.remote_addr,
|
||||
i.user_agent,
|
||||
i.created_at
|
||||
FROM ssh_user_ca_issuances i
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = i.ca_id
|
||||
LEFT JOIN users u ON u.id = i.issuer_user_id
|
||||
ORDER BY i.created_at DESC
|
||||
LIMIT ?`,
|
||||
limit,
|
||||
)
|
||||
} else {
|
||||
rows, err = s.Query(`SELECT
|
||||
public_id,
|
||||
ca_public_id,
|
||||
COALESCE(issuer_user_public_id, ''),
|
||||
issuer_username,
|
||||
issuer_kind,
|
||||
source_public_key,
|
||||
source_public_key_fingerprint,
|
||||
certificate,
|
||||
key_id,
|
||||
principals_json,
|
||||
grant_ids_json,
|
||||
grant_names_json,
|
||||
valid_after,
|
||||
valid_before,
|
||||
serial,
|
||||
remote_addr,
|
||||
user_agent,
|
||||
created_at
|
||||
FROM ssh_user_ca_issuances
|
||||
WHERE ca_public_id = ?
|
||||
ORDER BY created_at DESC
|
||||
i.public_id,
|
||||
COALESCE(ca.public_id, ''),
|
||||
COALESCE(u.public_id, ''),
|
||||
i.issuer_username,
|
||||
i.issuer_kind,
|
||||
i.source_public_key,
|
||||
i.source_public_key_fingerprint,
|
||||
i.certificate,
|
||||
i.key_id,
|
||||
i.principals_json,
|
||||
i.grant_ids_json,
|
||||
i.grant_names_json,
|
||||
i.valid_after,
|
||||
i.valid_before,
|
||||
i.serial,
|
||||
i.remote_addr,
|
||||
i.user_agent,
|
||||
i.created_at
|
||||
FROM ssh_user_ca_issuances i
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = i.ca_id
|
||||
LEFT JOIN users u ON u.id = i.issuer_user_id
|
||||
WHERE ca.public_id = ?
|
||||
ORDER BY i.created_at DESC
|
||||
LIMIT ?`,
|
||||
caID,
|
||||
limit,
|
||||
@@ -464,27 +471,29 @@ func (s *Store) ListSSHUserCAIssuancesForSelf(userID string, limit int) ([]model
|
||||
var grantNamesJSON string
|
||||
limit = normalizeSSHUserCAIssuanceLimit(limit)
|
||||
rows, err = s.Query(`SELECT
|
||||
public_id,
|
||||
ca_public_id,
|
||||
COALESCE(issuer_user_public_id, ''),
|
||||
issuer_username,
|
||||
issuer_kind,
|
||||
source_public_key,
|
||||
source_public_key_fingerprint,
|
||||
certificate,
|
||||
key_id,
|
||||
principals_json,
|
||||
grant_ids_json,
|
||||
grant_names_json,
|
||||
valid_after,
|
||||
valid_before,
|
||||
serial,
|
||||
remote_addr,
|
||||
user_agent,
|
||||
created_at
|
||||
FROM ssh_user_ca_issuances
|
||||
WHERE issuer_user_public_id = ? AND issuer_kind = 'self'
|
||||
ORDER BY created_at DESC
|
||||
i.public_id,
|
||||
COALESCE(ca.public_id, ''),
|
||||
COALESCE(u.public_id, ''),
|
||||
i.issuer_username,
|
||||
i.issuer_kind,
|
||||
i.source_public_key,
|
||||
i.source_public_key_fingerprint,
|
||||
i.certificate,
|
||||
i.key_id,
|
||||
i.principals_json,
|
||||
i.grant_ids_json,
|
||||
i.grant_names_json,
|
||||
i.valid_after,
|
||||
i.valid_before,
|
||||
i.serial,
|
||||
i.remote_addr,
|
||||
i.user_agent,
|
||||
i.created_at
|
||||
FROM ssh_user_ca_issuances i
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = i.ca_id
|
||||
LEFT JOIN users u ON u.id = i.issuer_user_id
|
||||
WHERE u.public_id = ? AND i.issuer_kind = 'self'
|
||||
ORDER BY i.created_at DESC
|
||||
LIMIT ?`,
|
||||
strings.TrimSpace(userID),
|
||||
limit,
|
||||
|
||||
@@ -143,17 +143,17 @@ func (s *Store) DeleteUser(id string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'user' AND subject_public_id = ?`, id)
|
||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'user' AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM ssh_principal_grant_targets WHERE target_type = 'user' AND target_public_id = ?`, id)
|
||||
_, err = tx.Exec(`DELETE FROM ssh_principal_grant_targets WHERE target_type = 'user' AND target_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`DELETE FROM subject_permissions WHERE subject_type = 'user' AND subject_public_id = ?`, id)
|
||||
_, err = tx.Exec(`DELETE FROM subject_permissions WHERE subject_type = 'user' AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
@@ -950,8 +950,8 @@ func (s *Store) CreateProject(project models.Project) (models.Project, error) {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return project, err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_public_id, role, created_at, updated_at)
|
||||
VALUES ((SELECT id FROM projects WHERE public_id = ?), 'user', ?, ?, ?, ?)`,
|
||||
_, err = tx.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at)
|
||||
VALUES ((SELECT id FROM projects WHERE public_id = ?), 'user', (SELECT id FROM users WHERE public_id = ?), ?, ?, ?)`,
|
||||
project.ID, project.CreatedBy, "admin", project.CreatedAt, project.UpdatedAt)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
@@ -1102,10 +1102,10 @@ func (s *Store) ListProjectsForUser(userID string) ([]models.Project, error) {
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND (
|
||||
(b.subject_type = 'user' AND b.subject_public_id = ?)
|
||||
(b.subject_type = 'user' AND b.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR
|
||||
(b.subject_type = 'group' AND b.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
(b.subject_type = 'group' AND b.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
@@ -1167,7 +1167,7 @@ func (s *Store) ListProjectsForPrincipal(principalID string) ([]models.Project,
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND b.subject_type = 'principal'
|
||||
AND b.subject_public_id = ?
|
||||
AND b.subject_id = (SELECT id FROM service_principals WHERE public_id = ?)
|
||||
)
|
||||
ORDER BY p.name
|
||||
`, principalID)
|
||||
@@ -1289,10 +1289,10 @@ func (s *Store) ListProjectsFilteredForUser(userID string, limit int, offset int
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND (
|
||||
(b.subject_type = 'user' AND b.subject_public_id = ?)
|
||||
(b.subject_type = 'user' AND b.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR
|
||||
(b.subject_type = 'group' AND b.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
(b.subject_type = 'group' AND b.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
@@ -1327,10 +1327,10 @@ func (s *Store) ListProjectsFilteredForUser(userID string, limit int, offset int
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND (
|
||||
(b.subject_type = 'user' AND b.subject_public_id = ?)
|
||||
(b.subject_type = 'user' AND b.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR
|
||||
(b.subject_type = 'group' AND b.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
(b.subject_type = 'group' AND b.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
@@ -1406,7 +1406,7 @@ func (s *Store) ListProjectsFilteredForPrincipal(principalID string, limit int,
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND b.subject_type = 'principal'
|
||||
AND b.subject_public_id = ?
|
||||
AND b.subject_id = (SELECT id FROM service_principals WHERE public_id = ?)
|
||||
)
|
||||
ORDER BY p.name LIMIT ? OFFSET ?`,
|
||||
principalID,
|
||||
@@ -1427,7 +1427,7 @@ func (s *Store) ListProjectsFilteredForPrincipal(principalID string, limit int,
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND b.subject_type = 'principal'
|
||||
AND b.subject_public_id = ?
|
||||
AND b.subject_id = (SELECT id FROM service_principals WHERE public_id = ?)
|
||||
) AND (p.name LIKE ? OR p.slug LIKE ?)
|
||||
ORDER BY p.name LIMIT ? OFFSET ?`,
|
||||
principalID,
|
||||
@@ -1498,10 +1498,10 @@ func (s *Store) CountProjectsFilteredForUser(userID string, query string) (int,
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND (
|
||||
(b.subject_type = 'user' AND b.subject_public_id = ?)
|
||||
(b.subject_type = 'user' AND b.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR
|
||||
(b.subject_type = 'group' AND b.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
(b.subject_type = 'group' AND b.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
@@ -1528,10 +1528,10 @@ func (s *Store) CountProjectsFilteredForUser(userID string, query string) (int,
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND (
|
||||
(b.subject_type = 'user' AND b.subject_public_id = ?)
|
||||
(b.subject_type = 'user' AND b.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR
|
||||
(b.subject_type = 'group' AND b.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
(b.subject_type = 'group' AND b.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
@@ -1573,7 +1573,7 @@ func (s *Store) CountProjectsFilteredForPrincipal(principalID string, query stri
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND b.subject_type = 'principal'
|
||||
AND b.subject_public_id = ?
|
||||
AND b.subject_id = (SELECT id FROM service_principals WHERE public_id = ?)
|
||||
)`,
|
||||
principalID,
|
||||
)
|
||||
@@ -1586,7 +1586,7 @@ func (s *Store) CountProjectsFilteredForPrincipal(principalID string, query stri
|
||||
FROM project_role_bindings b
|
||||
WHERE b.project_id = p.id
|
||||
AND b.subject_type = 'principal'
|
||||
AND b.subject_public_id = ?
|
||||
AND b.subject_id = (SELECT id FROM service_principals WHERE public_id = ?)
|
||||
) AND (p.name LIKE ? OR p.slug LIKE ?)`,
|
||||
principalID,
|
||||
"%"+query+"%",
|
||||
@@ -1614,11 +1614,11 @@ func (s *Store) AddProjectMember(projectID, userID, role string) (models.Project
|
||||
var rowsAffected int64
|
||||
now = time.Now().UTC().Unix()
|
||||
member = models.ProjectMember{ProjectID: projectID, UserID: userID, Role: role, CreatedAt: now}
|
||||
result, err = s.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_public_id, role, created_at, updated_at)
|
||||
SELECT (SELECT id FROM projects WHERE public_id = ?), 'user', u.public_id, ?, ?, ?
|
||||
result, err = s.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at)
|
||||
SELECT (SELECT id FROM projects WHERE public_id = ?), 'user', u.id, ?, ?, ?
|
||||
FROM users u
|
||||
WHERE u.public_id = ?
|
||||
ON CONFLICT(project_id, subject_type, subject_public_id) DO UPDATE SET role = excluded.role, updated_at = excluded.updated_at`,
|
||||
ON CONFLICT(project_id, subject_type, subject_id) DO UPDATE SET role = excluded.role, updated_at = excluded.updated_at`,
|
||||
member.ProjectID, member.Role, now, now, member.UserID)
|
||||
if err != nil {
|
||||
return member, err
|
||||
@@ -1639,7 +1639,7 @@ func (s *Store) UpdateProjectMemberRole(projectID, userID, role string) error {
|
||||
SET role = ?, updated_at = ?
|
||||
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
AND subject_type = 'user'
|
||||
AND subject_public_id = ?`, role, time.Now().UTC().Unix(), projectID, userID)
|
||||
AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, role, time.Now().UTC().Unix(), projectID, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1648,7 +1648,7 @@ func (s *Store) RemoveProjectMember(projectID, userID string) error {
|
||||
_, err = s.Exec(`DELETE FROM project_role_bindings
|
||||
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
AND subject_type = 'user'
|
||||
AND subject_public_id = ?`, projectID, userID)
|
||||
AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, projectID, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1660,7 +1660,7 @@ func (s *Store) ListProjectMembers(projectID string) ([]models.ProjectMember, er
|
||||
rows, err = s.Query(`SELECT p.public_id, u.public_id, b.role, b.created_at
|
||||
FROM project_role_bindings b
|
||||
JOIN projects p ON p.id = b.project_id
|
||||
JOIN users u ON u.public_id = b.subject_public_id
|
||||
JOIN users u ON u.id = b.subject_id
|
||||
WHERE b.project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
AND b.subject_type = 'user'
|
||||
ORDER BY b.role, u.username`, projectID)
|
||||
@@ -1685,7 +1685,7 @@ func (s *Store) GetProjectMemberRole(projectID, userID string) (string, error) {
|
||||
row = s.QueryRow(`SELECT role FROM project_role_bindings
|
||||
WHERE project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
AND subject_type = 'user'
|
||||
AND subject_public_id = ?`, projectID, userID)
|
||||
AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, projectID, userID)
|
||||
err = row.Scan(&role)
|
||||
return role, err
|
||||
}
|
||||
@@ -1929,11 +1929,11 @@ func (s *Store) ListProjectIDsForUser(userID string) ([]string, error) {
|
||||
rows, err = s.Query(`SELECT DISTINCT p.public_id
|
||||
FROM project_role_bindings b
|
||||
JOIN projects p ON p.id = b.project_id
|
||||
WHERE (b.subject_type = 'user' AND b.subject_public_id = ?)
|
||||
WHERE (b.subject_type = 'user' AND b.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR (
|
||||
b.subject_type = 'group'
|
||||
AND b.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
AND b.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
|
||||
@@ -18,17 +18,22 @@ func (s *Store) ListSubjectPermissions(permission string, subjectType string, su
|
||||
rows, err = s.Query(`SELECT
|
||||
sp.permission,
|
||||
sp.subject_type,
|
||||
sp.subject_public_id,
|
||||
CASE
|
||||
WHEN sp.subject_type = 'user' THEN COALESCE(u.public_id, '')
|
||||
WHEN sp.subject_type = 'group' THEN COALESCE(g.public_id, '')
|
||||
WHEN sp.subject_type = 'principal' THEN COALESCE(p.public_id, '')
|
||||
ELSE ''
|
||||
END AS subject_public_id,
|
||||
CASE
|
||||
WHEN sp.subject_type = 'user' THEN
|
||||
CASE
|
||||
WHEN u.public_id IS NULL THEN sp.subject_public_id
|
||||
WHEN u.public_id IS NULL THEN ''
|
||||
WHEN u.display_name <> '' THEN u.display_name || ' (' || u.username || ')'
|
||||
ELSE u.username
|
||||
END
|
||||
WHEN sp.subject_type = 'group' THEN COALESCE(g.name, sp.subject_public_id)
|
||||
WHEN sp.subject_type = 'principal' THEN COALESCE(p.name, sp.subject_public_id)
|
||||
ELSE sp.subject_public_id
|
||||
WHEN sp.subject_type = 'group' THEN COALESCE(g.name, '')
|
||||
WHEN sp.subject_type = 'principal' THEN COALESCE(p.name, '')
|
||||
ELSE ''
|
||||
END AS subject_name,
|
||||
CASE
|
||||
WHEN sp.subject_type = 'user' THEN CASE WHEN u.public_id IS NOT NULL AND u.disabled = 0 THEN 1 ELSE 0 END
|
||||
@@ -41,23 +46,28 @@ func (s *Store) ListSubjectPermissions(permission string, subjectType string, su
|
||||
FROM subject_permissions sp
|
||||
LEFT JOIN users u
|
||||
ON sp.subject_type = 'user'
|
||||
AND u.public_id = sp.subject_public_id
|
||||
AND u.id = sp.subject_id
|
||||
LEFT JOIN user_groups g
|
||||
ON sp.subject_type = 'group'
|
||||
AND g.public_id = sp.subject_public_id
|
||||
AND g.id = sp.subject_id
|
||||
LEFT JOIN service_principals p
|
||||
ON sp.subject_type = 'principal'
|
||||
AND p.public_id = sp.subject_public_id
|
||||
AND p.id = sp.subject_id
|
||||
WHERE (? = '' OR sp.permission = ?)
|
||||
AND (? = '' OR sp.subject_type = ?)
|
||||
AND (? = '' OR sp.subject_public_id = ?)
|
||||
ORDER BY sp.permission, sp.subject_type, subject_name, sp.subject_public_id`,
|
||||
AND (? = ''
|
||||
OR (sp.subject_type = 'user' AND u.public_id = ?)
|
||||
OR (sp.subject_type = 'group' AND g.public_id = ?)
|
||||
OR (sp.subject_type = 'principal' AND p.public_id = ?))
|
||||
ORDER BY sp.permission, sp.subject_type, subject_name, subject_public_id`,
|
||||
permission,
|
||||
permission,
|
||||
subjectType,
|
||||
subjectType,
|
||||
subjectID,
|
||||
subjectID,
|
||||
subjectID,
|
||||
subjectID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -112,11 +122,11 @@ func (s *Store) UpsertSubjectPermission(item models.SubjectPermission) (models.S
|
||||
item.UpdatedAt = now
|
||||
switch item.SubjectType {
|
||||
case "user":
|
||||
result, err = s.Exec(`INSERT INTO subject_permissions (permission, subject_type, subject_public_id, created_at, updated_at)
|
||||
SELECT ?, 'user', u.public_id, ?, ?
|
||||
result, err = s.Exec(`INSERT INTO subject_permissions (permission, subject_type, subject_id, created_at, updated_at)
|
||||
SELECT ?, 'user', u.id, ?, ?
|
||||
FROM users u
|
||||
WHERE u.public_id = ?
|
||||
ON CONFLICT(permission, subject_type, subject_public_id)
|
||||
ON CONFLICT(permission, subject_type, subject_id)
|
||||
DO UPDATE SET updated_at = excluded.updated_at`,
|
||||
item.Permission,
|
||||
item.CreatedAt,
|
||||
@@ -124,11 +134,11 @@ func (s *Store) UpsertSubjectPermission(item models.SubjectPermission) (models.S
|
||||
item.SubjectID,
|
||||
)
|
||||
case "group":
|
||||
result, err = s.Exec(`INSERT INTO subject_permissions (permission, subject_type, subject_public_id, created_at, updated_at)
|
||||
SELECT ?, 'group', g.public_id, ?, ?
|
||||
result, err = s.Exec(`INSERT INTO subject_permissions (permission, subject_type, subject_id, created_at, updated_at)
|
||||
SELECT ?, 'group', g.id, ?, ?
|
||||
FROM user_groups g
|
||||
WHERE g.public_id = ?
|
||||
ON CONFLICT(permission, subject_type, subject_public_id)
|
||||
ON CONFLICT(permission, subject_type, subject_id)
|
||||
DO UPDATE SET updated_at = excluded.updated_at`,
|
||||
item.Permission,
|
||||
item.CreatedAt,
|
||||
@@ -136,11 +146,11 @@ func (s *Store) UpsertSubjectPermission(item models.SubjectPermission) (models.S
|
||||
item.SubjectID,
|
||||
)
|
||||
case "principal":
|
||||
result, err = s.Exec(`INSERT INTO subject_permissions (permission, subject_type, subject_public_id, created_at, updated_at)
|
||||
SELECT ?, 'principal', p.public_id, ?, ?
|
||||
result, err = s.Exec(`INSERT INTO subject_permissions (permission, subject_type, subject_id, created_at, updated_at)
|
||||
SELECT ?, 'principal', p.id, ?, ?
|
||||
FROM service_principals p
|
||||
WHERE p.public_id = ?
|
||||
ON CONFLICT(permission, subject_type, subject_public_id)
|
||||
ON CONFLICT(permission, subject_type, subject_id)
|
||||
DO UPDATE SET updated_at = excluded.updated_at`,
|
||||
item.Permission,
|
||||
item.CreatedAt,
|
||||
@@ -168,9 +178,19 @@ func (s *Store) DeleteSubjectPermission(permission string, subjectType string, s
|
||||
_, err = s.Exec(`DELETE FROM subject_permissions
|
||||
WHERE permission = ?
|
||||
AND subject_type = ?
|
||||
AND subject_public_id = ?`,
|
||||
AND subject_id = CASE
|
||||
WHEN ? = 'user' THEN (SELECT id FROM users WHERE public_id = ?)
|
||||
WHEN ? = 'group' THEN (SELECT id FROM user_groups WHERE public_id = ?)
|
||||
WHEN ? = 'principal' THEN (SELECT id FROM service_principals WHERE public_id = ?)
|
||||
ELSE -1
|
||||
END`,
|
||||
strings.TrimSpace(permission),
|
||||
strings.ToLower(strings.TrimSpace(subjectType)),
|
||||
strings.ToLower(strings.TrimSpace(subjectType)),
|
||||
strings.TrimSpace(subjectID),
|
||||
strings.ToLower(strings.TrimSpace(subjectType)),
|
||||
strings.TrimSpace(subjectID),
|
||||
strings.ToLower(strings.TrimSpace(subjectType)),
|
||||
strings.TrimSpace(subjectID),
|
||||
)
|
||||
return err
|
||||
@@ -184,11 +204,11 @@ func (s *Store) ListUserPermissions(userID string) ([]string, error) {
|
||||
userID = strings.TrimSpace(userID)
|
||||
rows, err = s.Query(`SELECT DISTINCT sp.permission
|
||||
FROM subject_permissions sp
|
||||
WHERE (sp.subject_type = 'user' AND sp.subject_public_id = ?)
|
||||
WHERE (sp.subject_type = 'user' AND sp.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR (
|
||||
sp.subject_type = 'group'
|
||||
AND sp.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
AND sp.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
@@ -234,12 +254,12 @@ func (s *Store) UserHasPermission(userID string, permission string) (bool, error
|
||||
FROM subject_permissions sp
|
||||
WHERE sp.permission = ?
|
||||
AND (
|
||||
(sp.subject_type = 'user' AND sp.subject_public_id = ?)
|
||||
(sp.subject_type = 'user' AND sp.subject_id = (SELECT id FROM users WHERE public_id = ?))
|
||||
OR
|
||||
(
|
||||
sp.subject_type = 'group'
|
||||
AND sp.subject_public_id IN (
|
||||
SELECT g.public_id
|
||||
AND sp.subject_id IN (
|
||||
SELECT g.id
|
||||
FROM user_groups g
|
||||
WHERE g.disabled = 0
|
||||
AND (
|
||||
@@ -279,7 +299,7 @@ func (s *Store) PrincipalHasPermission(principalID string, permission string) (b
|
||||
FROM subject_permissions
|
||||
WHERE permission = ?
|
||||
AND subject_type = 'principal'
|
||||
AND subject_public_id = ?
|
||||
AND subject_id = (SELECT id FROM service_principals WHERE public_id = ?)
|
||||
LIMIT 1`,
|
||||
permission,
|
||||
principalID,
|
||||
|
||||
+467
-467
File diff suppressed because it is too large
Load Diff
@@ -133,7 +133,7 @@ func (api *API) ListPKIClientProfiles(w http.ResponseWriter, r *http.Request, _
|
||||
targetID = strings.TrimSpace(r.URL.Query().Get("target_id"))
|
||||
items, err = api.store(r).ListPKIClientProfiles(targetType, targetID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
for i = 0; i < len(items); i++ {
|
||||
@@ -158,38 +158,38 @@ func (api *API) CreatePKIClientProfile(w http.ResponseWriter, r *http.Request, _
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
ca, err = api.store(r).GetPKICA(strings.TrimSpace(req.CAID))
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "CA not found")
|
||||
return
|
||||
}
|
||||
if ca.Status != "active" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected CA is not active"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "selected CA is not active")
|
||||
return
|
||||
}
|
||||
permissions, permissionsValue, err = normalizePKIClientProfilePermissions(req.AuthzPermissions)
|
||||
_ = permissions
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
targets, err = normalizePKIClientProfileTargets(req.Targets)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
err = validatePKIClientProfileWindow(req.DefaultValidSeconds, req.MaxValidSeconds)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
req.SubjectOrganization = normalizePKIClientSubjectOrganization(req.SubjectOrganization, api.serverTitle())
|
||||
req.SANURIPrefix, err = normalizePKIClientSANURIPrefix(req.SANURIPrefix, api.serverId())
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
currentUser, hasCurrentUser = middleware.UserFromContext(r.Context())
|
||||
@@ -207,7 +207,7 @@ func (api *API) CreatePKIClientProfile(w http.ResponseWriter, r *http.Request, _
|
||||
Targets: targets,
|
||||
}
|
||||
if strings.TrimSpace(item.Name) == "" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
if hasCurrentUser {
|
||||
@@ -216,7 +216,7 @@ func (api *API) CreatePKIClientProfile(w http.ResponseWriter, r *http.Request, _
|
||||
item, err = api.store(r).CreatePKIClientProfile(item)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_WARN, "client profile create failed name=%s err=%v", item.Name, err)
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_INFO, "client profile create success id=%s name=%s targets=%d", item.ID, item.Name, len(item.Targets))
|
||||
@@ -237,47 +237,47 @@ func (api *API) UpdatePKIClientProfile(w http.ResponseWriter, r *http.Request, p
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
existing, err = api.store(r).GetPKIClientProfile(params["id"])
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "profile not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "profile not found")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
ca, err = api.store(r).GetPKICA(strings.TrimSpace(req.CAID))
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "CA not found")
|
||||
return
|
||||
}
|
||||
if ca.Status != "active" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected CA is not active"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "selected CA is not active")
|
||||
return
|
||||
}
|
||||
permissions, permissionsValue, err = normalizePKIClientProfilePermissions(req.AuthzPermissions)
|
||||
_ = permissions
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
targets, err = normalizePKIClientProfileTargets(req.Targets)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
err = validatePKIClientProfileWindow(req.DefaultValidSeconds, req.MaxValidSeconds)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
req.SubjectOrganization = normalizePKIClientSubjectOrganization(req.SubjectOrganization, api.serverTitle())
|
||||
req.SANURIPrefix, err = normalizePKIClientSANURIPrefix(req.SANURIPrefix, api.serverId())
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
existing.Name = strings.TrimSpace(req.Name)
|
||||
@@ -293,13 +293,13 @@ func (api *API) UpdatePKIClientProfile(w http.ResponseWriter, r *http.Request, p
|
||||
existing.Enabled = req.Enabled
|
||||
existing.Targets = targets
|
||||
if strings.TrimSpace(existing.Name) == "" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
existing, err = api.store(r).UpdatePKIClientProfile(existing)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_WARN, "client profile update failed id=%s err=%v", params["id"], err)
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_INFO, "client profile update success id=%s name=%s", existing.ID, existing.Name)
|
||||
@@ -315,7 +315,7 @@ func (api *API) DeletePKIClientProfile(w http.ResponseWriter, r *http.Request, p
|
||||
err = api.store(r).DeletePKIClientProfile(params["id"])
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_WARN, "client profile delete failed id=%s err=%v", params["id"], err)
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_INFO, "client profile delete success id=%s", params["id"])
|
||||
@@ -337,7 +337,7 @@ func (api *API) ListPKIClientProfilesForSelf(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
items, err = api.store(r).ListActivePKIClientProfilesForUser(user.ID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
for i = 0; i < len(items); i++ {
|
||||
@@ -361,7 +361,7 @@ func (api *API) ListPKIClientIssuancesForSelf(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
items, err = api.store(r).ListPKIClientIssuancesForUser(user.ID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
for i = 0; i < len(items); i++ {
|
||||
@@ -404,27 +404,27 @@ func (api *API) IssuePKIClientCertForSelf(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
profile, err = api.store(r).GetActivePKIClientProfileForUser(strings.TrimSpace(req.ProfileID), user.ID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "profile not found or not allowed"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "profile not found or not allowed")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
ca, err = api.store(r).GetPKICA(profile.CAID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "issuer CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "issuer CA not found")
|
||||
return
|
||||
}
|
||||
permissions = splitPKIClientPermissionList(profile.AuthzPermissions)
|
||||
validSeconds, err = normalizePKIClientRequestedValidSeconds(req.ValidSeconds, profile.DefaultValidSeconds, profile.MaxValidSeconds)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
commonName = strings.TrimSpace(req.CommonName)
|
||||
@@ -432,12 +432,12 @@ func (api *API) IssuePKIClientCertForSelf(w http.ResponseWriter, r *http.Request
|
||||
sanIPs = trimPKIClientStringList(req.SANIPs)
|
||||
if profile.AllowServerAuth {
|
||||
if commonName == "" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "common_name is required for profiles that allow server authentication"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "common_name is required for profiles that allow server authentication")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if commonName != "" || len(sanDNS) > 0 || len(sanIPs) > 0 {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "common_name, san_dns, and san_ips are only allowed when the profile allows server authentication"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "common_name, san_dns, and san_ips are only allowed when the profile allows server authentication")
|
||||
return
|
||||
}
|
||||
commonName = buildPKIClientCommonName(user)
|
||||
@@ -445,19 +445,19 @@ func (api *API) IssuePKIClientCertForSelf(w http.ResponseWriter, r *http.Request
|
||||
sanURI = profile.SANURIPrefix + user.ID
|
||||
_, err = url.ParseRequestURI(sanURI)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "generated SAN URI is invalid"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "generated SAN URI is invalid")
|
||||
return
|
||||
}
|
||||
serial, err = api.store(r).NextPKICASerial(ca.ID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
serialHex = strconv.FormatInt(serial, 16)
|
||||
certPEM, keyPEM, notBefore, notAfter, err = issuePKIClientCertFromProfile(ca, serial, user, profile, permissions, commonName, sanDNS, sanIPs, sanURI, validSeconds)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_WARN, "client cert issue failed user=%s profile=%s err=%v", user.Username, profile.ID, err)
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
createdByKind, createdBySubjectID, createdBySubjectName, issuanceSource = pkiCertProvenanceFromRequest(r, "client_profile")
|
||||
@@ -482,7 +482,7 @@ func (api *API) IssuePKIClientCertForSelf(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
cert, err = api.store(r).CreatePKICert(cert)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
issuance = models.PKIClientIssuance{
|
||||
@@ -504,12 +504,12 @@ func (api *API) IssuePKIClientCertForSelf(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
issuance, err = api.store(r).CreatePKIClientIssuance(issuance)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
caPEMs, err = api.pkiCertChainPEMs(r.Context(), cert.CAID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_INFO, "client cert issue success user=%s profile=%s cert=%s perms=%s scope=%s", user.Username, profile.ID, cert.ID, profile.AuthzPermissions, profile.AuthzScope)
|
||||
@@ -537,26 +537,26 @@ func (api *API) DownloadPKIClientCertBundleForSelf(w http.ResponseWriter, r *htt
|
||||
}
|
||||
owns, err = api.store(r).UserOwnsPKIClientCert(user.ID, params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !owns {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
cert, err = api.store(r).GetPKICert(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
caPEMs, err = api.pkiCertChainPEMs(r.Context(), cert.CAID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
data, err = buildPKIZipBundle(cert.CommonName, cert.CertPEM, cert.KeyPEM, false, caPEMs)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
@@ -579,21 +579,21 @@ func (api *API) GetPKIClientCertForSelf(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
owns, err = api.store(r).UserOwnsPKIClientCert(user.ID, params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !owns {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
cert, err = api.store(r).GetPKICert(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
caPEMs, err = api.pkiCertChainPEMs(r.Context(), cert.CAID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cert.CACertPEM = joinPKIPEMs(caPEMs)
|
||||
@@ -616,21 +616,21 @@ func (api *API) GetPKIClientCertInspectForSelf(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
owns, err = api.store(r).UserOwnsPKIClientCert(user.ID, params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !owns {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
cert, err = api.store(r).GetPKICert(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
parsed, err = parseCertificateFromPEM(cert.CertPEM)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid certificate"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid certificate")
|
||||
return
|
||||
}
|
||||
dump = buildX509CertificateDump(parsed)
|
||||
@@ -651,16 +651,16 @@ func (api *API) RevokePKIClientCertForSelf(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
owns, err = api.store(r).UserOwnsPKIClientCert(user.ID, params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !owns {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
cert, err = api.store(r).GetPKICert(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
if cert.Status == "revoked" {
|
||||
@@ -669,7 +669,7 @@ func (api *API) RevokePKIClientCertForSelf(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
err = api.store(r).RevokePKICert(cert.ID, "revoked by owner")
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDPKI, codit_logger.LOG_INFO, "client cert revoke success user=%s cert=%s", user.Username, cert.ID)
|
||||
|
||||
@@ -3,12 +3,21 @@ package handlers
|
||||
import "encoding/json"
|
||||
import "net/http"
|
||||
|
||||
import "codit/internal/middleware"
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, status int, value interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
|
||||
func WriteJSONWithErrorReason(w http.ResponseWriter, r* http.Request, status int, reason string) {
|
||||
middleware.RememberHttpStatusReason(r.Context(), reason)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": reason})
|
||||
}
|
||||
|
||||
func DecodeJSON(r *http.Request, target interface{}) error {
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ type sshSessionFileDownloadItem struct {
|
||||
RemotePath string
|
||||
Name string
|
||||
Size int64
|
||||
Audit *sshFileTransferAudit
|
||||
}
|
||||
|
||||
type sshSessionFileCopyItem struct {
|
||||
@@ -80,6 +81,15 @@ type sshSessionFileCopyResponse struct {
|
||||
Failed int `json:"failed"`
|
||||
}
|
||||
|
||||
type sshFileTransferAudit struct {
|
||||
Store *db.Store
|
||||
Item models.SSHFileTransfer
|
||||
Paths []string
|
||||
Bytes int64
|
||||
ErrorText string
|
||||
Finished bool
|
||||
}
|
||||
|
||||
type sshFileTransferContextReader struct {
|
||||
Context context.Context
|
||||
Reader io.Reader
|
||||
@@ -104,6 +114,92 @@ func (w *sshFileTransferContextWriter) Write(p []byte) (int, error) {
|
||||
return w.Writer.Write(p)
|
||||
}
|
||||
|
||||
func (api *API) startSSHFileTransferAudit(store *db.Store, r *http.Request, operation string, sessionItem models.SSHSession, targetSession models.SSHSession, targetDir string) *sshFileTransferAudit {
|
||||
var audit *sshFileTransferAudit
|
||||
var item models.SSHFileTransfer
|
||||
var err error
|
||||
|
||||
item = models.SSHFileTransfer{
|
||||
SessionID: sessionItem.ID,
|
||||
UserID: sessionItem.UserID,
|
||||
Username: sessionItem.Username,
|
||||
ProfileID: sessionItem.ProfileID,
|
||||
ServerID: sessionItem.ServerID,
|
||||
ServerName: sessionItem.ServerName,
|
||||
RemoteUsername: sessionItem.RemoteUsername,
|
||||
Operation: operation,
|
||||
TargetDir: strings.TrimSpace(targetDir),
|
||||
Status: "running",
|
||||
RemoteAddr: requestRemoteAddr(r),
|
||||
UserAgent: strings.TrimSpace(r.UserAgent()),
|
||||
}
|
||||
if operation == "copy_to_session" {
|
||||
item.SourceSessionID = sessionItem.ID
|
||||
item.TargetSessionID = targetSession.ID
|
||||
item.TargetServerID = targetSession.ServerID
|
||||
item.TargetServerName = targetSession.ServerName
|
||||
}
|
||||
item, err = store.CreateSSHFileTransfer(item)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file transfer audit create failed op=%s session=%s err=%s", operation, sessionItem.ID, err.Error())
|
||||
return nil
|
||||
}
|
||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
|
||||
"ssh file transfer start id=%s op=%s session=%s user=%s server=%s target_session=%s target_server=%s target_dir=%q",
|
||||
item.ID,
|
||||
operation,
|
||||
sessionItem.ID,
|
||||
sessionItem.Username,
|
||||
sessionItem.ServerName,
|
||||
targetSession.ID,
|
||||
targetSession.ServerName,
|
||||
targetDir)
|
||||
audit = &sshFileTransferAudit{Store: store, Item: item}
|
||||
return audit
|
||||
}
|
||||
|
||||
func (api *API) finishSSHFileTransferAudit(audit *sshFileTransferAudit, status string) {
|
||||
var err error
|
||||
|
||||
if audit == nil || audit.Finished {
|
||||
return
|
||||
}
|
||||
audit.Finished = true
|
||||
err = audit.Store.FinishSSHFileTransfer(audit.Item.ID, status, audit.Paths, audit.Bytes, audit.ErrorText)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file transfer audit finish failed id=%s op=%s err=%s", audit.Item.ID, audit.Item.Operation, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
|
||||
"ssh file transfer %s id=%s op=%s session=%s user=%s server=%s target_session=%s target_server=%s bytes=%d error=%q",
|
||||
status,
|
||||
audit.Item.ID,
|
||||
audit.Item.Operation,
|
||||
audit.Item.SessionID,
|
||||
audit.Item.Username,
|
||||
audit.Item.ServerName,
|
||||
audit.Item.TargetSessionID,
|
||||
audit.Item.TargetServerName,
|
||||
audit.Bytes,
|
||||
audit.ErrorText)
|
||||
}
|
||||
|
||||
func (api *API) requireSSHFileTransferPermission(w http.ResponseWriter, store *db.Store, user models.User, permission string) bool {
|
||||
var allowed bool
|
||||
var err error
|
||||
|
||||
allowed, err = store.UserHasPermission(user.ID, permission)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
if allowed {
|
||||
return true
|
||||
}
|
||||
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "ssh file transfer permission denied"})
|
||||
return false
|
||||
}
|
||||
|
||||
func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var user models.User
|
||||
var ok bool
|
||||
@@ -124,6 +220,7 @@ func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Requ
|
||||
var password string
|
||||
var otpCode string
|
||||
var seenUploadPaths map[string]bool
|
||||
var audit *sshFileTransferAudit
|
||||
var uploadCount int
|
||||
var opened bool
|
||||
var copied int64
|
||||
@@ -134,6 +231,10 @@ func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Requ
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
store = api.store(r)
|
||||
if !api.requireSSHFileTransferPermission(w, store, user, permissionSSHFileUpload) {
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, sshFileTransferMaxUploadBytes)
|
||||
multipartReader, err = r.MultipartReader()
|
||||
if err != nil {
|
||||
@@ -197,15 +298,23 @@ func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
if !opened {
|
||||
store = api.store(r)
|
||||
sessionItem, err = api.getSSHSessionFileTransferItem(store, user, params["id"])
|
||||
if err != nil {
|
||||
_ = part.Close()
|
||||
api.writeSSHSessionFileTransferError(w, err)
|
||||
return
|
||||
}
|
||||
audit = api.startSSHFileTransferAudit(store, r, "upload", sessionItem, models.SSHSession{}, targetDir)
|
||||
if audit != nil {
|
||||
audit.Paths = append(audit.Paths, remotePath)
|
||||
}
|
||||
sshClient, err = api.openSSHSessionFileTransferClient(store, user, sessionItem, password, otpCode)
|
||||
if err != nil {
|
||||
if audit != nil {
|
||||
audit.ErrorText = err.Error()
|
||||
api.finishSSHFileTransferAudit(audit, "failed")
|
||||
audit = nil
|
||||
}
|
||||
_ = part.Close()
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
@@ -215,6 +324,11 @@ func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Requ
|
||||
defer sshClient.Close()
|
||||
sftpClient, err = sftp.NewClient(sshClient)
|
||||
if err != nil {
|
||||
if audit != nil {
|
||||
audit.ErrorText = "sftp subsystem unavailable: " + err.Error()
|
||||
api.finishSSHFileTransferAudit(audit, "failed")
|
||||
audit = nil
|
||||
}
|
||||
_ = part.Close()
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sftp subsystem unavailable: " + err.Error()})
|
||||
return
|
||||
@@ -223,16 +337,41 @@ func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Requ
|
||||
opened = true
|
||||
}
|
||||
item.Path = remotePath
|
||||
if audit == nil {
|
||||
audit = api.startSSHFileTransferAudit(store, r, "upload", sessionItem, models.SSHSession{}, targetDir)
|
||||
}
|
||||
if audit != nil {
|
||||
if len(audit.Paths) == 0 {
|
||||
audit.Paths = append(audit.Paths, remotePath)
|
||||
}
|
||||
}
|
||||
copied, err = api.sftpUploadFile(r.Context(), sftpClient, targetDir, item.Name, part, overwrite)
|
||||
_ = part.Close()
|
||||
item.Size = copied
|
||||
if err != nil {
|
||||
if r.Context().Err() != nil {
|
||||
if audit != nil {
|
||||
audit.ErrorText = r.Context().Err().Error()
|
||||
api.finishSSHFileTransferAudit(audit, "cancelled")
|
||||
audit = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
item.Error = err.Error()
|
||||
if audit != nil && audit.ErrorText == "" {
|
||||
audit.ErrorText = err.Error()
|
||||
}
|
||||
response.Failed += 1
|
||||
if audit != nil {
|
||||
api.finishSSHFileTransferAudit(audit, "failed")
|
||||
audit = nil
|
||||
}
|
||||
} else {
|
||||
if audit != nil {
|
||||
audit.Bytes += copied
|
||||
api.finishSSHFileTransferAudit(audit, "success")
|
||||
audit = nil
|
||||
}
|
||||
response.Uploaded += 1
|
||||
}
|
||||
response.Items = append(response.Items, item)
|
||||
@@ -257,6 +396,7 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
||||
var item sshSessionFileDownloadItem
|
||||
var remotePath string
|
||||
var seenPaths map[string]bool
|
||||
var audit *sshFileTransferAudit
|
||||
var pathCount int
|
||||
var totalSize int64
|
||||
var contentDisposition string
|
||||
@@ -269,6 +409,10 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
store = api.store(r)
|
||||
if !api.requireSSHFileTransferPermission(w, store, user, permissionSSHFileDownload) {
|
||||
return
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
@@ -279,7 +423,6 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
seenPaths = make(map[string]bool)
|
||||
store = api.store(r)
|
||||
sessionItem, err = api.getSSHSessionFileTransferItem(store, user, params["id"])
|
||||
if err != nil {
|
||||
api.writeSSHSessionFileTransferError(w, err)
|
||||
@@ -319,11 +462,27 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
item, err = api.sftpDownloadMetadata(sftpClient, remotePath)
|
||||
if err != nil {
|
||||
audit = api.startSSHFileTransferAudit(store, r, "download", sessionItem, models.SSHSession{}, "")
|
||||
if audit != nil {
|
||||
audit.Paths = append(audit.Paths, remotePath)
|
||||
}
|
||||
if audit != nil && audit.ErrorText == "" {
|
||||
audit.ErrorText = fmt.Sprintf("%s: %s", remotePath, err.Error())
|
||||
api.finishSSHFileTransferAudit(audit, "failed")
|
||||
audit = nil
|
||||
}
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("%s: %s", remotePath, err.Error())})
|
||||
return
|
||||
}
|
||||
totalSize += item.Size
|
||||
if totalSize > sshFileTransferMaxDownloadBytes {
|
||||
audit = api.startSSHFileTransferAudit(store, r, "download", sessionItem, models.SSHSession{}, "")
|
||||
if audit != nil {
|
||||
audit.Paths = append(audit.Paths, item.RemotePath)
|
||||
audit.ErrorText = "download size exceeds limit"
|
||||
api.finishSSHFileTransferAudit(audit, "failed")
|
||||
audit = nil
|
||||
}
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "download size exceeds limit"})
|
||||
return
|
||||
}
|
||||
@@ -334,9 +493,29 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", contentDisposition)
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", items[0].Size))
|
||||
items[0].Audit = api.startSSHFileTransferAudit(store, r, "download", sessionItem, models.SSHSession{}, "")
|
||||
if items[0].Audit != nil {
|
||||
items[0].Audit.Paths = append(items[0].Audit.Paths, items[0].RemotePath)
|
||||
}
|
||||
err = api.sftpDownloadFile(r.Context(), sftpClient, items[0].RemotePath, w)
|
||||
if err != nil {
|
||||
if items[0].Audit != nil {
|
||||
items[0].Audit.ErrorText = err.Error()
|
||||
if r.Context().Err() != nil {
|
||||
items[0].Audit.ErrorText = r.Context().Err().Error()
|
||||
api.finishSSHFileTransferAudit(items[0].Audit, "cancelled")
|
||||
} else {
|
||||
api.finishSSHFileTransferAudit(items[0].Audit, "failed")
|
||||
}
|
||||
items[0].Audit = nil
|
||||
}
|
||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file download failed session=%s path=%q err=%s", sessionItem.ID, items[0].RemotePath, err.Error())
|
||||
} else {
|
||||
if items[0].Audit != nil {
|
||||
items[0].Audit.Bytes = items[0].Size
|
||||
api.finishSSHFileTransferAudit(items[0].Audit, "success")
|
||||
items[0].Audit = nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -344,7 +523,7 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
||||
contentDisposition = mime.FormatMediaType("attachment", map[string]string{"filename": archiveName})
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
w.Header().Set("Content-Disposition", contentDisposition)
|
||||
err = api.writeSSHSessionFileZip(r.Context(), sftpClient, items, w)
|
||||
err = api.writeSSHSessionFileZip(r.Context(), store, r, sessionItem, sftpClient, items, w)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file zip download failed session=%s err=%s", sessionItem.ID, err.Error())
|
||||
}
|
||||
@@ -369,6 +548,7 @@ func (api *API) CopySSHSessionFilesForSelf(w http.ResponseWriter, r *http.Reques
|
||||
var remotePath string
|
||||
var targetDir string
|
||||
var seenPaths map[string]bool
|
||||
var audit *sshFileTransferAudit
|
||||
var pathCount int
|
||||
var totalSize int64
|
||||
var startedAt time.Time
|
||||
@@ -381,6 +561,10 @@ func (api *API) CopySSHSessionFilesForSelf(w http.ResponseWriter, r *http.Reques
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
store = api.store(r)
|
||||
if !api.requireSSHFileTransferPermission(w, store, user, permissionSSHFileCopyToSession) {
|
||||
return
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
@@ -407,7 +591,6 @@ func (api *API) CopySSHSessionFilesForSelf(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
seenPaths = make(map[string]bool)
|
||||
store = api.store(r)
|
||||
sourceSession, err = api.getSSHSessionFileTransferItem(store, user, params["id"])
|
||||
if err != nil {
|
||||
api.writeSSHSessionFileTransferError(w, err)
|
||||
@@ -472,6 +655,13 @@ func (api *API) CopySSHSessionFilesForSelf(w http.ResponseWriter, r *http.Reques
|
||||
metadata, err = api.sftpDownloadMetadata(sourceSFTPClient, remotePath)
|
||||
if err != nil {
|
||||
item.Error = err.Error()
|
||||
audit = api.startSSHFileTransferAudit(store, r, "copy_to_session", sourceSession, targetSession, targetDir)
|
||||
if audit != nil {
|
||||
audit.Paths = append(audit.Paths, remotePath)
|
||||
audit.ErrorText = fmt.Sprintf("%s: %s", remotePath, err.Error())
|
||||
api.finishSSHFileTransferAudit(audit, "failed")
|
||||
audit = nil
|
||||
}
|
||||
response.Failed += 1
|
||||
response.Items = append(response.Items, item)
|
||||
continue
|
||||
@@ -479,6 +669,13 @@ func (api *API) CopySSHSessionFilesForSelf(w http.ResponseWriter, r *http.Reques
|
||||
totalSize += metadata.Size
|
||||
if totalSize > sshFileTransferMaxDownloadBytes {
|
||||
item.Error = "copy size exceeds limit"
|
||||
audit = api.startSSHFileTransferAudit(store, r, "copy_to_session", sourceSession, targetSession, targetDir)
|
||||
if audit != nil {
|
||||
audit.Paths = append(audit.Paths, metadata.RemotePath)
|
||||
audit.ErrorText = item.Error
|
||||
api.finishSSHFileTransferAudit(audit, "failed")
|
||||
audit = nil
|
||||
}
|
||||
response.Failed += 1
|
||||
response.Items = append(response.Items, item)
|
||||
break
|
||||
@@ -494,10 +691,19 @@ func (api *API) CopySSHSessionFilesForSelf(w http.ResponseWriter, r *http.Reques
|
||||
metadata.RemotePath,
|
||||
item.TargetPath,
|
||||
metadata.Size)
|
||||
audit = api.startSSHFileTransferAudit(store, r, "copy_to_session", sourceSession, targetSession, targetDir)
|
||||
if audit != nil {
|
||||
audit.Paths = append(audit.Paths, metadata.RemotePath)
|
||||
}
|
||||
item.Size, err = api.sftpCopyFile(r.Context(), sourceSFTPClient, targetSFTPClient, metadata.RemotePath, targetDir, metadata.Name, req.Overwrite)
|
||||
duration = time.Since(startedAt)
|
||||
if err != nil {
|
||||
if r.Context().Err() != nil {
|
||||
if audit != nil {
|
||||
audit.ErrorText = r.Context().Err().Error()
|
||||
api.finishSSHFileTransferAudit(audit, "cancelled")
|
||||
audit = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
|
||||
@@ -512,8 +718,18 @@ func (api *API) CopySSHSessionFilesForSelf(w http.ResponseWriter, r *http.Reques
|
||||
duration.Milliseconds(),
|
||||
err.Error())
|
||||
item.Error = err.Error()
|
||||
if audit != nil {
|
||||
audit.ErrorText = err.Error()
|
||||
api.finishSSHFileTransferAudit(audit, "failed")
|
||||
audit = nil
|
||||
}
|
||||
response.Failed += 1
|
||||
} else {
|
||||
if audit != nil {
|
||||
audit.Bytes += item.Size
|
||||
api.finishSSHFileTransferAudit(audit, "success")
|
||||
audit = nil
|
||||
}
|
||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
|
||||
"ssh file copy done source_session=%s source_server=%s target_session=%s target_server=%s source_path=%q target_path=%q size=%d dur_ms=%d",
|
||||
sourceSession.ID,
|
||||
@@ -772,7 +988,7 @@ func (api *API) sftpCopyFile(ctx context.Context, sourceClient *sftp.Client, tar
|
||||
return copied, err
|
||||
}
|
||||
|
||||
func (api *API) writeSSHSessionFileZip(ctx context.Context, client *sftp.Client, items []sshSessionFileDownloadItem, writer io.Writer) error {
|
||||
func (api *API) writeSSHSessionFileZip(ctx context.Context, store *db.Store, r *http.Request, sessionItem models.SSHSession, client *sftp.Client, items []sshSessionFileDownloadItem, writer io.Writer) error {
|
||||
var zipWriter *zip.Writer
|
||||
var zipFile io.Writer
|
||||
var closeErr error
|
||||
@@ -785,10 +1001,29 @@ func (api *API) writeSSHSessionFileZip(ctx context.Context, client *sftp.Client,
|
||||
err = ctx.Err()
|
||||
break
|
||||
}
|
||||
items[i].Audit = api.startSSHFileTransferAudit(store, r, "download", sessionItem, models.SSHSession{}, "")
|
||||
if items[i].Audit != nil {
|
||||
items[i].Audit.Paths = append(items[i].Audit.Paths, items[i].RemotePath)
|
||||
}
|
||||
zipFile, err = zipWriter.Create(sshDownloadEntryName(items[i], i))
|
||||
if err != nil { break }
|
||||
err = api.sftpDownloadFile(ctx, client, items[i].RemotePath, zipFile)
|
||||
if err != nil { break }
|
||||
if items[i].Audit != nil {
|
||||
items[i].Audit.Bytes = items[i].Size
|
||||
api.finishSSHFileTransferAudit(items[i].Audit, "success")
|
||||
items[i].Audit = nil
|
||||
}
|
||||
}
|
||||
if err != nil && i < len(items) && items[i].Audit != nil {
|
||||
items[i].Audit.ErrorText = err.Error()
|
||||
if ctx.Err() != nil {
|
||||
items[i].Audit.ErrorText = ctx.Err().Error()
|
||||
api.finishSSHFileTransferAudit(items[i].Audit, "cancelled")
|
||||
} else {
|
||||
api.finishSSHFileTransferAudit(items[i].Audit, "failed")
|
||||
}
|
||||
items[i].Audit = nil
|
||||
}
|
||||
closeErr = zipWriter.Close()
|
||||
if err == nil && closeErr != nil {
|
||||
|
||||
@@ -118,6 +118,13 @@ type sshSessionListResponse struct {
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
type sshFileTransferListResponse struct {
|
||||
Items []models.SSHFileTransfer `json:"items"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
type sshSessionStreamMessage struct {
|
||||
SessionID string `json:"session_id"`
|
||||
Type string `json:"type"`
|
||||
@@ -1849,6 +1856,64 @@ func (api *API) ListSSHSessionsAdmin(w http.ResponseWriter, r *http.Request, _ m
|
||||
WriteJSON(w, http.StatusOK, sshSessionListResponse{Items: items, Limit: limit, Offset: offset, HasMore: hasMore})
|
||||
}
|
||||
|
||||
func (api *API) ListSSHFileTransfersForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var user models.User
|
||||
var ok bool
|
||||
var limit int
|
||||
var offset int
|
||||
var query string
|
||||
var status string
|
||||
var sessionID string
|
||||
var items []models.SSHFileTransfer
|
||||
var hasMore bool
|
||||
var err error
|
||||
|
||||
user, ok = middleware.UserFromContext(r.Context())
|
||||
if !ok || user.Disabled {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
limit = parseSSHSessionListLimit(r.URL.Query().Get("limit"))
|
||||
offset = parseSSHSessionListOffset(r.URL.Query().Get("offset"))
|
||||
query = strings.TrimSpace(r.URL.Query().Get("q"))
|
||||
status = strings.TrimSpace(r.URL.Query().Get("status"))
|
||||
sessionID = strings.TrimSpace(r.URL.Query().Get("session_id"))
|
||||
items, hasMore, err = api.store(r).ListSSHFileTransfersForUserFiltered(user.ID, limit, offset, query, status, sessionID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, sshFileTransferListResponse{Items: items, Limit: limit, Offset: offset, HasMore: hasMore})
|
||||
}
|
||||
|
||||
func (api *API) ListSSHFileTransfersAdmin(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var limit int
|
||||
var offset int
|
||||
var query string
|
||||
var status string
|
||||
var sessionID string
|
||||
var userID string
|
||||
var items []models.SSHFileTransfer
|
||||
var hasMore bool
|
||||
var err error
|
||||
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
limit = parseSSHSessionListLimit(r.URL.Query().Get("limit"))
|
||||
offset = parseSSHSessionListOffset(r.URL.Query().Get("offset"))
|
||||
query = strings.TrimSpace(r.URL.Query().Get("q"))
|
||||
status = strings.TrimSpace(r.URL.Query().Get("status"))
|
||||
sessionID = strings.TrimSpace(r.URL.Query().Get("session_id"))
|
||||
userID = strings.TrimSpace(r.URL.Query().Get("user_id"))
|
||||
items, hasMore, err = api.store(r).ListSSHFileTransfersFiltered(limit, offset, query, status, sessionID, userID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, sshFileTransferListResponse{Items: items, Limit: limit, Offset: offset, HasMore: hasMore})
|
||||
}
|
||||
|
||||
func (api *API) DisconnectSSHSessionForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var user models.User
|
||||
var ok bool
|
||||
|
||||
@@ -117,7 +117,7 @@ func (api *API) ListSSHUserCAs(w http.ResponseWriter, r *http.Request, _ map[str
|
||||
}
|
||||
items, err = api.store(r).ListSSHUserCAs()
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
for i = 0; i < len(items); i++ {
|
||||
@@ -135,10 +135,10 @@ func (api *API) GetSSHUserCA(w http.ResponseWriter, r *http.Request, params map[
|
||||
item, err = api.store(r).GetSSHUserCA(params["id"])
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, buildSSHUserCASummary(item))
|
||||
@@ -160,34 +160,34 @@ func (api *API) CreateSSHUserCA(w http.ResponseWriter, r *http.Request, _ map[st
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if req.Name == "" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
algorithm, err = normalizeSSHCAAlgorithm(req.Algorithm)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
maxUserValidSeconds, err = normalizeSSHUserCAMaxValidSeconds(req.MaxUserValidSeconds)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.PrivateKeyPEM) == "" {
|
||||
privateKeyPEM, publicKey, fingerprint, err = generateSSHUserCAKeyPair(algorithm)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
signer, err = parseSSHSignerFromPEM(strings.TrimSpace(req.PrivateKeyPEM))
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
privateKeyPEM = strings.TrimSpace(req.PrivateKeyPEM)
|
||||
@@ -208,7 +208,7 @@ func (api *API) CreateSSHUserCA(w http.ResponseWriter, r *http.Request, _ map[st
|
||||
item, err = api.store(r).CreateSSHUserCA(item)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_WARN, "create failed name=%s err=%v", req.Name, err)
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
summary = buildSSHUserCASummary(item)
|
||||
@@ -226,26 +226,26 @@ func (api *API) UpdateSSHUserCA(w http.ResponseWriter, r *http.Request, params m
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if req.Name == "" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
maxUserValidSeconds, err = normalizeSSHUserCAMaxValidSeconds(req.MaxUserValidSeconds)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
item, err = api.store(r).GetSSHUserCA(params["id"])
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
item.Name = req.Name
|
||||
@@ -255,7 +255,7 @@ func (api *API) UpdateSSHUserCA(w http.ResponseWriter, r *http.Request, params m
|
||||
item, err = api.store(r).UpdateSSHUserCA(item)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_WARN, "update failed id=%s err=%v", params["id"], err)
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_INFO, "update success id=%s name=%s enabled=%t", item.ID, item.Name, item.Enabled)
|
||||
@@ -270,7 +270,7 @@ func (api *API) DeleteSSHUserCA(w http.ResponseWriter, r *http.Request, params m
|
||||
err = api.store(r).DeleteSSHUserCA(params["id"])
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_WARN, "delete failed id=%s err=%v", params["id"], err)
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_INFO, "delete success id=%s", params["id"])
|
||||
@@ -285,7 +285,7 @@ func (api *API) DownloadSSHUserCAPublicKey(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
item, err = api.store(r).GetSSHUserCA(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
@@ -305,7 +305,7 @@ func (api *API) DownloadSSHUserCAPublicKeyForSelf(w http.ResponseWriter, r *http
|
||||
}
|
||||
item, err = api.store(r).GetSSHUserCAForUser(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
@@ -321,7 +321,7 @@ func (api *API) DownloadSSHUserCAPrivateKey(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
item, err = api.store(r).GetSSHUserCA(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
@@ -350,43 +350,43 @@ func (api *API) SignSSHUserKey(w http.ResponseWriter, r *http.Request, params ma
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
item, err = api.store(r).GetSSHUserCA(params["id"])
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !item.Enabled {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "SSH user CA is disabled"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "SSH user CA is disabled")
|
||||
return
|
||||
}
|
||||
principals = normalizeSSHPrincipals(req.Principals)
|
||||
if len(principals) == 0 {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one principal is required"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "at least one principal is required")
|
||||
return
|
||||
}
|
||||
sourcePublicKey = strings.TrimSpace(req.PublicKey)
|
||||
sourcePublicKeyFingerprint, err = sshAuthorizedKeyFingerprint(sourcePublicKey)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
validSeconds = req.ValidSeconds
|
||||
response, err = api.signSSHUserKeyWithCA(api.store(r), item, sourcePublicKey, strings.TrimSpace(req.KeyID), principals, validSeconds, 604800)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
err = api.recordSSHUserCAIssuance(r, item, user, "admin", sourcePublicKey, sourcePublicKeyFingerprint, nil, nil, response)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_WARN, "audit write failed ca_id=%s serial=%d err=%v", item.ID, response.Serial, err)
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to record issuance audit"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, "failed to record issuance audit")
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_INFO, "sign success ca_id=%s serial=%d key_id=%s principals=%s", item.ID, response.Serial, response.KeyID, strings.Join(principals, ","))
|
||||
@@ -407,7 +407,7 @@ func (api *API) ListSSHUserCAsForSelf(w http.ResponseWriter, r *http.Request, _
|
||||
}
|
||||
items, err = api.store(r).ListSSHUserCAsForUser()
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
for i = 0; i < len(items); i++ {
|
||||
@@ -429,10 +429,10 @@ func (api *API) GetSSHUserCAForSelf(w http.ResponseWriter, r *http.Request, para
|
||||
item, err = api.store(r).GetSSHUserCAForUser(params["id"])
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, buildSSHUserCASummary(item))
|
||||
@@ -445,22 +445,21 @@ func (api *API) ListSSHUserCAIssuances(w http.ResponseWriter, r *http.Request, p
|
||||
var out []sshUserCAIssuanceSummary
|
||||
var i int
|
||||
var err error
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
if !api.requireAdmin(w, r) { return }
|
||||
ca, err = api.store(r).GetSSHUserCA(params["id"])
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
limit = parseSSHUserCAIssuanceLimit(r.URL.Query().Get("limit"))
|
||||
items, err = api.store(r).ListSSHUserCAIssuancesByCA(ca.ID, limit)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
for i = 0; i < len(items); i++ {
|
||||
@@ -480,19 +479,18 @@ func (api *API) ListSSHUserCAIssuancesAll(w http.ResponseWriter, r *http.Request
|
||||
var ok bool
|
||||
var i int
|
||||
var err error
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
if !api.requireAdmin(w, r) { return }
|
||||
limit = parseSSHUserCAIssuanceLimit(r.URL.Query().Get("limit"))
|
||||
caID = strings.TrimSpace(r.URL.Query().Get("ca_id"))
|
||||
items, err = api.store(r).ListSSHUserCAIssuances(limit, caID)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cas, err = api.store(r).ListSSHUserCAs()
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
caByID = map[string]string{}
|
||||
@@ -520,6 +518,7 @@ func (api *API) ListSSHUserCAIssuancesForSelf(w http.ResponseWriter, r *http.Req
|
||||
var caName string
|
||||
var i int
|
||||
var err error
|
||||
|
||||
user, ok = middleware.UserFromContext(r.Context())
|
||||
if !ok || user.Disabled {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
@@ -528,7 +527,7 @@ func (api *API) ListSSHUserCAIssuancesForSelf(w http.ResponseWriter, r *http.Req
|
||||
limit = parseSSHUserCAIssuanceLimit(r.URL.Query().Get("limit"))
|
||||
items, err = api.store(r).ListSSHUserCAIssuancesForSelf(user.ID, limit)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
caByID = map[string]string{}
|
||||
@@ -563,17 +562,17 @@ func (api *API) InspectSSHCertificate(w http.ResponseWriter, r *http.Request, _
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
key, _, _, _, err = ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(req.Certificate)))
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid SSH certificate"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid SSH certificate")
|
||||
return
|
||||
}
|
||||
cert, ok = key.(*ssh.Certificate)
|
||||
if !ok {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "not an SSH certificate"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "not an SSH certificate")
|
||||
return
|
||||
}
|
||||
dump = buildSSHCertificateDump(cert)
|
||||
@@ -614,36 +613,36 @@ func (api *API) SignSSHUserKeyForSelf(w http.ResponseWriter, r *http.Request, pa
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
item, err = api.store(r).GetSSHUserCA(params["id"])
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "SSH user CA not found")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !item.Enabled {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "SSH user CA is disabled"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "SSH user CA is disabled")
|
||||
return
|
||||
}
|
||||
if !item.AllowUserSign {
|
||||
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "self-sign is not allowed for this CA"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "self-sign is not allowed for this CA")
|
||||
return
|
||||
}
|
||||
now = time.Now().UTC().Unix()
|
||||
grants, err = api.store(r).ListActiveSSHPrincipalGrantsForUser(user.ID, now)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
sourcePublicKey = strings.TrimSpace(req.PublicKey)
|
||||
sourcePublicKeyFingerprint, err = sshAuthorizedKeyFingerprint(sourcePublicKey)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
requestedPrincipals = normalizeSSHPrincipals(req.Principals)
|
||||
@@ -669,7 +668,7 @@ func (api *API) SignSSHUserKeyForSelf(w http.ResponseWriter, r *http.Request, pa
|
||||
for i = 0; i < len(requestedPrincipals); i++ {
|
||||
grantID, ok = grantsByPrincipal[requestedPrincipals[i]]
|
||||
if !ok {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected principal is not granted"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "selected principal is not granted")
|
||||
return
|
||||
}
|
||||
selectedGrantIDs[grantID] = true
|
||||
@@ -680,7 +679,7 @@ func (api *API) SignSSHUserKeyForSelf(w http.ResponseWriter, r *http.Request, pa
|
||||
}
|
||||
}
|
||||
if len(selectedGrantIDs) == 0 {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one granted principal is required"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "at least one granted principal is required")
|
||||
return
|
||||
}
|
||||
for grantID = range selectedGrantIDs {
|
||||
@@ -690,7 +689,7 @@ func (api *API) SignSSHUserKeyForSelf(w http.ResponseWriter, r *http.Request, pa
|
||||
for i = 0; i < len(selectedIDs); i++ {
|
||||
grant, ok = grantsByID[selectedIDs[i]]
|
||||
if !ok {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected grant is not active"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "selected grant is not active")
|
||||
return
|
||||
}
|
||||
for j = 0; j < len(grant.Principals); j++ {
|
||||
@@ -714,21 +713,21 @@ func (api *API) SignSSHUserKeyForSelf(w http.ResponseWriter, r *http.Request, pa
|
||||
response, err = api.signSSHUserKeyWithCA(api.store(r), item, sourcePublicKey, strings.TrimSpace(req.KeyID), principals, validSeconds, item.MaxUserValidSeconds)
|
||||
}
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
for grantID = range selectedGrantIDs {
|
||||
err = api.store(r).MarkSSHPrincipalGrantUsed(grantID)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_WARN, "self-sign grant consume failed ca_id=%s user=%s grant_id=%s err=%v", item.ID, user.Username, grantID, err)
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected principal grant is not active"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "selected principal grant is not active")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = api.recordSSHUserCAIssuance(r, item, user, "self", sourcePublicKey, sourcePublicKeyFingerprint, selectedIDs, selectedGrantNames, response)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_WARN, "self-sign audit write failed ca_id=%s user=%s serial=%d err=%v", item.ID, user.Username, response.Serial, err)
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to record issuance audit"})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, "failed to record issuance audit")
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDSSHCA, codit_logger.LOG_INFO, "self-sign success ca_id=%s user=%s serial=%d key_id=%s", item.ID, user.Username, response.Serial, response.KeyID)
|
||||
|
||||
@@ -8,6 +8,9 @@ import "codit/internal/middleware"
|
||||
import "codit/internal/models"
|
||||
|
||||
const permissionProjectCreate string = "project.create"
|
||||
const permissionSSHFileUpload string = "ssh.file.upload"
|
||||
const permissionSSHFileDownload string = "ssh.file.download"
|
||||
const permissionSSHFileCopyToSession string = "ssh.file.copy_to_session"
|
||||
|
||||
type meResponse struct {
|
||||
ID string `json:"id"`
|
||||
@@ -194,6 +197,12 @@ func normalizeSubjectPermissionName(raw string) (string, error) {
|
||||
switch value {
|
||||
case permissionProjectCreate:
|
||||
return value, nil
|
||||
case permissionSSHFileUpload:
|
||||
return value, nil
|
||||
case permissionSSHFileDownload:
|
||||
return value, nil
|
||||
case permissionSSHFileCopyToSession:
|
||||
return value, nil
|
||||
default:
|
||||
return "", errors.New("unsupported permission")
|
||||
}
|
||||
|
||||
@@ -91,28 +91,39 @@ func AccessLog(logger codit_logger.Logger, next http.Handler) http.Handler {
|
||||
var userLabel string
|
||||
var mtlsInfo ClientCertLogInfo
|
||||
var authReason string
|
||||
var httpStatusReason string
|
||||
var user models.User
|
||||
var logLevel codit_logger.LogLevel
|
||||
var statusCode int
|
||||
var ok bool
|
||||
|
||||
if logger == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
recorder = NewStatusRecorder(w, http.StatusOK)
|
||||
start = time.Now()
|
||||
next.ServeHTTP(recorder, r)
|
||||
duration = time.Since(start)
|
||||
|
||||
userLabel = "-"
|
||||
user, ok = UserFromContext(r.Context())
|
||||
if ok && user.Username != "" {
|
||||
userLabel = user.Username
|
||||
}
|
||||
if ok && user.Username != "" { userLabel = user.Username }
|
||||
|
||||
mtlsInfo = ClientCertLogFields(r)
|
||||
authReason = "-"
|
||||
|
||||
authReason, ok = AuthReasonFromContext(r.Context())
|
||||
if !ok || authReason == "" {
|
||||
authReason = "-"
|
||||
}
|
||||
logger.Write(logIDAPI, codit_logger.LOG_INFO, "method=%s path=%s remote=%s user=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q status=%d dur_ms=%d auth_reason=%s",
|
||||
r.Method, r.URL.Path, r.RemoteAddr, userLabel, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, recorder.Status(), duration.Milliseconds(), authReason)
|
||||
if !ok || authReason == "" { authReason = "-" }
|
||||
|
||||
httpStatusReason, ok = HttpStatusReasonFromContext(r.Context())
|
||||
if !ok || httpStatusReason == "" { httpStatusReason = "-" }
|
||||
|
||||
logLevel = codit_logger.LOG_INFO
|
||||
statusCode = recorder.Status()
|
||||
if statusCode >= http.StatusInternalServerError { logLevel = codit_logger.LOG_ERROR }
|
||||
|
||||
logger.Write(logIDAPI, logLevel, "method=%s path=%s remote=%s user=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q status=%d dur_ms=%d auth_reason=%s reason=%q",
|
||||
r.Method, r.URL.Path, r.RemoteAddr, userLabel, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, statusCode, duration.Milliseconds(), authReason, httpStatusReason)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const userHolderKey ctxKey = "user_holder"
|
||||
const principalHolderKey ctxKey = "principal_holder"
|
||||
const afterCommitKey ctxKey = "after_commit_holder"
|
||||
const authReasonHolderKey ctxKey = "auth_reason_holder"
|
||||
const httpStatusReasonHolderKey ctxKey = "http_status_reason"
|
||||
|
||||
type userHolder struct {
|
||||
User models.User
|
||||
@@ -37,6 +38,11 @@ type authReasonHolder struct {
|
||||
OK bool
|
||||
}
|
||||
|
||||
type httpStatusReasonHolder struct {
|
||||
Reason string
|
||||
OK bool
|
||||
}
|
||||
|
||||
type bufferedResponseWriter struct {
|
||||
header http.Header
|
||||
body bytes.Buffer
|
||||
@@ -53,31 +59,27 @@ type passThroughStatusWriter struct {
|
||||
func WithRequestStore(store *db.Store, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var ctx context.Context
|
||||
var holder *userHolder
|
||||
var principal *principalHolder
|
||||
var afterHolder *afterCommitHolder
|
||||
var authHolder *authReasonHolder
|
||||
|
||||
ctx = r.Context()
|
||||
if _, ok := StoreFromContext(ctx); !ok {
|
||||
ctx = context.WithValue(ctx, storeKey, store)
|
||||
}
|
||||
if _, ok := ctx.Value(userHolderKey).(*userHolder); !ok {
|
||||
holder = &userHolder{}
|
||||
ctx = context.WithValue(ctx, userHolderKey, holder)
|
||||
ctx = context.WithValue(ctx, userHolderKey, &userHolder{})
|
||||
}
|
||||
if _, ok := ctx.Value(principalHolderKey).(*principalHolder); !ok {
|
||||
principal = &principalHolder{}
|
||||
ctx = context.WithValue(ctx, principalHolderKey, principal)
|
||||
ctx = context.WithValue(ctx, principalHolderKey, &principalHolder{})
|
||||
}
|
||||
if _, ok := ctx.Value(afterCommitKey).(*afterCommitHolder); !ok {
|
||||
afterHolder = &afterCommitHolder{}
|
||||
ctx = context.WithValue(ctx, afterCommitKey, afterHolder)
|
||||
ctx = context.WithValue(ctx, afterCommitKey, &afterCommitHolder{})
|
||||
}
|
||||
if _, ok := ctx.Value(authReasonHolderKey).(*authReasonHolder); !ok {
|
||||
authHolder = &authReasonHolder{}
|
||||
ctx = context.WithValue(ctx, authReasonHolderKey, authHolder)
|
||||
ctx = context.WithValue(ctx, authReasonHolderKey, &authReasonHolder{})
|
||||
}
|
||||
if _, ok := ctx.Value(httpStatusReasonHolderKey).(*httpStatusReasonHolder); !ok {
|
||||
ctx = context.WithValue(ctx, httpStatusReasonHolderKey, &httpStatusReasonHolder{})
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -269,9 +271,8 @@ func RememberAuthReason(ctx context.Context, reason string) {
|
||||
var holder *authReasonHolder
|
||||
var ok bool
|
||||
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil { return }
|
||||
|
||||
holder, ok = ctx.Value(authReasonHolderKey).(*authReasonHolder)
|
||||
if ok && holder != nil {
|
||||
holder.Reason = reason
|
||||
@@ -283,9 +284,8 @@ func AuthReasonFromContext(ctx context.Context) (string, bool) {
|
||||
var holder *authReasonHolder
|
||||
var ok bool
|
||||
|
||||
if ctx == nil {
|
||||
return "", false
|
||||
}
|
||||
if ctx == nil { return "", false }
|
||||
|
||||
holder, ok = ctx.Value(authReasonHolderKey).(*authReasonHolder)
|
||||
if ok && holder != nil && holder.OK {
|
||||
return holder.Reason, true
|
||||
@@ -293,6 +293,32 @@ func AuthReasonFromContext(ctx context.Context) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func RememberHttpStatusReason(ctx context.Context, reason string) {
|
||||
var holder *httpStatusReasonHolder
|
||||
var ok bool
|
||||
|
||||
if ctx == nil { return }
|
||||
|
||||
holder, ok = ctx.Value(httpStatusReasonHolderKey).(*httpStatusReasonHolder)
|
||||
if ok && holder != nil {
|
||||
holder.Reason = reason
|
||||
holder.OK = strings.TrimSpace(reason) != ""
|
||||
}
|
||||
}
|
||||
|
||||
func HttpStatusReasonFromContext(ctx context.Context) (string, bool) {
|
||||
var holder *httpStatusReasonHolder
|
||||
var ok bool
|
||||
|
||||
if ctx == nil { return "", false }
|
||||
|
||||
holder, ok = ctx.Value(httpStatusReasonHolderKey).(*httpStatusReasonHolder)
|
||||
if ok && holder != nil && holder.OK {
|
||||
return holder.Reason, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isReadOnlyMethod(method string) bool {
|
||||
return method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions
|
||||
}
|
||||
|
||||
@@ -664,6 +664,32 @@ type SSHSession struct {
|
||||
TranscriptAvailable bool `json:"transcript_available"`
|
||||
}
|
||||
|
||||
type SSHFileTransfer struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
ProfileID string `json:"profile_id"`
|
||||
ServerID string `json:"server_id"`
|
||||
ServerName string `json:"server_name"`
|
||||
RemoteUsername string `json:"remote_username"`
|
||||
Operation string `json:"operation"`
|
||||
SourceSessionID string `json:"source_session_id"`
|
||||
TargetSessionID string `json:"target_session_id"`
|
||||
TargetServerID string `json:"target_server_id"`
|
||||
TargetServerName string `json:"target_server_name"`
|
||||
TargetDir string `json:"target_dir"`
|
||||
Paths []string `json:"paths"`
|
||||
BytesTransferred int64 `json:"bytes_transferred"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
StartedAt int64 `json:"started_at"`
|
||||
FinishedAt int64 `json:"finished_at"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
||||
|
||||
type ServicePrincipal struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
+105
-77
@@ -87,22 +87,22 @@ CREATE TABLE IF NOT EXISTS project_role_bindings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
subject_type TEXT NOT NULL,
|
||||
subject_public_id TEXT NOT NULL,
|
||||
subject_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
UNIQUE (project_id, subject_type, subject_public_id)
|
||||
UNIQUE (project_id, subject_type, subject_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subject_permissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
permission TEXT NOT NULL,
|
||||
subject_type TEXT NOT NULL,
|
||||
subject_public_id TEXT NOT NULL,
|
||||
subject_id INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
UNIQUE (permission, subject_type, subject_public_id)
|
||||
UNIQUE (permission, subject_type, subject_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS repos (
|
||||
@@ -265,19 +265,19 @@ CREATE TABLE IF NOT EXISTS pki_client_profiles (
|
||||
CREATE TABLE IF NOT EXISTS pki_client_profile_targets (
|
||||
profile_id INTEGER NOT NULL,
|
||||
target_type TEXT NOT NULL,
|
||||
target_public_id TEXT NOT NULL,
|
||||
target_id INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (profile_id, target_type, target_public_id),
|
||||
PRIMARY KEY (profile_id, target_type, target_id),
|
||||
FOREIGN KEY (profile_id) REFERENCES pki_client_profiles(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pki_client_issuances (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
cert_public_id TEXT NOT NULL,
|
||||
user_public_id TEXT NOT NULL,
|
||||
cert_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
username TEXT NOT NULL DEFAULT '',
|
||||
profile_public_id TEXT NOT NULL,
|
||||
profile_id INTEGER NOT NULL,
|
||||
profile_name TEXT NOT NULL DEFAULT '',
|
||||
serial_hex TEXT NOT NULL DEFAULT '',
|
||||
common_name TEXT NOT NULL DEFAULT '',
|
||||
@@ -308,8 +308,8 @@ CREATE TABLE IF NOT EXISTS ssh_user_cas (
|
||||
CREATE TABLE IF NOT EXISTS ssh_user_ca_issuances (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
ca_public_id TEXT NOT NULL,
|
||||
issuer_user_public_id TEXT,
|
||||
ca_id INTEGER NOT NULL,
|
||||
issuer_user_id INTEGER,
|
||||
issuer_username TEXT NOT NULL DEFAULT '',
|
||||
issuer_kind TEXT NOT NULL DEFAULT 'unknown',
|
||||
source_public_key TEXT NOT NULL,
|
||||
@@ -325,9 +325,7 @@ CREATE TABLE IF NOT EXISTS ssh_user_ca_issuances (
|
||||
remote_addr TEXT NOT NULL DEFAULT '',
|
||||
user_agent TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (ca_public_id) REFERENCES ssh_user_cas(public_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (issuer_user_public_id) REFERENCES users(public_id) ON DELETE SET NULL,
|
||||
UNIQUE (ca_public_id, serial)
|
||||
UNIQUE (ca_id, serial)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_principal_grants (
|
||||
@@ -342,20 +340,20 @@ CREATE TABLE IF NOT EXISTS ssh_principal_grants (
|
||||
used_count INTEGER NOT NULL DEFAULT 0,
|
||||
disabled INTEGER NOT NULL DEFAULT 0,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
created_by_user_public_id TEXT,
|
||||
created_by_user_id INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (created_by_user_public_id) REFERENCES users(public_id) ON DELETE SET NULL
|
||||
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_principal_grant_targets (
|
||||
grant_public_id TEXT NOT NULL,
|
||||
grant_id INTEGER NOT NULL,
|
||||
target_type TEXT NOT NULL,
|
||||
target_public_id TEXT NOT NULL,
|
||||
target_id INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (grant_public_id, target_type, target_public_id),
|
||||
FOREIGN KEY (grant_public_id) REFERENCES ssh_principal_grants(public_id) ON DELETE CASCADE
|
||||
PRIMARY KEY (grant_id, target_type, target_id),
|
||||
FOREIGN KEY (grant_id) REFERENCES ssh_principal_grants(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_servers (
|
||||
@@ -369,24 +367,25 @@ CREATE TABLE IF NOT EXISTS ssh_servers (
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
host_key_policy TEXT NOT NULL DEFAULT 'strict',
|
||||
owner_scope TEXT NOT NULL DEFAULT 'admin',
|
||||
owner_user_public_id TEXT NOT NULL DEFAULT '',
|
||||
owner_user_id INTEGER,
|
||||
created_by_kind TEXT NOT NULL DEFAULT 'user',
|
||||
created_by_subject_id TEXT NOT NULL DEFAULT '',
|
||||
created_by_subject_name TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_server_host_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
server_public_id TEXT NOT NULL,
|
||||
server_id INTEGER NOT NULL,
|
||||
algorithm TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
fingerprint TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
UNIQUE(server_public_id, fingerprint),
|
||||
FOREIGN KEY (server_public_id) REFERENCES ssh_servers(public_id) ON DELETE CASCADE
|
||||
UNIQUE(server_id, fingerprint),
|
||||
FOREIGN KEY (server_id) REFERENCES ssh_servers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_server_groups (
|
||||
@@ -403,12 +402,12 @@ CREATE TABLE IF NOT EXISTS ssh_server_groups (
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_server_group_members (
|
||||
group_public_id TEXT NOT NULL,
|
||||
server_public_id TEXT NOT NULL,
|
||||
group_id INTEGER NOT NULL,
|
||||
server_id INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (group_public_id, server_public_id),
|
||||
FOREIGN KEY (group_public_id) REFERENCES ssh_server_groups(public_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_public_id) REFERENCES ssh_servers(public_id) ON DELETE CASCADE
|
||||
PRIMARY KEY (group_id, server_id),
|
||||
FOREIGN KEY (group_id) REFERENCES ssh_server_groups(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_id) REFERENCES ssh_servers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_secrets (
|
||||
@@ -431,40 +430,41 @@ CREATE TABLE IF NOT EXISTS ssh_credentials (
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
type TEXT NOT NULL DEFAULT 'private_key',
|
||||
secret_public_id TEXT NOT NULL,
|
||||
secret_id INTEGER NOT NULL,
|
||||
public_key TEXT NOT NULL DEFAULT '',
|
||||
fingerprint TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
owner_scope TEXT NOT NULL DEFAULT 'admin',
|
||||
owner_user_public_id TEXT NOT NULL DEFAULT '',
|
||||
owner_user_id INTEGER,
|
||||
created_by_kind TEXT NOT NULL DEFAULT 'user',
|
||||
created_by_subject_id TEXT NOT NULL DEFAULT '',
|
||||
created_by_subject_name TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (secret_public_id) REFERENCES ssh_secrets(public_id) ON DELETE CASCADE
|
||||
FOREIGN KEY (secret_id) REFERENCES ssh_secrets(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_access_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
server_public_id TEXT NOT NULL,
|
||||
server_id INTEGER NOT NULL,
|
||||
server_target_type TEXT NOT NULL DEFAULT 'server',
|
||||
server_group_public_id TEXT DEFAULT '',
|
||||
server_group_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
remote_username TEXT NOT NULL,
|
||||
auth_method TEXT NOT NULL,
|
||||
second_factor_mode TEXT NOT NULL DEFAULT 'none',
|
||||
owner_scope TEXT NOT NULL DEFAULT 'user',
|
||||
owner_user_public_id TEXT DEFAULT '',
|
||||
owner_user_id INTEGER,
|
||||
allow_user_edit INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
secret_public_id TEXT DEFAULT '',
|
||||
ssh_credential_public_id TEXT DEFAULT '',
|
||||
secret_id INTEGER,
|
||||
ssh_credential_id INTEGER,
|
||||
auth_public_key TEXT NOT NULL DEFAULT '',
|
||||
auth_public_key_fingerprint TEXT NOT NULL DEFAULT '',
|
||||
ssh_user_ca_public_id TEXT DEFAULT '',
|
||||
ssh_user_ca_id INTEGER,
|
||||
ssh_principal_grant_ids_json TEXT NOT NULL DEFAULT '[]',
|
||||
default_cert_valid_seconds INTEGER NOT NULL DEFAULT 3600,
|
||||
max_cert_valid_seconds INTEGER NOT NULL DEFAULT 3600,
|
||||
@@ -475,30 +475,31 @@ CREATE TABLE IF NOT EXISTS ssh_access_profiles (
|
||||
created_by_subject_name TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (server_public_id) REFERENCES ssh_servers(public_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_group_public_id) REFERENCES ssh_server_groups(public_id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (secret_public_id) REFERENCES ssh_secrets(public_id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (ssh_credential_public_id) REFERENCES ssh_credentials(public_id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (ssh_user_ca_public_id) REFERENCES ssh_user_cas(public_id) ON DELETE SET NULL
|
||||
FOREIGN KEY (server_id) REFERENCES ssh_servers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_group_id) REFERENCES ssh_server_groups(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (secret_id) REFERENCES ssh_secrets(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (ssh_credential_id) REFERENCES ssh_credentials(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (ssh_user_ca_id) REFERENCES ssh_user_cas(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_access_profile_targets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
profile_public_id TEXT NOT NULL,
|
||||
profile_id INTEGER NOT NULL,
|
||||
target_type TEXT NOT NULL,
|
||||
target_public_id TEXT NOT NULL,
|
||||
target_id INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
UNIQUE(profile_public_id, target_type, target_public_id),
|
||||
FOREIGN KEY (profile_public_id) REFERENCES ssh_access_profiles(public_id) ON DELETE CASCADE
|
||||
UNIQUE(profile_id, target_type, target_id),
|
||||
FOREIGN KEY (profile_id) REFERENCES ssh_access_profiles(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
profile_public_id TEXT NOT NULL,
|
||||
server_public_id TEXT NOT NULL,
|
||||
user_public_id TEXT NOT NULL,
|
||||
profile_id INTEGER NOT NULL,
|
||||
server_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
remote_username TEXT NOT NULL,
|
||||
auth_method TEXT NOT NULL,
|
||||
@@ -515,10 +516,34 @@ CREATE TABLE IF NOT EXISTS ssh_sessions (
|
||||
ended_at INTEGER NOT NULL DEFAULT 0,
|
||||
remote_addr TEXT NOT NULL DEFAULT '',
|
||||
user_agent TEXT NOT NULL DEFAULT '',
|
||||
error TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_file_transfers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
session_id INTEGER,
|
||||
user_id INTEGER,
|
||||
username TEXT NOT NULL DEFAULT '',
|
||||
profile_id INTEGER,
|
||||
server_id INTEGER,
|
||||
server_name TEXT NOT NULL DEFAULT '',
|
||||
remote_username TEXT NOT NULL DEFAULT '',
|
||||
operation TEXT NOT NULL,
|
||||
source_session_id INTEGER,
|
||||
target_session_id INTEGER,
|
||||
target_server_id INTEGER,
|
||||
target_server_name TEXT NOT NULL DEFAULT '',
|
||||
target_dir TEXT NOT NULL DEFAULT '',
|
||||
paths_json TEXT NOT NULL DEFAULT '[]',
|
||||
bytes_transferred INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY (profile_public_id) REFERENCES ssh_access_profiles(public_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_public_id) REFERENCES ssh_servers(public_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_public_id) REFERENCES users(public_id) ON DELETE CASCADE
|
||||
started_at INTEGER NOT NULL,
|
||||
finished_at INTEGER NOT NULL DEFAULT 0,
|
||||
duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
remote_addr TEXT NOT NULL DEFAULT '',
|
||||
user_agent TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS acme_profiles (
|
||||
@@ -544,20 +569,19 @@ CREATE TABLE IF NOT EXISTS acme_profiles (
|
||||
CREATE TABLE IF NOT EXISTS acme_orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
profile_public_id TEXT NOT NULL,
|
||||
profile_id INTEGER NOT NULL,
|
||||
common_name TEXT NOT NULL,
|
||||
san_dns TEXT NOT NULL DEFAULT '',
|
||||
order_url TEXT NOT NULL DEFAULT '',
|
||||
finalize_url TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
last_error TEXT NOT NULL DEFAULT '',
|
||||
cert_public_id TEXT NOT NULL DEFAULT '',
|
||||
cert_id INTEGER,
|
||||
challenges_json TEXT NOT NULL DEFAULT '[]',
|
||||
csr_pem TEXT NOT NULL DEFAULT '',
|
||||
key_pem TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (profile_public_id) REFERENCES acme_profiles(public_id) ON DELETE CASCADE
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tls_listeners (
|
||||
@@ -712,40 +736,44 @@ CREATE INDEX IF NOT EXISTS idx_user_groups_name ON user_groups(name);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS user_groups_one_all_users ON user_groups(scope) WHERE scope = 'all_users';
|
||||
CREATE INDEX IF NOT EXISTS idx_user_group_members_user ON user_group_members(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_project_role_bindings_project ON project_role_bindings(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_project_role_bindings_subject ON project_role_bindings(subject_type, subject_public_id, project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_subject_permissions_lookup ON subject_permissions(permission, subject_type, subject_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_project_role_bindings_subject ON project_role_bindings(subject_type, subject_id, project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_subject_permissions_lookup ON subject_permissions(permission, subject_type, subject_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_cas_parent ON pki_cas(parent_ca_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_certs_ca ON pki_certs(ca_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_client_profiles_ca_id ON pki_client_profiles(ca_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_client_profile_targets_target ON pki_client_profile_targets(target_type, target_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_client_issuances_user_created_at ON pki_client_issuances(user_public_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_client_issuances_cert_public_id ON pki_client_issuances(cert_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_client_profile_targets_target ON pki_client_profile_targets(target_type, target_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_client_issuances_user_created_at ON pki_client_issuances(user_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_pki_client_issuances_cert_id ON pki_client_issuances(cert_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_ssh_user_cas_fingerprint ON ssh_user_cas(fingerprint);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_user_ca_issuances_ca_created_at ON ssh_user_ca_issuances(ca_public_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_user_ca_issuances_issuer_created_at ON ssh_user_ca_issuances(issuer_user_public_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_user_ca_issuances_ca_created_at ON ssh_user_ca_issuances(ca_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_user_ca_issuances_issuer_created_at ON ssh_user_ca_issuances(issuer_user_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_principal_grants_principal ON ssh_principal_grants(principal);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_principal_grants_active ON ssh_principal_grants(disabled, valid_after, valid_before);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_principal_grant_targets_target ON ssh_principal_grant_targets(target_type, target_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_principal_grant_targets_target ON ssh_principal_grant_targets(target_type, target_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_servers_name ON ssh_servers(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_servers_host_port ON ssh_servers(host, port);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_servers_owner ON ssh_servers(owner_scope, owner_user_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_server_host_keys_server ON ssh_server_host_keys(server_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_servers_owner ON ssh_servers(owner_scope, owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_server_host_keys_server ON ssh_server_host_keys(server_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_server_groups_name ON ssh_server_groups(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_server_groups_enabled ON ssh_server_groups(enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_server_group_members_server ON ssh_server_group_members(server_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_server_group_members_server ON ssh_server_group_members(server_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_secrets_kind ON ssh_secrets(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_credentials_enabled ON ssh_credentials(enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_credentials_fingerprint ON ssh_credentials(fingerprint);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_credentials_owner ON ssh_credentials(owner_scope, owner_user_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profiles_server ON ssh_access_profiles(server_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profiles_server_group ON ssh_access_profiles(server_group_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_credentials_owner ON ssh_credentials(owner_scope, owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profiles_server ON ssh_access_profiles(server_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profiles_server_group ON ssh_access_profiles(server_group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profiles_owner_scope ON ssh_access_profiles(owner_scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profiles_owner_user ON ssh_access_profiles(owner_user_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profiles_owner_user ON ssh_access_profiles(owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profiles_enabled ON ssh_access_profiles(enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profile_targets_profile ON ssh_access_profile_targets(profile_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profile_targets_target ON ssh_access_profile_targets(target_type, target_public_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_sessions_user ON ssh_sessions(user_public_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_sessions_profile ON ssh_sessions(profile_public_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profile_targets_profile ON ssh_access_profile_targets(profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_access_profile_targets_target ON ssh_access_profile_targets(target_type, target_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_sessions_user ON ssh_sessions(user_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_sessions_profile ON ssh_sessions(profile_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_sessions_status ON ssh_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_file_transfers_session ON ssh_file_transfers(session_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_file_transfers_user ON ssh_file_transfers(user_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_file_transfers_server ON ssh_file_transfers(server_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssh_file_transfers_status ON ssh_file_transfers(status, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_cert_principal_bindings_principal_id ON cert_principal_bindings(principal_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_principal_project_roles_project_id ON principal_project_roles(project_id);
|
||||
|
||||
@@ -760,6 +760,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
||||
router.Handle("PATCH", "/api/admin/ssh/access-profiles/:id", api.UpdateSSHAccessProfileAdmin)
|
||||
router.Handle("DELETE", "/api/admin/ssh/access-profiles/:id", api.DeleteSSHAccessProfileAdmin)
|
||||
router.Handle("GET", "/api/admin/ssh/sessions", api.ListSSHSessionsAdmin)
|
||||
router.Handle("GET", "/api/admin/ssh/file-transfers", api.ListSSHFileTransfersAdmin)
|
||||
router.Handle("GET", "/api/admin/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptAdmin)
|
||||
router.Handle("GET", "/api/ssh/user-cas", api.ListSSHUserCAsForSelf)
|
||||
router.Handle("GET", "/api/ssh/user-cas/:id", api.GetSSHUserCAForSelf)
|
||||
@@ -787,6 +788,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
||||
router.Handle("DELETE", "/api/ssh/access-profiles/:id", api.DeleteSSHAccessProfileForSelf)
|
||||
router.Handle("POST", "/api/ssh/access-profiles/:id/connect", api.CreateSSHSessionForSelf)
|
||||
router.Handle("GET", "/api/ssh/sessions", api.ListSSHSessionsForSelf)
|
||||
router.Handle("GET", "/api/ssh/file-transfers", api.ListSSHFileTransfersForSelf)
|
||||
router.Handle("GET", "/api/ssh/stream", api.StreamSSHWorkspaceForSelf)
|
||||
router.Handle("GET", "/api/ssh/sessions/:id", api.GetSSHSessionForSelf)
|
||||
router.Handle("GET", "/api/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptForSelf)
|
||||
|
||||
+32
-31
@@ -5,7 +5,8 @@ The current baseline is a single clean initialization migration. Future schema c
|
||||
|
||||
## Conventions
|
||||
|
||||
- Most durable user-facing IDs are stored as `public_id` in the database and exposed as `id` in JSON APIs.
|
||||
- Durable user-facing IDs are stored only on owning entity rows as `public_id` and exposed as `id` in JSON APIs.
|
||||
- Internal relationships use integer `id` columns. Store code translates between public IDs at the API boundary and internal row IDs at persistence boundaries.
|
||||
- Internal integer `id` columns are local database primary keys and should not be exposed unless a handler explicitly needs them.
|
||||
- `created_at`, `updated_at`, `started_at`, `ended_at`, and similar integer timestamp columns store Unix seconds unless the schema explicitly uses `TIMESTAMP`.
|
||||
- Boolean values are stored as `INTEGER` with `0`/`1` in SQLite.
|
||||
@@ -14,7 +15,7 @@ The current baseline is a single clean initialization migration. Future schema c
|
||||
|
||||
## Relationship Diagrams
|
||||
|
||||
The diagrams below are intentionally split by domain. They show the main logical relationships; a few relationships are implemented through `public_id` string columns rather than strict integer foreign keys.
|
||||
The diagrams below are intentionally split by domain. They show the main logical relationships.
|
||||
|
||||
### Identity And Authorization
|
||||
|
||||
@@ -210,10 +211,10 @@ Stores global permissions assigned directly to users, groups, or service princip
|
||||
| --- | --- |
|
||||
| `permission` | Permission string. |
|
||||
| `subject_type` | `user`, `group`, or `principal`. |
|
||||
| `subject_public_id` | External ID of the subject. |
|
||||
| `subject_id` | Internal row ID of the subject table selected by `subject_type`. |
|
||||
| `created_at`, `updated_at` | Lifecycle timestamps. |
|
||||
|
||||
Unique key: `(permission, subject_type, subject_public_id)`.
|
||||
Unique key: `(permission, subject_type, subject_id)`.
|
||||
|
||||
### `service_principals`
|
||||
|
||||
@@ -307,11 +308,11 @@ Assigns project roles to subjects such as groups or principals.
|
||||
| --- | --- |
|
||||
| `project_id` | Internal project FK. |
|
||||
| `subject_type` | Subject kind. |
|
||||
| `subject_public_id` | External subject ID. |
|
||||
| `subject_id` | Internal row ID of the subject table selected by `subject_type`. |
|
||||
| `role` | Project role. |
|
||||
| `created_at`, `updated_at` | Lifecycle timestamps. |
|
||||
|
||||
Unique key: `(project_id, subject_type, subject_public_id)`.
|
||||
Unique key: `(project_id, subject_type, subject_id)`.
|
||||
|
||||
### `repos`
|
||||
|
||||
@@ -437,7 +438,7 @@ Important fields:
|
||||
|
||||
Maps client certificate profiles to users or groups.
|
||||
|
||||
Primary key: `(profile_id, target_type, target_public_id)`.
|
||||
Primary key: `(profile_id, target_type, target_id)`.
|
||||
|
||||
### `pki_client_issuances`
|
||||
|
||||
@@ -445,9 +446,9 @@ Records self-service client certificate issuance history.
|
||||
|
||||
Important fields:
|
||||
|
||||
- `cert_public_id`: issued cert ID.
|
||||
- `user_public_id`, `username`: requesting user.
|
||||
- `profile_public_id`, `profile_name`: profile snapshot.
|
||||
- `cert_id`: internal issued cert FK.
|
||||
- `user_id`, `username`: internal requesting user FK and username snapshot.
|
||||
- `profile_id`, `profile_name`: internal profile FK and profile name snapshot.
|
||||
- `serial_hex`, `common_name`, `san_uri`: issued cert identity data.
|
||||
- `authz_permissions`, `authz_scope`: authorization snapshot.
|
||||
- `not_before`, `not_after`, `created_at`: validity/history timestamps.
|
||||
@@ -472,11 +473,11 @@ Tracks ACME orders and DNS challenges.
|
||||
|
||||
Important fields:
|
||||
|
||||
- `profile_public_id`
|
||||
- `profile_id`
|
||||
- `common_name`, `san_dns`
|
||||
- `order_url`, `finalize_url`
|
||||
- `status`, `last_error`
|
||||
- `cert_public_id`
|
||||
- `cert_id`
|
||||
- `challenges_json`
|
||||
- `csr_pem`, `key_pem`
|
||||
|
||||
@@ -505,8 +506,8 @@ Records SSH user certificate signing history.
|
||||
|
||||
Important fields:
|
||||
|
||||
- `ca_public_id`: SSH user CA ID.
|
||||
- `issuer_user_public_id`, `issuer_username`, `issuer_kind`: issuer attribution.
|
||||
- `ca_id`: internal SSH user CA FK.
|
||||
- `issuer_user_id`, `issuer_username`, `issuer_kind`: internal issuer user FK and issuer attribution.
|
||||
- `source_public_key`, `source_public_key_fingerprint`: key that was signed.
|
||||
- `certificate`: issued SSH certificate.
|
||||
- `key_id`, `principals_json`: certificate identity data.
|
||||
@@ -528,14 +529,14 @@ Defines grant-only SSH principal authorization.
|
||||
| `max_uses`, `used_count` | Use limit and usage count. |
|
||||
| `disabled` | Disables the grant. |
|
||||
| `reason` | Administrative note. |
|
||||
| `created_by_user_public_id` | Creator user ID. |
|
||||
| `created_by_user_id` | Internal creator user FK. |
|
||||
| `last_used_at` | Last successful use. |
|
||||
|
||||
### `ssh_principal_grant_targets`
|
||||
|
||||
Maps SSH principal grants to users or groups.
|
||||
|
||||
Primary key: `(grant_public_id, target_type, target_public_id)`.
|
||||
Primary key: `(grant_id, target_type, target_id)`.
|
||||
|
||||
### `ssh_servers`
|
||||
|
||||
@@ -548,10 +549,10 @@ Important fields:
|
||||
- `enabled`
|
||||
- `host_key_policy`: `strict` requires an existing pinned host key; `trust_on_first_use` allows the first connection to pin the observed host key.
|
||||
- `owner_scope`: `admin` for centrally managed servers or `user` for self-service personal servers.
|
||||
- `owner_user_public_id`: owner user when `owner_scope = 'user'`.
|
||||
- `owner_user_id`: internal owner user FK when `owner_scope = 'user'`.
|
||||
- `created_by_kind`, `created_by_subject_id`, `created_by_subject_name`
|
||||
|
||||
Self-service server lists use `owner_scope = 'user'` and `owner_user_public_id`, not creator attribution. Admin-created servers stay centrally managed even when the admin actor is a normal user subject.
|
||||
Self-service server lists use `owner_scope = 'user'` and `owner_user_id`, not creator attribution. Admin-created servers stay centrally managed even when the admin actor is a normal user subject.
|
||||
When `host_key_policy = 'trust_on_first_use'`, the first successful SSH connection writes the observed key into `ssh_server_host_keys`; later connections use the pinned-key check.
|
||||
|
||||
### `ssh_server_host_keys`
|
||||
@@ -560,12 +561,12 @@ Stores pinned/known SSH server host keys.
|
||||
|
||||
Important fields:
|
||||
|
||||
- `server_public_id`
|
||||
- `server_id`
|
||||
- `algorithm`
|
||||
- `public_key`
|
||||
- `fingerprint`
|
||||
|
||||
Unique key: `(server_public_id, fingerprint)`.
|
||||
Unique key: `(server_id, fingerprint)`.
|
||||
|
||||
### `ssh_server_groups`
|
||||
|
||||
@@ -584,7 +585,7 @@ Server groups let a single SSH access profile target multiple servers. Users cho
|
||||
|
||||
Maps SSH server groups to member servers.
|
||||
|
||||
Primary key: `(group_public_id, server_public_id)`.
|
||||
Primary key: `(group_id, server_id)`.
|
||||
|
||||
### `ssh_credentials`
|
||||
|
||||
@@ -594,11 +595,11 @@ Important fields:
|
||||
|
||||
- `name`, `description`
|
||||
- `type`: currently `private_key`.
|
||||
- `secret_public_id`: encrypted secret reference in `ssh_secrets`.
|
||||
- `secret_id`: internal encrypted secret FK in `ssh_secrets`.
|
||||
- `public_key`, `fingerprint`: non-secret public key metadata shown in the UI/API.
|
||||
- `enabled`: disabled credentials must not be used for new SSH connections.
|
||||
- `owner_scope`: `admin` or `user`.
|
||||
- `owner_user_public_id`: owner for user-owned credentials.
|
||||
- `owner_user_id`: internal owner user FK for user-owned credentials.
|
||||
- creator attribution fields.
|
||||
|
||||
The private key PEM itself is not stored in this table and is not returned by the API. It is encrypted into the referenced `ssh_secrets` row.
|
||||
@@ -624,17 +625,17 @@ Defines how a user may connect to an SSH server.
|
||||
Important fields:
|
||||
|
||||
- `server_target_type`: `server` or `group`.
|
||||
- `server_public_id`: target server when `server_target_type = 'server'`.
|
||||
- `server_group_public_id`: target group when `server_target_type = 'group'`.
|
||||
- `server_id`: target server when `server_target_type = 'server'`.
|
||||
- `server_group_id`: internal target group FK when `server_target_type = 'group'`.
|
||||
- `remote_username`: remote account name.
|
||||
- `auth_method`: `managed_ssh_cert`, `prompted_password`, `stored_password`, or `stored_private_key`.
|
||||
- `second_factor_mode`: `none` or `prompted_totp`.
|
||||
- `owner_scope`: `admin_shared` or `user`.
|
||||
- `owner_user_public_id`: owner for self-service profiles.
|
||||
- `secret_public_id`: stored credential reference.
|
||||
- `ssh_credential_public_id`: imported SSH credential reference when using a stored private key credential.
|
||||
- `owner_user_id`: internal owner user FK for self-service profiles.
|
||||
- `secret_id`: internal stored secret FK.
|
||||
- `ssh_credential_id`: internal imported SSH credential FK when using a stored private key credential.
|
||||
- `auth_public_key`, `auth_public_key_fingerprint`: public-key metadata.
|
||||
- `ssh_user_ca_public_id`: SSH CA used for managed cert auth.
|
||||
- `ssh_user_ca_id`: internal SSH CA FK used for managed cert auth.
|
||||
- `ssh_principal_grant_ids_json`: grants used for managed cert auth.
|
||||
- `default_cert_valid_seconds`, `max_cert_valid_seconds`: SSH certificate validity bounds for managed-cert connections.
|
||||
- `valid_after`, `valid_before`: optional profile availability window. `0` means unbounded.
|
||||
@@ -646,7 +647,7 @@ Stored private-key and stored-password profiles ultimately reference `ssh_secret
|
||||
|
||||
Maps admin-shared SSH access profiles to users or groups.
|
||||
|
||||
Unique key: `(profile_public_id, target_type, target_public_id)`.
|
||||
Unique key: `(profile_id, target_type, target_id)`.
|
||||
|
||||
### `ssh_sessions`
|
||||
|
||||
@@ -654,7 +655,7 @@ Stores SSH web terminal session history.
|
||||
|
||||
Important fields:
|
||||
|
||||
- `profile_public_id`, `server_public_id`, `user_public_id`: session ownership/context.
|
||||
- `profile_id`, `server_id`, `user_id`: internal session ownership/context FKs.
|
||||
- `username`, `remote_username`: local and remote usernames.
|
||||
- `auth_method`, `second_factor_mode`: auth method snapshot.
|
||||
- `host`, `port`: target snapshot.
|
||||
|
||||
@@ -1196,6 +1196,39 @@ export interface SSHSessionFileCopyResponse {
|
||||
failed: number
|
||||
}
|
||||
|
||||
export interface SSHFileTransfer {
|
||||
id: string
|
||||
session_id: string
|
||||
user_id: string
|
||||
username: string
|
||||
profile_id: string
|
||||
server_id: string
|
||||
server_name: string
|
||||
remote_username: string
|
||||
operation: 'upload' | 'download' | 'copy_to_session' | string
|
||||
source_session_id: string
|
||||
target_session_id: string
|
||||
target_server_id: string
|
||||
target_server_name: string
|
||||
target_dir: string
|
||||
paths: string[]
|
||||
bytes_transferred: number
|
||||
status: string
|
||||
error: string
|
||||
started_at: number
|
||||
finished_at: number
|
||||
duration_ms: number
|
||||
remote_addr: string
|
||||
user_agent: string
|
||||
}
|
||||
|
||||
export interface SSHFileTransferListResponse {
|
||||
items: SSHFileTransfer[]
|
||||
limit: number
|
||||
offset: number
|
||||
has_more: boolean
|
||||
}
|
||||
|
||||
export interface BinaryDownload {
|
||||
data: ArrayBuffer
|
||||
filename: string
|
||||
@@ -1592,6 +1625,25 @@ export const api = {
|
||||
if (params?.status) search.set('status', params.status)
|
||||
return request<SSHSessionListResponse>(`/api/admin/ssh/sessions${search.toString() ? `?${search.toString()}` : ''}`, options)
|
||||
},
|
||||
listSSHFileTransfersForSelf: (params?: { limit?: number; offset?: number; q?: string; status?: string; session_id?: string }, options?: RequestInit) => {
|
||||
const search: URLSearchParams = 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)
|
||||
if (params?.session_id) search.set('session_id', params.session_id)
|
||||
return request<SSHFileTransferListResponse>(`/api/ssh/file-transfers${search.toString() ? `?${search.toString()}` : ''}`, options)
|
||||
},
|
||||
listSSHFileTransfersAdmin: (params?: { limit?: number; offset?: number; q?: string; status?: string; session_id?: string; user_id?: string }, options?: RequestInit) => {
|
||||
const search: URLSearchParams = 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)
|
||||
if (params?.session_id) search.set('session_id', params.session_id)
|
||||
if (params?.user_id) search.set('user_id', params.user_id)
|
||||
return request<SSHFileTransferListResponse>(`/api/admin/ssh/file-transfers${search.toString() ? `?${search.toString()}` : ''}`, options)
|
||||
},
|
||||
getSSHSessionForSelf: (id: string, options?: RequestInit) => request<SSHSession>(`/api/ssh/sessions/${id}`, options),
|
||||
getSSHSessionTranscriptForSelf: (id: string, options?: RequestInit) => requestText(`/api/ssh/sessions/${id}/transcript`, options),
|
||||
getSSHSessionTranscriptAdmin: (id: string, options?: RequestInit) => requestText(`/api/admin/ssh/sessions/${id}/transcript`, options),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import Button from '@mui/material/Button'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Dialog from './ModalDialog'
|
||||
import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import FormDialogContent from './FormDialogContent'
|
||||
import PageAlert from './PageAlert'
|
||||
import SelectField from './SelectField'
|
||||
|
||||
export type UserGroupFormState = {
|
||||
name: string
|
||||
description: string
|
||||
disabled: boolean
|
||||
totpRequired: boolean
|
||||
scope: 'explicit' | 'all_users'
|
||||
}
|
||||
|
||||
type UserGroupFormDialogProps = {
|
||||
open: boolean
|
||||
editID: string
|
||||
dialogError: string | null
|
||||
saving: boolean
|
||||
form: UserGroupFormState
|
||||
setForm: Dispatch<SetStateAction<UserGroupFormState>>
|
||||
onClose: () => void
|
||||
onSave: () => void
|
||||
onClearError: () => void
|
||||
}
|
||||
|
||||
export default function UserGroupFormDialog(props: UserGroupFormDialogProps) {
|
||||
const title: string = props.editID ? 'Edit Group' : 'New Group'
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<FormDialogContent>
|
||||
<PageAlert message={props.dialogError} onClose={props.onClearError} />
|
||||
<TextField
|
||||
label="Name"
|
||||
value={props.form.name}
|
||||
onChange={(event) => props.setForm((prev: UserGroupFormState) => ({ ...prev, name: event.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
value={props.form.description}
|
||||
onChange={(event) => props.setForm((prev: UserGroupFormState) => ({ ...prev, description: event.target.value }))}
|
||||
multiline
|
||||
minRows={2}
|
||||
/>
|
||||
<SelectField
|
||||
select
|
||||
label="Scope"
|
||||
value={props.form.scope}
|
||||
onChange={(event) => props.setForm((prev: UserGroupFormState) => ({ ...prev, scope: event.target.value === 'all_users' ? 'all_users' : 'explicit' }))}
|
||||
>
|
||||
<MenuItem value="explicit">Explicit members</MenuItem>
|
||||
<MenuItem value="all_users">All users</MenuItem>
|
||||
</SelectField>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={props.form.disabled} onChange={(event) => props.setForm((prev: UserGroupFormState) => ({ ...prev, disabled: event.target.checked }))} />}
|
||||
label="Disabled"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={props.form.totpRequired} onChange={(event) => props.setForm((prev: UserGroupFormState) => ({ ...prev, totpRequired: event.target.checked }))} />}
|
||||
label="Require TOTP for members"
|
||||
/>
|
||||
</FormDialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={props.onSave} disabled={props.saving || !props.form.name.trim()}>
|
||||
{props.saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import SSHAccessProfileDetailsDialog from '../components/SSHAccessProfileDetails
|
||||
import SSHServerDetailsDialog from '../components/SSHServerDetailsDialog'
|
||||
import SSHTranscriptDialog from '../components/SSHTranscriptDialog'
|
||||
import PageAlert from '../components/PageAlert'
|
||||
import { api, SSHAccessProfile, SSHServer, SSHSession, SSHSessionListResponse } from '../api'
|
||||
import { api, SSHAccessProfile, SSHFileTransfer, SSHFileTransferListResponse, SSHServer, SSHSession, SSHSessionListResponse } from '../api'
|
||||
|
||||
function fmt(value: number): string {
|
||||
if (!value || value <= 0) {
|
||||
@@ -29,6 +29,28 @@ function sessionTitle(item: SSHSession): string {
|
||||
return `${item.remote_username}@${item.host}:${item.port}`
|
||||
}
|
||||
|
||||
function formatTransferBytes(value: number): string {
|
||||
if (!value || value < 0) { return '0 B' }
|
||||
if (value < 1024) { return `${value} B` }
|
||||
if (value < 1024 * 1024) { return `${(value / 1024).toFixed(1)} KB` }
|
||||
if (value < 1024 * 1024 * 1024) { return `${(value / (1024 * 1024)).toFixed(1)} MB` }
|
||||
return `${(value / (1024 * 1024 * 1024)).toFixed(3)} GB`
|
||||
}
|
||||
|
||||
function formatTransferOperation(value: string): string {
|
||||
if (value === 'copy_to_session') { return 'Copy' }
|
||||
if (value === 'upload') { return 'Upload' }
|
||||
if (value === 'download') { return 'Download' }
|
||||
return value || '-'
|
||||
}
|
||||
|
||||
function transferPathText(item: SSHFileTransfer): string {
|
||||
if (Array.isArray(item.paths) && item.paths.length > 0) {
|
||||
return item.paths.join(', ')
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
function downloadTextFile(filename: string, text: string): void {
|
||||
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
@@ -59,6 +81,16 @@ export default function AdminSSHSessionsPage() {
|
||||
const [limit, setLimit] = useState(25)
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [transferItems, setTransferItems] = useState<SSHFileTransfer[]>([])
|
||||
const [loadingTransfers, setLoadingTransfers] = useState(false)
|
||||
const [transferError, setTransferError] = useState<string | null>(null)
|
||||
const [transferQuery, setTransferQuery] = useState('')
|
||||
const [transferStatus, setTransferStatus] = useState('')
|
||||
const [transferSessionID, setTransferSessionID] = useState('')
|
||||
const [transferUserID, setTransferUserID] = useState('')
|
||||
const [transferLimit, setTransferLimit] = useState(25)
|
||||
const [transferOffset, setTransferOffset] = useState(0)
|
||||
const [transferHasMore, setTransferHasMore] = useState(false)
|
||||
|
||||
const load = useCallback(async (signal?: AbortSignal) => {
|
||||
let profileList: SSHAccessProfile[]
|
||||
@@ -108,6 +140,34 @@ export default function AdminSSHSessionsPage() {
|
||||
}
|
||||
}, [limit, offset, query, status])
|
||||
|
||||
const loadTransfers = useCallback(async (signal?: AbortSignal) => {
|
||||
let response: SSHFileTransferListResponse
|
||||
let nextQuery: string
|
||||
|
||||
setLoadingTransfers(true)
|
||||
setTransferError(null)
|
||||
try {
|
||||
nextQuery = transferQuery.trim()
|
||||
response = await api.listSSHFileTransfersAdmin({
|
||||
limit: transferLimit,
|
||||
offset: transferOffset,
|
||||
q: nextQuery || undefined,
|
||||
status: transferStatus || undefined,
|
||||
session_id: transferSessionID.trim() || undefined,
|
||||
user_id: transferUserID.trim() || undefined
|
||||
}, signal ? { signal } : undefined)
|
||||
setTransferItems(Array.isArray(response.items) ? response.items : [])
|
||||
setTransferHasMore(Boolean(response.has_more))
|
||||
} catch (err) {
|
||||
if (signal?.aborted) { return }
|
||||
setTransferError(err instanceof Error ? err.message : 'Failed to load SSH transfer history')
|
||||
setTransferItems([])
|
||||
setTransferHasMore(false)
|
||||
} finally {
|
||||
if (!signal?.aborted) setLoadingTransfers(false)
|
||||
}
|
||||
}, [transferLimit, transferOffset, transferQuery, transferSessionID, transferStatus, transferUserID])
|
||||
|
||||
useEffect(() => {
|
||||
const ac: AbortController = new AbortController()
|
||||
|
||||
@@ -117,6 +177,15 @@ export default function AdminSSHSessionsPage() {
|
||||
}
|
||||
}, [load])
|
||||
|
||||
useEffect(() => {
|
||||
const ac: AbortController = new AbortController()
|
||||
|
||||
void loadTransfers(ac.signal)
|
||||
return () => {
|
||||
ac.abort()
|
||||
}
|
||||
}, [loadTransfers])
|
||||
|
||||
const openTranscript = useCallback(async (item: SSHSession) => {
|
||||
let text: string
|
||||
|
||||
@@ -161,7 +230,9 @@ export default function AdminSSHSessionsPage() {
|
||||
<Typography variant="h5">Admin: SSH Sessions</Typography>
|
||||
<PageAlert message={error} onClose={() => setError(null)} />
|
||||
<SectionCard
|
||||
title="SSH Session History"
|
||||
collapsible
|
||||
collapseStorageKey="admin-ssh-sessions-session-history"
|
||||
title="Session History"
|
||||
subtitle="All SSH sessions across users."
|
||||
actions={(
|
||||
<Button variant="outlined" size="small" onClick={() => void load()} disabled={loading}>
|
||||
@@ -305,6 +376,159 @@ export default function AdminSSHSessionsPage() {
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionCard>
|
||||
<PageAlert message={transferError} onClose={() => setTransferError(null)} />
|
||||
<SectionCard
|
||||
collapsible
|
||||
collapseStorageKey="admin-ssh-sessions-transfer-history"
|
||||
title="Transfer History"
|
||||
subtitle="SSH file uploads, downloads, and copy-to-session operations across users."
|
||||
actions={(
|
||||
<Button variant="outlined" size="small" onClick={() => void loadTransfers()} disabled={loadingTransfers}>
|
||||
{loadingTransfers ? 'Refreshing...' : 'Refresh'}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search"
|
||||
value={transferQuery}
|
||||
onChange={(event) => {
|
||||
setTransferQuery(event.target.value)
|
||||
setTransferOffset(0)
|
||||
}}
|
||||
sx={{ minWidth: 240, maxWidth: 360 }}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
label="Status"
|
||||
value={transferStatus}
|
||||
onChange={(event) => {
|
||||
setTransferStatus(event.target.value)
|
||||
setTransferOffset(0)
|
||||
}}
|
||||
sx={{ minWidth: 150 }}
|
||||
>
|
||||
<MenuItem value="">(all)</MenuItem>
|
||||
<MenuItem value="success">success</MenuItem>
|
||||
<MenuItem value="failed">failed</MenuItem>
|
||||
<MenuItem value="cancelled">cancelled</MenuItem>
|
||||
<MenuItem value="running">running</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Session ID"
|
||||
value={transferSessionID}
|
||||
onChange={(event) => {
|
||||
setTransferSessionID(event.target.value)
|
||||
setTransferOffset(0)
|
||||
}}
|
||||
sx={{ minWidth: 220 }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="User ID"
|
||||
value={transferUserID}
|
||||
onChange={(event) => {
|
||||
setTransferUserID(event.target.value)
|
||||
setTransferOffset(0)
|
||||
}}
|
||||
sx={{ minWidth: 220 }}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
label="Page Size"
|
||||
value={String(transferLimit)}
|
||||
onChange={(event) => {
|
||||
setTransferLimit(Number(event.target.value) || 25)
|
||||
setTransferOffset(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>
|
||||
{loadingTransfers && !transferItems.length ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Loading...
|
||||
</Typography>
|
||||
) : null}
|
||||
{!loadingTransfers && !transferItems.length ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No SSH transfer history found.
|
||||
</Typography>
|
||||
) : null}
|
||||
{transferItems.length ? (
|
||||
<List>
|
||||
{transferItems.map((item: SSHFileTransfer) => {
|
||||
const statusColor: 'success' | 'error' | 'warning' | 'default' =
|
||||
item.status === 'success' ? 'success' :
|
||||
item.status === 'failed' ? 'error' :
|
||||
item.status === 'cancelled' ? 'warning' :
|
||||
'default'
|
||||
|
||||
return (
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography>
|
||||
{formatTransferOperation(item.operation)} · {item.server_name || item.server_id || '-'}
|
||||
{item.target_server_name ? ` → ${item.target_server_name}` : ''}{' '}
|
||||
<Chip
|
||||
label={item.status || 'unknown'}
|
||||
size="small"
|
||||
color={statusColor}
|
||||
variant={item.status === 'success' ? 'filled' : 'outlined'}
|
||||
/>
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Box sx={{ display: 'grid' }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
Path: {transferPathText(item)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
User: {item.username || item.user_id} · Bytes: {formatTransferBytes(item.bytes_transferred)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
Started: {fmt(item.started_at)} · Finished: {fmt(item.finished_at)} · Duration: {item.duration_ms || 0} ms
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
Transfer: {item.id} · Session: {item.session_id}
|
||||
</Typography>
|
||||
{item.error ? (
|
||||
<Typography variant="caption" color="error" sx={{ wordBreak: 'break-word' }}>
|
||||
{item.error}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
}
|
||||
disableTypography
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
) : null}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Showing {transferItems.length ? transferOffset + 1 : 0}-{transferOffset + transferItems.length}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant="outlined" size="small" onClick={() => setTransferOffset((prev: number) => Math.max(0, prev - transferLimit))} disabled={loadingTransfers || transferOffset <= 0}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outlined" size="small" onClick={() => setTransferOffset((prev: number) => prev + transferLimit)} disabled={loadingTransfers || !transferHasMore}>
|
||||
Next
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionCard>
|
||||
<SSHAccessProfileDetailsDialog
|
||||
item={viewProfile}
|
||||
onClose={() => setViewProfile(null)}
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import CompactListItemText from '../components/CompactListItemText'
|
||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||
import FormDialogContent from '../components/FormDialogContent'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
MenuItem,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
@@ -29,16 +22,34 @@ import TintedPanel from '../components/TintedPanel'
|
||||
import PageAlert from '../components/PageAlert'
|
||||
import { api, SubjectPermission, User, UserGroup, UserGroupMember, UserGroupUpsertPayload } from '../api'
|
||||
import SelectField from '../components/SelectField'
|
||||
import UserGroupFormDialog, { UserGroupFormState } from '../components/UserGroupFormDialog'
|
||||
|
||||
const projectCreatePermission = 'project.create'
|
||||
const projectCreatePermission: string = 'project.create'
|
||||
const sshFileUploadPermission: string = 'ssh.file.upload'
|
||||
const sshFileDownloadPermission: string = 'ssh.file.download'
|
||||
const sshFileCopyToSessionPermission: string = 'ssh.file.copy_to_session'
|
||||
const groupOptionPermissions: { permission: string, label: string, chip: string }[] = [
|
||||
{ permission: projectCreatePermission, label: 'Allow project creation for members', chip: projectCreatePermission },
|
||||
{ permission: sshFileUploadPermission, label: 'Allow SSH file upload', chip: sshFileUploadPermission },
|
||||
{ permission: sshFileDownloadPermission, label: 'Allow SSH file download', chip:sshFileDownloadPermission },
|
||||
{ permission: sshFileCopyToSessionPermission, label: 'Allow SSH file copy between sessions', chip: sshFileCopyToSessionPermission }
|
||||
]
|
||||
|
||||
function fmt(value: number): string {
|
||||
if (!value || value <= 0) {
|
||||
return '-'
|
||||
}
|
||||
if (!value || value <= 0) return '-'
|
||||
return new Date(value * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
function defaultUserGroupForm(): UserGroupFormState {
|
||||
return {
|
||||
name: '',
|
||||
description: '',
|
||||
disabled: false,
|
||||
totpRequired: false,
|
||||
scope: 'explicit'
|
||||
}
|
||||
}
|
||||
|
||||
export default function AdminUserGroupsPage() {
|
||||
const [groups, setGroups] = useState<UserGroup[]>([])
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
@@ -54,11 +65,7 @@ export default function AdminUserGroupsPage() {
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [editID, setEditID] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const [totpRequired, setTOTPRequired] = useState<boolean>(false)
|
||||
const [scope, setScope] = useState<'explicit' | 'all_users'>('explicit')
|
||||
const [form, setForm] = useState<UserGroupFormState>(() => defaultUserGroupForm())
|
||||
|
||||
const [deleteItem, setDeleteItem] = useState<UserGroup | null>(null)
|
||||
const [deleteConfirm, setDeleteConfirm] = useState('')
|
||||
@@ -92,7 +99,7 @@ export default function AdminUserGroupsPage() {
|
||||
try {
|
||||
;[list, permissions] = await Promise.all([
|
||||
api.listUserGroups(),
|
||||
api.listSubjectPermissions(projectCreatePermission, 'group')
|
||||
api.listSubjectPermissions('', 'group')
|
||||
])
|
||||
setGroups(Array.isArray(list) ? list : [])
|
||||
setSubjectPermissions(Array.isArray(permissions) ? permissions : [])
|
||||
@@ -149,22 +156,20 @@ export default function AdminUserGroupsPage() {
|
||||
const openCreate = () => {
|
||||
setDialogError(null)
|
||||
setEditID('')
|
||||
setName('')
|
||||
setDescription('')
|
||||
setDisabled(false)
|
||||
setTOTPRequired(false)
|
||||
setScope('explicit')
|
||||
setForm(defaultUserGroupForm())
|
||||
setEditOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (item: UserGroup) => {
|
||||
setDialogError(null)
|
||||
setEditID(item.id)
|
||||
setName(item.name)
|
||||
setDescription(item.description || '')
|
||||
setDisabled(item.disabled)
|
||||
setTOTPRequired(Boolean(item.totp_required))
|
||||
setScope(item.scope === 'all_users' ? 'all_users' : 'explicit')
|
||||
setForm({
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
disabled: item.disabled,
|
||||
totpRequired: Boolean(item.totp_required),
|
||||
scope: item.scope === 'all_users' ? 'all_users' : 'explicit'
|
||||
})
|
||||
setEditOpen(true)
|
||||
}
|
||||
|
||||
@@ -174,7 +179,13 @@ export default function AdminUserGroupsPage() {
|
||||
setDialogError(null)
|
||||
setError(null)
|
||||
try {
|
||||
payload = { name: name.trim(), description: description.trim(), disabled, totp_required: totpRequired, scope }
|
||||
payload = {
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim(),
|
||||
disabled: form.disabled,
|
||||
totp_required: form.totpRequired,
|
||||
scope: form.scope
|
||||
}
|
||||
if (editID) {
|
||||
await api.updateUserGroup(editID, payload)
|
||||
} else {
|
||||
@@ -263,11 +274,19 @@ export default function AdminUserGroupsPage() {
|
||||
return member.username || member.user_id
|
||||
}
|
||||
|
||||
const hasDirectProjectCreate = (groupID: string) => {
|
||||
return subjectPermissions.some((item) => item.permission === projectCreatePermission && item.subject_id === groupID)
|
||||
const groupOptionCaption = (groupID: string) => {
|
||||
const labels: string[] = groupOptionPermissions
|
||||
.filter((item: { permission: string, label: string, chip: string }) => hasGroupOption(groupID, item.permission))
|
||||
.map((item: { permission: string, label: string, chip: string }) => item.chip)
|
||||
|
||||
return labels.length > 0 ? ` · ${labels.join(', ')}` : ''
|
||||
}
|
||||
|
||||
const toggleSelectedGroupProjectCreate = async (next: boolean) => {
|
||||
const hasGroupOption = (groupID: string, permission: string) => {
|
||||
return subjectPermissions.some((item: SubjectPermission) => item.permission === permission && item.subject_id === groupID)
|
||||
}
|
||||
|
||||
const toggleSelectedGroupOption = async (permission: string, next: boolean) => {
|
||||
if (!selectedGroupID) {
|
||||
return
|
||||
}
|
||||
@@ -276,15 +295,40 @@ export default function AdminUserGroupsPage() {
|
||||
try {
|
||||
if (next) {
|
||||
await api.createSubjectPermissions({
|
||||
permission: projectCreatePermission,
|
||||
permission,
|
||||
targets: [{ subject_type: 'group', subject_id: selectedGroupID }]
|
||||
})
|
||||
} else {
|
||||
await api.deleteSubjectPermission(projectCreatePermission, 'group', selectedGroupID)
|
||||
await api.deleteSubjectPermission(permission, 'group', selectedGroupID)
|
||||
}
|
||||
await loadGroups()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update permissions')
|
||||
setError(err instanceof Error ? err.message : 'Failed to update group options')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelectedGroupTOTPRequired = async (next: boolean) => {
|
||||
let payload: UserGroupUpsertPayload
|
||||
|
||||
if (!selectedGroup) {
|
||||
return
|
||||
}
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
try {
|
||||
payload = {
|
||||
name: selectedGroup.name,
|
||||
description: selectedGroup.description || '',
|
||||
disabled: selectedGroup.disabled,
|
||||
totp_required: next,
|
||||
scope: selectedGroup.scope === 'all_users' ? 'all_users' : 'explicit'
|
||||
}
|
||||
await api.updateUserGroup(selectedGroup.id, payload)
|
||||
await loadGroups()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update group options')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
@@ -334,7 +378,7 @@ export default function AdminUserGroupsPage() {
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="caption" color="text.secondary" sx={{ whiteSpace: 'normal', wordBreak: 'break-all' }}>
|
||||
{group.id} · {group.scope === 'all_users' ? 'all users' : 'explicit'} · updated: {fmt(group.updated_at)}{group.totp_required ? ' · TOTP required' : ''}{hasDirectProjectCreate(group.id) ? ' · project.create' : ''}
|
||||
{group.id} · {group.scope === 'all_users' ? 'all users' : 'explicit'} · updated: {fmt(group.updated_at)}{group.totp_required ? ' · TOTP required' : ''}{groupOptionCaption(group.id)}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
@@ -381,9 +425,11 @@ export default function AdminUserGroupsPage() {
|
||||
color={selectedGroup.disabled ? 'error' : 'success'}
|
||||
label={selectedGroup.disabled ? 'Disabled' : 'Active'}
|
||||
/>
|
||||
{hasDirectProjectCreate(selectedGroup.id) ? (
|
||||
<Chip size="small" variant="outlined" label="project.create" />
|
||||
) : null}
|
||||
{groupOptionPermissions.map((item: { permission: string, label: string, chip: string }) => (
|
||||
hasGroupOption(selectedGroup.id, item.permission) ? (
|
||||
<Chip key={item.permission} size="small" variant="outlined" label={item.chip} />
|
||||
) : null
|
||||
))}
|
||||
{selectedGroup.totp_required ? (
|
||||
<Chip size="small" variant="outlined" label="TOTP required" />
|
||||
) : null}
|
||||
@@ -478,18 +524,33 @@ export default function AdminUserGroupsPage() {
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Global Permissions">
|
||||
<SectionCard title="Member Options">
|
||||
{selectedGroup ? (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={hasDirectProjectCreate(selectedGroupID)}
|
||||
onChange={(event) => toggleSelectedGroupProjectCreate(event.target.checked)}
|
||||
disabled={busy}
|
||||
<Box sx={{ display: 'grid' }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={Boolean(selectedGroup.totp_required)}
|
||||
onChange={(event) => toggleSelectedGroupTOTPRequired(event.target.checked)}
|
||||
disabled={busy}
|
||||
/>
|
||||
}
|
||||
label="Require TOTP for members"
|
||||
/>
|
||||
{groupOptionPermissions.map((item: { permission: string, label: string, chip: string }) => (
|
||||
<FormControlLabel
|
||||
key={item.permission}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={hasGroupOption(selectedGroupID, item.permission)}
|
||||
onChange={(event) => toggleSelectedGroupOption(item.permission, event.target.checked)}
|
||||
disabled={busy}
|
||||
/>
|
||||
}
|
||||
label={item.label}
|
||||
/>
|
||||
}
|
||||
label="Allow project creation for members"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Select a group.
|
||||
@@ -499,43 +560,17 @@ export default function AdminUserGroupsPage() {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Dialog open={editOpen} onClose={() => setEditOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{editID ? 'Edit Group' : 'New Group'}</DialogTitle>
|
||||
<FormDialogContent>
|
||||
<PageAlert message={dialogError} onClose={() => setDialogError(null)} />
|
||||
<TextField label="Name" value={name} onChange={(event) => setName(event.target.value)} />
|
||||
<TextField
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
multiline
|
||||
minRows={2}
|
||||
/>
|
||||
<SelectField
|
||||
select
|
||||
label="Scope"
|
||||
value={scope}
|
||||
onChange={(event) => setScope(event.target.value === 'all_users' ? 'all_users' : 'explicit')}
|
||||
>
|
||||
<MenuItem value="explicit">Explicit members</MenuItem>
|
||||
<MenuItem value="all_users">All users</MenuItem>
|
||||
</SelectField>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={disabled} onChange={(event) => setDisabled(event.target.checked)} />}
|
||||
label="Disabled"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={totpRequired} onChange={(event) => setTOTPRequired(event.target.checked)} />}
|
||||
label="Require TOTP for members"
|
||||
/>
|
||||
</FormDialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={saveGroup} disabled={busy || !name.trim()}>
|
||||
{busy ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
<Button onClick={() => setEditOpen(false)}>Cancel</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<UserGroupFormDialog
|
||||
open={editOpen}
|
||||
editID={editID}
|
||||
dialogError={dialogError}
|
||||
saving={busy}
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
onClose={() => setEditOpen(false)}
|
||||
onSave={saveGroup}
|
||||
onClearError={() => setDialogError(null)}
|
||||
/>
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
open={!!deleteItem}
|
||||
|
||||
@@ -28,7 +28,7 @@ import SSHSessionFileUploadDialog from '../components/SSHSessionFileUploadDialog
|
||||
import SSHTranscriptDialog from '../components/SSHTranscriptDialog'
|
||||
import TintedPanel from '../components/TintedPanel'
|
||||
import PageAlert from '../components/PageAlert'
|
||||
import { api, SSHAccessProfile, SSHServer, SSHSession, SSHSessionConnectResponse, SSHSessionListResponse } from '../api'
|
||||
import { api, SSHAccessProfile, SSHFileTransfer, SSHFileTransferListResponse, SSHServer, SSHSession, SSHSessionConnectResponse, SSHSessionListResponse } from '../api'
|
||||
|
||||
type SSHSessionStreamMessage = {
|
||||
session_id?: string
|
||||
@@ -121,6 +121,28 @@ function formatTimestamp(value: number): string {
|
||||
return new Date(value * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
function formatTransferBytes(value: number): string {
|
||||
if (!value || value < 0) { return '0 B' }
|
||||
if (value < 1024) { return `${value} B` }
|
||||
if (value < 1024 * 1024) { return `${(value / 1024).toFixed(1)} KB` }
|
||||
if (value < 1024 * 1024 * 1024) { return `${(value / (1024 * 1024)).toFixed(1)} MB` }
|
||||
return `${(value / (1024 * 1024 * 1024)).toFixed(3)} GB`
|
||||
}
|
||||
|
||||
function formatTransferOperation(value: string): string {
|
||||
if (value === 'copy_to_session') { return 'Copy' }
|
||||
if (value === 'upload') { return 'Upload' }
|
||||
if (value === 'download') { return 'Download' }
|
||||
return value || '-'
|
||||
}
|
||||
|
||||
function transferPathText(item: SSHFileTransfer): string {
|
||||
if (Array.isArray(item.paths) && item.paths.length > 0) {
|
||||
return item.paths.join(', ')
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
function buildHistorySessionTitle(session: SSHSession): string {
|
||||
if (session.server_name) { return `${session.server_name} (${session.remote_username}@${session.host}:${session.port})` }
|
||||
return `${session.remote_username}@${session.host}:${session.port}`
|
||||
@@ -750,6 +772,16 @@ export default function SSHSessionPage() {
|
||||
const [historyOffset, setHistoryOffset] = useState(0)
|
||||
const [historyHasMore, setHistoryHasMore] = useState(false)
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [transferItems, setTransferItems] = useState<SSHFileTransfer[]>([])
|
||||
const [loadingTransfers, setLoadingTransfers] = useState(false)
|
||||
const [transferError, setTransferError] = useState<string | null>(null)
|
||||
const [transferQuery, setTransferQuery] = useState('')
|
||||
const [transferStatus, setTransferStatus] = useState('')
|
||||
const [transferSessionFilter, setTransferSessionFilter] = useState('')
|
||||
const [transferLimit, setTransferLimit] = useState(25)
|
||||
const [transferOffset, setTransferOffset] = useState(0)
|
||||
const [transferHasMore, setTransferHasMore] = useState(false)
|
||||
const [transfersOpen, setTransfersOpen] = useState(false)
|
||||
const [historyConnectItem, setHistoryConnectItem] = useState<SSHSession | null>(null)
|
||||
const [historyConnectPassword, setHistoryConnectPassword] = useState('')
|
||||
const [historyConnectOTPCode, setHistoryConnectOTPCode] = useState('')
|
||||
@@ -864,6 +896,33 @@ export default function SSHSessionPage() {
|
||||
}
|
||||
}, [historyLimit, historyOffset, historyQuery, historyStatus])
|
||||
|
||||
const loadTransfers = useCallback(async (signal?: AbortSignal) => {
|
||||
let response: SSHFileTransferListResponse
|
||||
let nextQuery: string
|
||||
|
||||
setLoadingTransfers(true)
|
||||
setTransferError(null)
|
||||
try {
|
||||
nextQuery = transferQuery.trim()
|
||||
response = await api.listSSHFileTransfersForSelf({
|
||||
limit: transferLimit,
|
||||
offset: transferOffset,
|
||||
q: nextQuery || undefined,
|
||||
status: transferStatus || undefined,
|
||||
session_id: transferSessionFilter || undefined
|
||||
}, signal ? { signal } : undefined)
|
||||
setTransferItems(Array.isArray(response.items) ? response.items : [])
|
||||
setTransferHasMore(Boolean(response.has_more))
|
||||
} catch (err) {
|
||||
if (signal?.aborted) { return }
|
||||
setTransferError(err instanceof Error ? err.message : 'Failed to load SSH file transfer history')
|
||||
setTransferItems([])
|
||||
setTransferHasMore(false)
|
||||
} finally {
|
||||
if (!signal?.aborted) setLoadingTransfers(false)
|
||||
}
|
||||
}, [transferLimit, transferOffset, transferQuery, transferSessionFilter, transferStatus])
|
||||
|
||||
const loadProfiles = useCallback(async (signal?: AbortSignal) => {
|
||||
let items: SSHAccessProfile[]
|
||||
|
||||
@@ -886,6 +945,12 @@ export default function SSHSessionPage() {
|
||||
return () => { ac.abort() }
|
||||
}, [historyOpen, loadHistory, openSessionIDs.length])
|
||||
|
||||
useEffect(() => {
|
||||
const ac: AbortController = new AbortController()
|
||||
if (transfersOpen) void loadTransfers(ac.signal)
|
||||
return () => { ac.abort() }
|
||||
}, [loadTransfers, openSessionIDs.length, transfersOpen])
|
||||
|
||||
const clampSplitRatio = useCallback((value: number) => {
|
||||
return Math.max(0.25, Math.min(0.75, value))
|
||||
}, [])
|
||||
@@ -982,6 +1047,14 @@ export default function SSHSessionPage() {
|
||||
setHistoryOpen(false)
|
||||
}, [])
|
||||
|
||||
const handleOpenTransfers = useCallback(() => {
|
||||
setTransfersOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseTransfers = useCallback(() => {
|
||||
setTransfersOpen(false)
|
||||
}, [])
|
||||
|
||||
const closeHistoryConnectPrompt = useCallback(() => {
|
||||
if (connectingHistorySession) {
|
||||
return
|
||||
@@ -1079,6 +1152,15 @@ export default function SSHSessionPage() {
|
||||
setHistoryOffset((prev) => prev + historyLimit)
|
||||
}, [historyHasMore, historyLimit])
|
||||
|
||||
const handleTransferPrev = useCallback(() => {
|
||||
setTransferOffset((prev: number) => Math.max(0, prev - transferLimit))
|
||||
}, [transferLimit])
|
||||
|
||||
const handleTransferNext = useCallback(() => {
|
||||
if (!transferHasMore) { return }
|
||||
setTransferOffset((prev: number) => prev + transferLimit)
|
||||
}, [transferHasMore, transferLimit])
|
||||
|
||||
const queueWorkspaceMessage = useCallback((message: SSHSessionStreamMessage) => {
|
||||
const queueLimit: number = 500
|
||||
|
||||
@@ -1469,7 +1551,7 @@ export default function SSHSessionPage() {
|
||||
const historyContent = (
|
||||
<Box sx={{ display: 'grid', gap: 1.5, p: historyDialogMode ? 2 : 2, width: historyDialogMode ? 'auto' : 420, maxWidth: '100%', height: '100%', overflow: 'hidden' }}>
|
||||
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||
<Typography variant="h6">SSH Session History</Typography>
|
||||
<Typography variant="h6">Session History</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Recent SSH sessions for your account.
|
||||
</Typography>
|
||||
@@ -1613,6 +1695,156 @@ export default function SSHSessionPage() {
|
||||
</Box>
|
||||
)
|
||||
|
||||
const transfersContent = (
|
||||
<Box sx={{ display: 'grid', gap: 1.5, p: historyDialogMode ? 2 : 2, width: historyDialogMode ? 'auto' : 480, maxWidth: '100%', height: '100%', overflow: 'hidden' }}>
|
||||
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||
<Typography variant="h6">Transfer History</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Recent upload, download, and copy activity for your account.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search"
|
||||
value={transferQuery}
|
||||
onChange={(event) => {
|
||||
setTransferQuery(event.target.value)
|
||||
setTransferOffset(0)
|
||||
}}
|
||||
sx={{ minWidth: 220, maxWidth: historyDialogMode ? undefined : 320, flex: '1 1 220px' }}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
label="Status"
|
||||
value={transferStatus}
|
||||
onChange={(event) => {
|
||||
setTransferStatus(event.target.value)
|
||||
setTransferOffset(0)
|
||||
}}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
<MenuItem value="">(all)</MenuItem>
|
||||
<MenuItem value="success">success</MenuItem>
|
||||
<MenuItem value="failed">failed</MenuItem>
|
||||
<MenuItem value="cancelled">cancelled</MenuItem>
|
||||
<MenuItem value="running">running</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
label="Session"
|
||||
value={transferSessionFilter}
|
||||
onChange={(event) => {
|
||||
setTransferSessionFilter(event.target.value)
|
||||
setTransferOffset(0)
|
||||
}}
|
||||
sx={{ minWidth: 170 }}
|
||||
>
|
||||
<MenuItem value="">All sessions</MenuItem>
|
||||
{openSessionIDs.map((id: string) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{sessionSummaries[id]?.title || id}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
label="Page Size"
|
||||
value={String(transferLimit)}
|
||||
onChange={(event) => {
|
||||
setTransferLimit(Number(event.target.value) || 25)
|
||||
setTransferOffset(0)
|
||||
}}
|
||||
sx={{ minWidth: 130 }}
|
||||
>
|
||||
<MenuItem value="10">10</MenuItem>
|
||||
<MenuItem value="25">25</MenuItem>
|
||||
<MenuItem value="50">50</MenuItem>
|
||||
</TextField>
|
||||
<Button variant="outlined" size="small" onClick={() => void loadTransfers()} disabled={loadingTransfers}>
|
||||
{loadingTransfers ? 'Refreshing...' : 'Refresh'}
|
||||
</Button>
|
||||
</Box>
|
||||
<PageAlert message={transferError} onClose={() => setTransferError(null)} />
|
||||
<Box sx={{ display: 'grid', gap: 0, minHeight: 0, flex: '1 1 auto', overflowY: 'auto' }}>
|
||||
{loadingTransfers && !transferItems.length ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Loading...
|
||||
</Typography>
|
||||
) : null}
|
||||
{!loadingTransfers && !transferItems.length ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No SSH file transfer history found.
|
||||
</Typography>
|
||||
) : null}
|
||||
{transferItems.map((item: SSHFileTransfer) => {
|
||||
const statusColor: 'success' | 'error' | 'warning' | 'default' =
|
||||
item.status === 'success' ? 'success' :
|
||||
item.status === 'failed' ? 'error' :
|
||||
item.status === 'cancelled' ? 'warning' :
|
||||
'default'
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={item.id}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gap: 0.5,
|
||||
py: 1,
|
||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Box sx={{ minWidth: 0, display: 'grid', gap: 0.25 }}>
|
||||
<Typography variant="subtitle2" sx={{ wordBreak: 'break-word' }}>
|
||||
{formatTransferOperation(item.operation)} · {item.server_name || item.server_id || '-'}
|
||||
{item.target_server_name ? ` → ${item.target_server_name}` : ''}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
Path: {transferPathText(item)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
Bytes: {formatTransferBytes(item.bytes_transferred)} · Started: {formatTimestamp(item.started_at)} · Finished: {formatTimestamp(item.finished_at)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
Session: {item.session_id} · User: {item.username || item.user_id}
|
||||
</Typography>
|
||||
{item.error ? (
|
||||
<Typography variant="caption" color="error" sx={{ wordBreak: 'break-word' }}>
|
||||
{item.error}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
<Chip
|
||||
label={item.status}
|
||||
size="small"
|
||||
color={statusColor}
|
||||
variant={item.status === 'success' ? 'filled' : 'outlined'}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Showing {transferItems.length ? transferOffset + 1 : 0}-{transferOffset + transferItems.length}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant="outlined" size="small" onClick={handleTransferPrev} disabled={loadingTransfers || transferOffset <= 0}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outlined" size="small" onClick={handleTransferNext} disabled={loadingTransfers || !transferHasMore}>
|
||||
Next
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'grid', gridTemplateRows: 'auto auto minmax(0, 1fr)', gap: 1, height: 'calc(100vh - 76px)', minHeight: 0, overflow: 'hidden' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||
@@ -1622,7 +1854,10 @@ export default function SSHSessionPage() {
|
||||
New Session
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleOpenHistory}>
|
||||
History
|
||||
Session History
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleOpenTransfers}>
|
||||
Transfer History
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleLeave}>
|
||||
Close
|
||||
@@ -1850,6 +2085,29 @@ export default function SSHSessionPage() {
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
{historyDialogMode ? (
|
||||
<Dialog open={transfersOpen} onClose={handleCloseTransfers} fullScreen>
|
||||
{transfersContent}
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseTransfers}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : (
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={transfersOpen}
|
||||
onClose={handleCloseTransfers}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
top: { xs: '56px', sm: '64px' },
|
||||
height: { xs: 'calc(100% - 56px)', sm: 'calc(100% - 64px)' }
|
||||
}
|
||||
}}
|
||||
>
|
||||
{transfersContent}
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
<SSHCredentialsPromptDialog
|
||||
open={Boolean(historyConnectItem)}
|
||||
targetText={historyConnectItem ? buildHistorySessionTitle(historyConnectItem) : 'Enter the password for this SSH access profile.'}
|
||||
|
||||
Reference in New Issue
Block a user