Compare commits

..

3 Commits

5 changed files with 415 additions and 4 deletions
+20
View File
@@ -294,6 +294,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
var item models.SSHAccessProfile
var grantIDsJSON string
var serverTagsJSON string
var groupTagsJSON string
rows, err = s.Query(`SELECT
p.public_id,
@@ -308,6 +309,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
COALESCE(g.created_by_subject_name, ''),
COALESCE(g.created_at, 0),
COALESCE(g.updated_at, 0),
COALESCE(g.tags_json, '[]'),
p.name,
p.description,
p.remote_username,
@@ -374,6 +376,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
&item.ServerGroup.CreatedBySubjectName,
&item.ServerGroup.CreatedAt,
&item.ServerGroup.UpdatedAt,
&groupTagsJSON,
&item.Name,
&item.Description,
&item.RemoteUsername,
@@ -425,6 +428,8 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
item.Server.Tags, err = decodeStringList(serverTagsJSON)
if err != nil { return nil, err }
item.ServerGroup.Tags, err = decodeStringList(groupTagsJSON)
if err != nil { return nil, err }
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
if err != nil { return nil, err }
@@ -442,6 +447,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
var err error
var grantIDsJSON string
var serverTagsJSON string
var groupTagsJSON string
row = s.QueryRow(`SELECT
p.public_id,
@@ -456,6 +462,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
COALESCE(g.created_by_subject_name, ''),
COALESCE(g.created_at, 0),
COALESCE(g.updated_at, 0),
COALESCE(g.tags_json, '[]'),
p.name,
p.description,
p.remote_username,
@@ -518,6 +525,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
&item.ServerGroup.CreatedBySubjectName,
&item.ServerGroup.CreatedAt,
&item.ServerGroup.UpdatedAt,
&groupTagsJSON,
&item.Name,
&item.Description,
&item.RemoteUsername,
@@ -573,6 +581,10 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
if err != nil {
return item, err
}
item.ServerGroup.Tags, err = decodeStringList(groupTagsJSON)
if err != nil {
return item, err
}
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
if err != nil {
return item, err
@@ -920,6 +932,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
var item models.SSHAccessProfile
var grantIDsJSON string
var serverTagsJSON string
var groupTagsJSON string
var trimmedUserID string
trimmedUserID = strings.TrimSpace(userID)
@@ -936,6 +949,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
COALESCE(g.created_by_subject_name, ''),
COALESCE(g.created_at, 0),
COALESCE(g.updated_at, 0),
COALESCE(g.tags_json, '[]'),
p.name,
p.description,
p.remote_username,
@@ -1025,6 +1039,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
&item.ServerGroup.CreatedBySubjectName,
&item.ServerGroup.CreatedAt,
&item.ServerGroup.UpdatedAt,
&groupTagsJSON,
&item.Name,
&item.Description,
&item.RemoteUsername,
@@ -1076,6 +1091,8 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
item.Server.Tags, err = decodeStringList(serverTagsJSON)
if err != nil { return nil, err }
item.ServerGroup.Tags, err = decodeStringList(groupTagsJSON)
if err != nil { return nil, err }
item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
if err != nil { return nil, err }
@@ -1094,6 +1111,7 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
var item models.SSHAccessProfile
var grantIDsJSON string
var serverTagsJSON string
var groupTagsJSON string
var trimmedUserID string
var trimmedID string
var err error
@@ -1116,6 +1134,7 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
COALESCE(g.created_by_subject_name, ''),
COALESCE(g.created_at, 0),
COALESCE(g.updated_at, 0),
COALESCE(g.tags_json, '[]'),
p.name,
p.description,
p.remote_username,
@@ -1200,6 +1219,7 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
&item.ServerGroup.CreatedBySubjectName,
&item.ServerGroup.CreatedAt,
&item.ServerGroup.UpdatedAt,
&groupTagsJSON,
&item.Name,
&item.Description,
&item.RemoteUsername,
+28
View File
@@ -85,6 +85,10 @@ erDiagram
ssh_access_profiles ||--o{ ssh_sessions : starts
ssh_servers ||--o{ ssh_sessions : target
users ||--o{ ssh_sessions : opens
ssh_sessions ||..o{ ssh_file_transfers : source
ssh_sessions ||..o{ ssh_file_transfers : target
ssh_servers ||..o{ ssh_file_transfers : target_snapshot
users ||..o{ ssh_file_transfers : actor
```
### TLS And RPM Mirror State
@@ -576,6 +580,7 @@ Important fields:
- `name`, `description`
- `enabled`
- `tags_json`: UI/API tags for filtering and display.
- `created_by_kind`, `created_by_subject_id`, `created_by_subject_name`
- `created_at`, `updated_at`
@@ -668,6 +673,29 @@ Important fields:
Transcripts are stored on disk under the configured data directory, not in this table.
### `ssh_file_transfers`
Stores SSH file transfer audit history.
Important fields:
- `session_id`: internal source session reference when available.
- `user_id`, `username`: user who initiated the transfer.
- `profile_id`, `server_id`, `server_name`: source profile/server context and server-name snapshot.
- `remote_username`: remote account used for the source session.
- `operation`: `upload`, `download`, or `copy_to_session`.
- `source_session_id`, `target_session_id`: source and destination sessions for inter-session copies.
- `target_server_id`, `target_server_name`: destination server context and server-name snapshot for inter-session copies.
- `target_dir`: destination directory for uploads/copies.
- `paths_json`: transferred source path list. Duplicate input paths are de-duplicated before execution.
- `bytes_transferred`: total successfully transferred bytes recorded by the backend.
- `status`: `running`, `success`, `partial`, `failed`, or `cancelled`.
- `error`: transfer error summary.
- `started_at`, `finished_at`, `duration_ms`: lifecycle timing.
- `remote_addr`, `user_agent`: requester metadata.
This table is audit/history data. References are nullable and intentionally tolerate deleted sessions, users, profiles, or servers by keeping string snapshots such as `username`, `server_name`, and `target_server_name`.
## TLS
### `tls_listeners`
+354 -2
View File
@@ -4809,6 +4809,59 @@ paths:
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/api/admin/ssh/file-transfers:
get:
tags:
- Admin SSH
summary: List SSH file transfer audit history
operationId: ListSSHFileTransfersAdmin
x-codit-handler: ListSSHFileTransfersAdmin
parameters:
- name: limit
in: query
required: false
schema:
type: integer
- name: offset
in: query
required: false
schema:
type: integer
- name: q
in: query
required: false
schema:
type: string
- name: status
in: query
required: false
schema:
type: string
- name: session_id
in: query
required: false
schema:
type: string
- name: user_id
in: query
required: false
schema:
type: string
responses:
'200':
description: Successful response.
content:
application/json:
schema:
$ref: '#/components/schemas/SSHFileTransferListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/api/admin/ssh/sessions/{id}/transcript:
get:
tags:
@@ -5685,6 +5738,70 @@ paths:
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/api/ssh/access-profiles/{id}/servers/{serverId}:
get:
tags:
- SSH Self Service
summary: Get one SSH server available through an access profile
operationId: GetSSHAccessProfileServerForSelf
x-codit-handler: GetSSHAccessProfileServerForSelf
parameters:
- name: id
in: path
required: true
schema:
type: string
- name: serverId
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful response.
content:
application/json:
schema:
$ref: '#/components/schemas/SSHServer'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/api/ssh/access-profiles/{id}/server-groups/{serverGroupId}/servers:
get:
tags:
- SSH Self Service
summary: List member servers for a server group available through an access profile
operationId: ListSSHAccessProfileServerGroupMembersForSelf
x-codit-handler: ListSSHAccessProfileServerGroupMembersForSelf
parameters:
- name: id
in: path
required: true
schema:
type: string
- name: serverGroupId
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful response.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/SSHServer'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/api/ssh/access-profiles/{id}/connect:
post:
tags:
@@ -5783,6 +5900,52 @@ paths:
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/api/ssh/file-transfers:
get:
tags:
- SSH Self Service
summary: List SSH file transfer audit history for the current user
operationId: ListSSHFileTransfersForSelf
x-codit-handler: ListSSHFileTransfersForSelf
parameters:
- name: limit
in: query
required: false
schema:
type: integer
- name: offset
in: query
required: false
schema:
type: integer
- name: q
in: query
required: false
schema:
type: string
- name: status
in: query
required: false
schema:
type: string
- name: session_id
in: query
required: false
schema:
type: string
responses:
'200':
description: Successful response.
content:
application/json:
schema:
$ref: '#/components/schemas/SSHFileTransferListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/api/ssh/stream:
get:
tags:
@@ -10114,6 +10277,18 @@ components:
- user
owner_user_id:
type: string
created_by_kind:
type: string
created_by_subject_id:
type: string
created_by_subject_name:
type: string
created_at:
type: integer
format: int64
updated_at:
type: integer
format: int64
editable:
type: boolean
additionalProperties: true
@@ -10155,6 +10330,18 @@ components:
type: string
enabled:
type: boolean
tags:
type: array
items:
type: string
server_ids:
type: array
items:
type: string
servers:
type: array
items:
$ref: '#/components/schemas/SSHServer'
created_by_kind:
type: string
created_by_subject_id:
@@ -10177,6 +10364,10 @@ components:
type: string
enabled:
type: boolean
tags:
type: array
items:
type: string
additionalProperties: false
SSHServerGroupMemberRequest:
type: object
@@ -10262,6 +10453,8 @@ components:
type: string
server_id:
type: string
server:
$ref: '#/components/schemas/SSHServer'
server_target_type:
type: string
enum:
@@ -10269,8 +10462,8 @@ components:
- group
server_group_id:
type: string
server_group_name:
type: string
server_group:
$ref: '#/components/schemas/SSHServerGroup'
name:
type: string
description:
@@ -10294,12 +10487,24 @@ components:
enum:
- admin_shared
- user
owner_user_id:
type: string
allow_user_edit:
type: boolean
ssh_credential_id:
type: string
ssh_credential_name:
type: string
auth_public_key:
type: string
auth_public_key_fingerprint:
type: string
ssh_user_ca_id:
type: string
ssh_principal_grant_ids:
type: array
items:
type: string
default_cert_valid_seconds:
type: integer
format: int64
@@ -10314,9 +10519,45 @@ components:
format: int64
enabled:
type: boolean
created_by_kind:
type: string
created_by_subject_id:
type: string
created_by_subject_name:
type: string
created_at:
type: integer
format: int64
updated_at:
type: integer
format: int64
targets:
type: array
items:
$ref: '#/components/schemas/SSHAccessProfileTarget'
editable:
type: boolean
additionalProperties: true
SSHAccessProfileTarget:
type: object
properties:
profile_id:
type: string
target_type:
type: string
enum:
- user
- group
target_id:
type: string
target_name:
type: string
target_active:
type: boolean
created_at:
type: integer
format: int64
additionalProperties: true
SSHSessionFileDownloadRequest:
type: object
properties:
@@ -10415,16 +10656,53 @@ components:
type: string
server_name:
type: string
user_id:
type: string
username:
type: string
remote_username:
type: string
auth_method:
type: string
enum:
- managed_ssh_cert
- prompted_password
- stored_password
- stored_private_key
second_factor_mode:
type: string
enum:
- none
- prompted_totp
host:
type: string
port:
type: integer
status:
type: string
host_key_fingerprint:
type: string
requested_term:
type: string
requested_cols:
type: integer
requested_rows:
type: integer
started_at:
type: integer
format: int64
connected_at:
type: integer
format: int64
ended_at:
type: integer
format: int64
remote_addr:
type: string
user_agent:
type: string
error:
type: string
transcript_available:
type: boolean
additionalProperties: true
@@ -10442,3 +10720,77 @@ components:
has_more:
type: boolean
additionalProperties: true
SSHFileTransfer:
type: object
properties:
id:
type: string
session_id:
type: string
user_id:
type: string
username:
type: string
profile_id:
type: string
server_id:
type: string
server_name:
type: string
remote_username:
type: string
operation:
type: string
enum:
- upload
- download
- copy_to_session
source_session_id:
type: string
target_session_id:
type: string
target_server_id:
type: string
target_server_name:
type: string
target_dir:
type: string
paths:
type: array
items:
type: string
bytes_transferred:
type: integer
format: int64
status:
type: string
error:
type: string
started_at:
type: integer
format: int64
finished_at:
type: integer
format: int64
duration_ms:
type: integer
format: int64
remote_addr:
type: string
user_agent:
type: string
additionalProperties: true
SSHFileTransferListResponse:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/SSHFileTransfer'
limit:
type: integer
offset:
type: integer
has_more:
type: boolean
additionalProperties: true
+1 -1
View File
@@ -759,7 +759,7 @@ export default function SSHServersPage() {
{item.name} ({item.id})
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
{(item.tags || []).map((tag: string) => (
<Chip key={tag} label={tag} size="small" variant="outlined" sx={{ ml: 0.5 }} />
<Chip key={tag} label={tag} size="small" sx={{ ml: 0.5 }} />
))}
</Typography>
}
+12 -1
View File
@@ -193,6 +193,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
const [uploadDialogOpen, setUploadDialogOpen] = useState(false)
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false)
const [copyDialogOpen, setCopyDialogOpen] = useState(false)
const [terminalFocused, setTerminalFocused] = useState(false)
const terminalHostRef = useRef<HTMLDivElement | null>(null)
const terminalRef = useRef<Terminal | null>(null)
const fitAddonRef = useRef<FitAddon | null>(null)
@@ -341,8 +342,14 @@ function SessionTerminalPanel(props: SessionPanelProps) {
term.loadAddon(unicodeAddon)
term.unicode.activeVersion = '11'
term.open(terminalHostRef.current)
// attach the event listeners before calling term.focus()
// otherwise, we lost the first focus.
term.textarea?.addEventListener('focus', () => { setTerminalFocused(true) })
term.textarea?.addEventListener('blur', () => { setTerminalFocused(false) })
fitAddon.fit()
term.focus()
term.writeln('Connecting...')
terminalRef.current = term
fitAddonRef.current = fitAddon
@@ -420,6 +427,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
fitAddonRef.current = null
unicodeAddonRef.current = null
terminalRef.current = null
setTerminalFocused(false)
unregisterStream(sessionID)
term?.dispose()
}
@@ -628,7 +636,10 @@ function SessionTerminalPanel(props: SessionPanelProps) {
flexDirection: 'column',
gap: 0.75,
overflow: 'hidden',
minWidth: 0
minWidth: 0,
outline: (terminalFocused && openSessionIDs.length > 1) ? (theme) => `3px solid ${theme.palette.primary.main}` : '3px solid transparent',
outlineOffset: '-3px',
transition: 'outline-color 0.15s ease'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap', px: 0.25 }}>