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 item models.SSHAccessProfile
var grantIDsJSON string var grantIDsJSON string
var serverTagsJSON string var serverTagsJSON string
var groupTagsJSON string
rows, err = s.Query(`SELECT rows, err = s.Query(`SELECT
p.public_id, p.public_id,
@@ -308,6 +309,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
COALESCE(g.created_by_subject_name, ''), COALESCE(g.created_by_subject_name, ''),
COALESCE(g.created_at, 0), COALESCE(g.created_at, 0),
COALESCE(g.updated_at, 0), COALESCE(g.updated_at, 0),
COALESCE(g.tags_json, '[]'),
p.name, p.name,
p.description, p.description,
p.remote_username, p.remote_username,
@@ -374,6 +376,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
&item.ServerGroup.CreatedBySubjectName, &item.ServerGroup.CreatedBySubjectName,
&item.ServerGroup.CreatedAt, &item.ServerGroup.CreatedAt,
&item.ServerGroup.UpdatedAt, &item.ServerGroup.UpdatedAt,
&groupTagsJSON,
&item.Name, &item.Name,
&item.Description, &item.Description,
&item.RemoteUsername, &item.RemoteUsername,
@@ -425,6 +428,8 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
item.Server.Tags, err = decodeStringList(serverTagsJSON) item.Server.Tags, err = decodeStringList(serverTagsJSON)
if err != nil { return nil, err } 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) item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
if err != nil { return nil, err } if err != nil { return nil, err }
@@ -442,6 +447,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
var err error var err error
var grantIDsJSON string var grantIDsJSON string
var serverTagsJSON string var serverTagsJSON string
var groupTagsJSON string
row = s.QueryRow(`SELECT row = s.QueryRow(`SELECT
p.public_id, 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_by_subject_name, ''),
COALESCE(g.created_at, 0), COALESCE(g.created_at, 0),
COALESCE(g.updated_at, 0), COALESCE(g.updated_at, 0),
COALESCE(g.tags_json, '[]'),
p.name, p.name,
p.description, p.description,
p.remote_username, p.remote_username,
@@ -518,6 +525,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
&item.ServerGroup.CreatedBySubjectName, &item.ServerGroup.CreatedBySubjectName,
&item.ServerGroup.CreatedAt, &item.ServerGroup.CreatedAt,
&item.ServerGroup.UpdatedAt, &item.ServerGroup.UpdatedAt,
&groupTagsJSON,
&item.Name, &item.Name,
&item.Description, &item.Description,
&item.RemoteUsername, &item.RemoteUsername,
@@ -573,6 +581,10 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
if err != nil { if err != nil {
return item, err return item, err
} }
item.ServerGroup.Tags, err = decodeStringList(groupTagsJSON)
if err != nil {
return item, err
}
item.Targets, err = s.listSSHAccessProfileTargets(item.ID) item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
if err != nil { if err != nil {
return item, err return item, err
@@ -920,6 +932,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
var item models.SSHAccessProfile var item models.SSHAccessProfile
var grantIDsJSON string var grantIDsJSON string
var serverTagsJSON string var serverTagsJSON string
var groupTagsJSON string
var trimmedUserID string var trimmedUserID string
trimmedUserID = strings.TrimSpace(userID) 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_by_subject_name, ''),
COALESCE(g.created_at, 0), COALESCE(g.created_at, 0),
COALESCE(g.updated_at, 0), COALESCE(g.updated_at, 0),
COALESCE(g.tags_json, '[]'),
p.name, p.name,
p.description, p.description,
p.remote_username, p.remote_username,
@@ -1025,6 +1039,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
&item.ServerGroup.CreatedBySubjectName, &item.ServerGroup.CreatedBySubjectName,
&item.ServerGroup.CreatedAt, &item.ServerGroup.CreatedAt,
&item.ServerGroup.UpdatedAt, &item.ServerGroup.UpdatedAt,
&groupTagsJSON,
&item.Name, &item.Name,
&item.Description, &item.Description,
&item.RemoteUsername, &item.RemoteUsername,
@@ -1076,6 +1091,8 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
item.Server.Tags, err = decodeStringList(serverTagsJSON) item.Server.Tags, err = decodeStringList(serverTagsJSON)
if err != nil { return nil, err } 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) item.Targets, err = s.listSSHAccessProfileTargets(item.ID)
if err != nil { return nil, err } 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 item models.SSHAccessProfile
var grantIDsJSON string var grantIDsJSON string
var serverTagsJSON string var serverTagsJSON string
var groupTagsJSON string
var trimmedUserID string var trimmedUserID string
var trimmedID string var trimmedID string
var err error 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_by_subject_name, ''),
COALESCE(g.created_at, 0), COALESCE(g.created_at, 0),
COALESCE(g.updated_at, 0), COALESCE(g.updated_at, 0),
COALESCE(g.tags_json, '[]'),
p.name, p.name,
p.description, p.description,
p.remote_username, p.remote_username,
@@ -1200,6 +1219,7 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
&item.ServerGroup.CreatedBySubjectName, &item.ServerGroup.CreatedBySubjectName,
&item.ServerGroup.CreatedAt, &item.ServerGroup.CreatedAt,
&item.ServerGroup.UpdatedAt, &item.ServerGroup.UpdatedAt,
&groupTagsJSON,
&item.Name, &item.Name,
&item.Description, &item.Description,
&item.RemoteUsername, &item.RemoteUsername,
+28
View File
@@ -85,6 +85,10 @@ erDiagram
ssh_access_profiles ||--o{ ssh_sessions : starts ssh_access_profiles ||--o{ ssh_sessions : starts
ssh_servers ||--o{ ssh_sessions : target ssh_servers ||--o{ ssh_sessions : target
users ||--o{ ssh_sessions : opens 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 ### TLS And RPM Mirror State
@@ -576,6 +580,7 @@ Important fields:
- `name`, `description` - `name`, `description`
- `enabled` - `enabled`
- `tags_json`: UI/API tags for filtering and display.
- `created_by_kind`, `created_by_subject_id`, `created_by_subject_name` - `created_by_kind`, `created_by_subject_id`, `created_by_subject_name`
- `created_at`, `updated_at` - `created_at`, `updated_at`
@@ -668,6 +673,29 @@ Important fields:
Transcripts are stored on disk under the configured data directory, not in this table. 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
### `tls_listeners` ### `tls_listeners`
+354 -2
View File
@@ -4809,6 +4809,59 @@ paths:
$ref: '#/components/responses/Forbidden' $ref: '#/components/responses/Forbidden'
'404': '404':
$ref: '#/components/responses/NotFound' $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: /api/admin/ssh/sessions/{id}/transcript:
get: get:
tags: tags:
@@ -5685,6 +5738,70 @@ paths:
$ref: '#/components/responses/Unauthorized' $ref: '#/components/responses/Unauthorized'
'404': '404':
$ref: '#/components/responses/NotFound' $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: /api/ssh/access-profiles/{id}/connect:
post: post:
tags: tags:
@@ -5783,6 +5900,52 @@ paths:
$ref: '#/components/responses/Unauthorized' $ref: '#/components/responses/Unauthorized'
'404': '404':
$ref: '#/components/responses/NotFound' $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: /api/ssh/stream:
get: get:
tags: tags:
@@ -10114,6 +10277,18 @@ components:
- user - user
owner_user_id: owner_user_id:
type: string 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: editable:
type: boolean type: boolean
additionalProperties: true additionalProperties: true
@@ -10155,6 +10330,18 @@ components:
type: string type: string
enabled: enabled:
type: boolean 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: created_by_kind:
type: string type: string
created_by_subject_id: created_by_subject_id:
@@ -10177,6 +10364,10 @@ components:
type: string type: string
enabled: enabled:
type: boolean type: boolean
tags:
type: array
items:
type: string
additionalProperties: false additionalProperties: false
SSHServerGroupMemberRequest: SSHServerGroupMemberRequest:
type: object type: object
@@ -10262,6 +10453,8 @@ components:
type: string type: string
server_id: server_id:
type: string type: string
server:
$ref: '#/components/schemas/SSHServer'
server_target_type: server_target_type:
type: string type: string
enum: enum:
@@ -10269,8 +10462,8 @@ components:
- group - group
server_group_id: server_group_id:
type: string type: string
server_group_name: server_group:
type: string $ref: '#/components/schemas/SSHServerGroup'
name: name:
type: string type: string
description: description:
@@ -10294,12 +10487,24 @@ components:
enum: enum:
- admin_shared - admin_shared
- user - user
owner_user_id:
type: string
allow_user_edit:
type: boolean
ssh_credential_id: ssh_credential_id:
type: string type: string
ssh_credential_name:
type: string
auth_public_key:
type: string
auth_public_key_fingerprint: auth_public_key_fingerprint:
type: string type: string
ssh_user_ca_id: ssh_user_ca_id:
type: string type: string
ssh_principal_grant_ids:
type: array
items:
type: string
default_cert_valid_seconds: default_cert_valid_seconds:
type: integer type: integer
format: int64 format: int64
@@ -10314,9 +10519,45 @@ components:
format: int64 format: int64
enabled: enabled:
type: boolean 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: editable:
type: boolean type: boolean
additionalProperties: true 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: SSHSessionFileDownloadRequest:
type: object type: object
properties: properties:
@@ -10415,16 +10656,53 @@ components:
type: string type: string
server_name: server_name:
type: string type: string
user_id:
type: string
username: username:
type: string type: string
remote_username: remote_username:
type: string type: string
auth_method: auth_method:
type: string 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: status:
type: string type: string
host_key_fingerprint: host_key_fingerprint:
type: string 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: transcript_available:
type: boolean type: boolean
additionalProperties: true additionalProperties: true
@@ -10442,3 +10720,77 @@ components:
has_more: has_more:
type: boolean type: boolean
additionalProperties: true 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.name} ({item.id})
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)} {item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
{(item.tags || []).map((tag: string) => ( {(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> </Typography>
} }
+12 -1
View File
@@ -193,6 +193,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
const [uploadDialogOpen, setUploadDialogOpen] = useState(false) const [uploadDialogOpen, setUploadDialogOpen] = useState(false)
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false) const [downloadDialogOpen, setDownloadDialogOpen] = useState(false)
const [copyDialogOpen, setCopyDialogOpen] = useState(false) const [copyDialogOpen, setCopyDialogOpen] = useState(false)
const [terminalFocused, setTerminalFocused] = useState(false)
const terminalHostRef = useRef<HTMLDivElement | null>(null) const terminalHostRef = useRef<HTMLDivElement | null>(null)
const terminalRef = useRef<Terminal | null>(null) const terminalRef = useRef<Terminal | null>(null)
const fitAddonRef = useRef<FitAddon | null>(null) const fitAddonRef = useRef<FitAddon | null>(null)
@@ -341,8 +342,14 @@ function SessionTerminalPanel(props: SessionPanelProps) {
term.loadAddon(unicodeAddon) term.loadAddon(unicodeAddon)
term.unicode.activeVersion = '11' term.unicode.activeVersion = '11'
term.open(terminalHostRef.current) 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() fitAddon.fit()
term.focus() term.focus()
term.writeln('Connecting...') term.writeln('Connecting...')
terminalRef.current = term terminalRef.current = term
fitAddonRef.current = fitAddon fitAddonRef.current = fitAddon
@@ -420,6 +427,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
fitAddonRef.current = null fitAddonRef.current = null
unicodeAddonRef.current = null unicodeAddonRef.current = null
terminalRef.current = null terminalRef.current = null
setTerminalFocused(false)
unregisterStream(sessionID) unregisterStream(sessionID)
term?.dispose() term?.dispose()
} }
@@ -628,7 +636,10 @@ function SessionTerminalPanel(props: SessionPanelProps) {
flexDirection: 'column', flexDirection: 'column',
gap: 0.75, gap: 0.75,
overflow: 'hidden', 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 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap', px: 0.25 }}>