Refine audit UI and vault settings placement
This commit is contained in:
@@ -165,210 +165,214 @@ const (
|
||||
)
|
||||
|
||||
type ui struct {
|
||||
mode string
|
||||
theme *material.Theme
|
||||
logoHorizontal paint.ImageOp
|
||||
splashSquare paint.ImageOp
|
||||
search widget.Editor
|
||||
vaultPath widget.Editor
|
||||
saveAsPath widget.Editor
|
||||
remoteBaseURL widget.Editor
|
||||
remotePath widget.Editor
|
||||
remoteUsername widget.Editor
|
||||
remotePassword widget.Editor
|
||||
masterPassword widget.Editor
|
||||
keyFilePath widget.Editor
|
||||
apiTokenName widget.Editor
|
||||
apiTokenClientName widget.Editor
|
||||
apiTokenExpiresAt widget.Editor
|
||||
apiPolicyOperation widget.Editor
|
||||
apiPolicyPath widget.Editor
|
||||
apiPolicyEntryID widget.Editor
|
||||
securityCipher widget.Editor
|
||||
securityKDF widget.Editor
|
||||
entryID widget.Editor
|
||||
entryTitle widget.Editor
|
||||
entryUsername widget.Editor
|
||||
entryPassword widget.Editor
|
||||
entryURL widget.Editor
|
||||
entryNotes widget.Editor
|
||||
entryTags widget.Editor
|
||||
entryPath widget.Editor
|
||||
entryFields widget.Editor
|
||||
customFieldKeys []widget.Editor
|
||||
customFieldValues []widget.Editor
|
||||
historyIndex widget.Editor
|
||||
groupName widget.Editor
|
||||
groupParentPath widget.Editor
|
||||
passwordProfile widget.Editor
|
||||
attachmentName widget.Editor
|
||||
attachmentPath widget.Editor
|
||||
exportAttachmentPath widget.Editor
|
||||
list widget.List
|
||||
groupList widget.List
|
||||
detailList widget.List
|
||||
apiPolicyList widget.List
|
||||
lifecycleList widget.List
|
||||
copyUser widget.Clickable
|
||||
copyPass widget.Clickable
|
||||
copyURL widget.Clickable
|
||||
lockVault widget.Clickable
|
||||
unlockVault widget.Clickable
|
||||
createVault widget.Clickable
|
||||
openVault widget.Clickable
|
||||
saveVault widget.Clickable
|
||||
saveAsVault widget.Clickable
|
||||
openRemote widget.Clickable
|
||||
changeMasterKey widget.Clickable
|
||||
synchronizeVault widget.Clickable
|
||||
toggleSyncMenu widget.Clickable
|
||||
openAdvancedSync widget.Clickable
|
||||
openSecuritySettings widget.Clickable
|
||||
closeAdvancedSync widget.Clickable
|
||||
closeSecuritySettings widget.Clickable
|
||||
runAdvancedSync widget.Clickable
|
||||
saveSecuritySettings widget.Clickable
|
||||
editEntry widget.Clickable
|
||||
cancelEdit widget.Clickable
|
||||
pickVaultPath widget.Clickable
|
||||
pickKeyFile widget.Clickable
|
||||
pickSyncLocalPath widget.Clickable
|
||||
clearVaultSelection widget.Clickable
|
||||
clearRemoteSelection widget.Clickable
|
||||
dismissBanner widget.Clickable
|
||||
addEntry widget.Clickable
|
||||
saveEntry widget.Clickable
|
||||
duplicateEntry widget.Clickable
|
||||
deleteEntry widget.Clickable
|
||||
restoreEntry widget.Clickable
|
||||
saveTemplate widget.Clickable
|
||||
deleteTemplate widget.Clickable
|
||||
instantiateTemplate widget.Clickable
|
||||
addAttachment widget.Clickable
|
||||
replaceAttachment widget.Clickable
|
||||
removeAttachment widget.Clickable
|
||||
exportAttachment widget.Clickable
|
||||
restoreHistory widget.Clickable
|
||||
generatePassword widget.Clickable
|
||||
goToRootGroup widget.Clickable
|
||||
goToParentGroup widget.Clickable
|
||||
createGroup widget.Clickable
|
||||
moveGroup widget.Clickable
|
||||
renameGroup widget.Clickable
|
||||
deleteGroup widget.Clickable
|
||||
confirmDeleteGroup widget.Clickable
|
||||
cancelDeleteGroup widget.Clickable
|
||||
addCustomField widget.Clickable
|
||||
toggleGroupControls widget.Clickable
|
||||
toggleLifecycleAdvanced widget.Clickable
|
||||
toggleHistory widget.Clickable
|
||||
togglePasswordInline widget.Clickable
|
||||
toggleSyncPassword widget.Clickable
|
||||
showEntries widget.Clickable
|
||||
showTemplates widget.Clickable
|
||||
showRecycle widget.Clickable
|
||||
showAPITokens widget.Clickable
|
||||
showAPIAudit widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
showSyncLocal widget.Clickable
|
||||
showSyncRemote widget.Clickable
|
||||
showSyncPull widget.Clickable
|
||||
showSyncPush widget.Clickable
|
||||
allowApproval widget.Clickable
|
||||
denyApproval widget.Clickable
|
||||
cancelApproval widget.Clickable
|
||||
cancelLifecycleProgress widget.Clickable
|
||||
retryLifecycleOpen widget.Clickable
|
||||
approvalPermanent widget.Bool
|
||||
rememberRemoteAuth widget.Bool
|
||||
apiPolicyAllow widget.Bool
|
||||
apiPolicyGroupScopeW widget.Bool
|
||||
apiTokenDisabled widget.Bool
|
||||
entryClicks []widget.Clickable
|
||||
apiTokenClicks []widget.Clickable
|
||||
apiPolicyRemoves []widget.Clickable
|
||||
apiAuditClicks []widget.Clickable
|
||||
historyClicks []widget.Clickable
|
||||
attachmentClicks []widget.Clickable
|
||||
breadcrumbs []widget.Clickable
|
||||
groupClicks []widget.Clickable
|
||||
recentVaultClicks []widget.Clickable
|
||||
recentRemoteClicks []widget.Clickable
|
||||
removeCustomFields []widget.Clickable
|
||||
state appstate.State
|
||||
visible []entry
|
||||
currentPath []string
|
||||
syncedPath []string
|
||||
selectedHistoryIndex int
|
||||
showPassword bool
|
||||
generatedPasswordDraft bool
|
||||
togglePassword widget.Clickable
|
||||
copyAPITokenSecret widget.Clickable
|
||||
issueAPIToken widget.Clickable
|
||||
saveAPIToken widget.Clickable
|
||||
rotateAPIToken widget.Clickable
|
||||
disableAPIToken widget.Clickable
|
||||
revokeAPIToken widget.Clickable
|
||||
deleteAPIToken widget.Clickable
|
||||
addAPIPolicyRule widget.Clickable
|
||||
phoneSplit widget.Float
|
||||
splitDrag gesture.Drag
|
||||
splitBase float32
|
||||
splitStartY float32
|
||||
phoneSpan int
|
||||
eyeIcon *widget.Icon
|
||||
eyeOffIcon *widget.Icon
|
||||
copyIcon *widget.Icon
|
||||
expandMoreIcon *widget.Icon
|
||||
expandLessIcon *widget.Icon
|
||||
chevronDownIcon *widget.Icon
|
||||
settingsIcon *widget.Icon
|
||||
clipboardWriter clipboard.Writer
|
||||
loadingMessage string
|
||||
loadingActionLabel string
|
||||
lifecycleMode string
|
||||
syncSourceMode syncSourceMode
|
||||
syncDirection syncDirection
|
||||
syncLocalPath widget.Editor
|
||||
syncRemoteBaseURL widget.Editor
|
||||
syncRemotePath widget.Editor
|
||||
syncRemoteUsername widget.Editor
|
||||
syncRemotePassword widget.Editor
|
||||
syncDialogOpen bool
|
||||
syncMenuOpen bool
|
||||
securityDialogOpen bool
|
||||
showSyncPassword bool
|
||||
keyboardFocus focusID
|
||||
defaultSaveAsPath string
|
||||
recentVaultsPath string
|
||||
uiPreferencesPath string
|
||||
recentRemotesPath string
|
||||
autofillCachePath string
|
||||
editingEntry bool
|
||||
groupControlsHidden bool
|
||||
lifecycleAdvancedHidden bool
|
||||
historyHidden bool
|
||||
recentVaults []string
|
||||
recentRemotes []recentRemoteRecord
|
||||
recentVaultGroups map[string][]string
|
||||
recentVaultUsedAt map[string]time.Time
|
||||
entriesState entriesSectionState
|
||||
deleteGroupPath []string
|
||||
apiPolicyGroupScope bool
|
||||
apiTokenSecret string
|
||||
selectedAuditIndex int
|
||||
statusExpiresAt time.Time
|
||||
now func() time.Time
|
||||
apiHost *api.Host
|
||||
auditLog *apiaudit.Log
|
||||
grpcAddress string
|
||||
backgroundResults chan backgroundActionResult
|
||||
backgroundActionSerial int
|
||||
activeBackgroundAction int
|
||||
lastLifecycleAction string
|
||||
requestMasterPassFocus bool
|
||||
invalidate func()
|
||||
mode string
|
||||
theme *material.Theme
|
||||
logoHorizontal paint.ImageOp
|
||||
splashSquare paint.ImageOp
|
||||
search widget.Editor
|
||||
vaultPath widget.Editor
|
||||
saveAsPath widget.Editor
|
||||
remoteBaseURL widget.Editor
|
||||
remotePath widget.Editor
|
||||
remoteUsername widget.Editor
|
||||
remotePassword widget.Editor
|
||||
masterPassword widget.Editor
|
||||
keyFilePath widget.Editor
|
||||
apiTokenName widget.Editor
|
||||
apiTokenClientName widget.Editor
|
||||
apiTokenExpiresAt widget.Editor
|
||||
apiPolicyOperation widget.Editor
|
||||
apiPolicyPath widget.Editor
|
||||
apiPolicyEntryID widget.Editor
|
||||
securityCipher widget.Editor
|
||||
securityKDF widget.Editor
|
||||
entryID widget.Editor
|
||||
entryTitle widget.Editor
|
||||
entryUsername widget.Editor
|
||||
entryPassword widget.Editor
|
||||
entryURL widget.Editor
|
||||
entryNotes widget.Editor
|
||||
entryTags widget.Editor
|
||||
entryPath widget.Editor
|
||||
entryFields widget.Editor
|
||||
customFieldKeys []widget.Editor
|
||||
customFieldValues []widget.Editor
|
||||
historyIndex widget.Editor
|
||||
groupName widget.Editor
|
||||
groupParentPath widget.Editor
|
||||
passwordProfile widget.Editor
|
||||
attachmentName widget.Editor
|
||||
attachmentPath widget.Editor
|
||||
exportAttachmentPath widget.Editor
|
||||
list widget.List
|
||||
groupList widget.List
|
||||
detailList widget.List
|
||||
apiPolicyList widget.List
|
||||
lifecycleList widget.List
|
||||
copyUser widget.Clickable
|
||||
copyPass widget.Clickable
|
||||
copyURL widget.Clickable
|
||||
lockVault widget.Clickable
|
||||
unlockVault widget.Clickable
|
||||
createVault widget.Clickable
|
||||
openVault widget.Clickable
|
||||
saveVault widget.Clickable
|
||||
saveAsVault widget.Clickable
|
||||
openRemote widget.Clickable
|
||||
changeMasterKey widget.Clickable
|
||||
synchronizeVault widget.Clickable
|
||||
toggleSyncMenu widget.Clickable
|
||||
openAdvancedSync widget.Clickable
|
||||
openSecuritySettings widget.Clickable
|
||||
closeAdvancedSync widget.Clickable
|
||||
closeSecuritySettings widget.Clickable
|
||||
runAdvancedSync widget.Clickable
|
||||
saveSecuritySettings widget.Clickable
|
||||
editEntry widget.Clickable
|
||||
cancelEdit widget.Clickable
|
||||
pickVaultPath widget.Clickable
|
||||
pickKeyFile widget.Clickable
|
||||
pickSyncLocalPath widget.Clickable
|
||||
clearVaultSelection widget.Clickable
|
||||
clearRemoteSelection widget.Clickable
|
||||
dismissBanner widget.Clickable
|
||||
addEntry widget.Clickable
|
||||
saveEntry widget.Clickable
|
||||
duplicateEntry widget.Clickable
|
||||
deleteEntry widget.Clickable
|
||||
restoreEntry widget.Clickable
|
||||
saveTemplate widget.Clickable
|
||||
deleteTemplate widget.Clickable
|
||||
instantiateTemplate widget.Clickable
|
||||
addAttachment widget.Clickable
|
||||
replaceAttachment widget.Clickable
|
||||
removeAttachment widget.Clickable
|
||||
exportAttachment widget.Clickable
|
||||
restoreHistory widget.Clickable
|
||||
generatePassword widget.Clickable
|
||||
goToRootGroup widget.Clickable
|
||||
goToParentGroup widget.Clickable
|
||||
createGroup widget.Clickable
|
||||
moveGroup widget.Clickable
|
||||
renameGroup widget.Clickable
|
||||
deleteGroup widget.Clickable
|
||||
confirmDeleteGroup widget.Clickable
|
||||
cancelDeleteGroup widget.Clickable
|
||||
addCustomField widget.Clickable
|
||||
toggleGroupControls widget.Clickable
|
||||
toggleLifecycleAdvanced widget.Clickable
|
||||
toggleHistory widget.Clickable
|
||||
togglePasswordInline widget.Clickable
|
||||
toggleSyncPassword widget.Clickable
|
||||
showEntries widget.Clickable
|
||||
showTemplates widget.Clickable
|
||||
showRecycle widget.Clickable
|
||||
showAPITokens widget.Clickable
|
||||
showAPIAudit widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
showSyncLocal widget.Clickable
|
||||
showSyncRemote widget.Clickable
|
||||
showSyncPull widget.Clickable
|
||||
showSyncPush widget.Clickable
|
||||
allowApproval widget.Clickable
|
||||
denyApproval widget.Clickable
|
||||
cancelApproval widget.Clickable
|
||||
cancelLifecycleProgress widget.Clickable
|
||||
retryLifecycleOpen widget.Clickable
|
||||
approvalPermanent widget.Bool
|
||||
rememberRemoteAuth widget.Bool
|
||||
apiPolicyAllow widget.Bool
|
||||
apiPolicyGroupScopeW widget.Bool
|
||||
apiTokenDisabled widget.Bool
|
||||
entryClicks []widget.Clickable
|
||||
apiTokenClicks []widget.Clickable
|
||||
apiPolicyRemoves []widget.Clickable
|
||||
apiAuditClicks []widget.Clickable
|
||||
apiAuditTokenFilters []widget.Clickable
|
||||
apiAuditDecisionFilters []widget.Clickable
|
||||
apiAuditOperationFilters []widget.Clickable
|
||||
clearAPIAuditFilters widget.Clickable
|
||||
historyClicks []widget.Clickable
|
||||
attachmentClicks []widget.Clickable
|
||||
breadcrumbs []widget.Clickable
|
||||
groupClicks []widget.Clickable
|
||||
recentVaultClicks []widget.Clickable
|
||||
recentRemoteClicks []widget.Clickable
|
||||
removeCustomFields []widget.Clickable
|
||||
state appstate.State
|
||||
visible []entry
|
||||
currentPath []string
|
||||
syncedPath []string
|
||||
selectedHistoryIndex int
|
||||
showPassword bool
|
||||
generatedPasswordDraft bool
|
||||
togglePassword widget.Clickable
|
||||
copyAPITokenSecret widget.Clickable
|
||||
issueAPIToken widget.Clickable
|
||||
saveAPIToken widget.Clickable
|
||||
rotateAPIToken widget.Clickable
|
||||
disableAPIToken widget.Clickable
|
||||
revokeAPIToken widget.Clickable
|
||||
deleteAPIToken widget.Clickable
|
||||
addAPIPolicyRule widget.Clickable
|
||||
phoneSplit widget.Float
|
||||
splitDrag gesture.Drag
|
||||
splitBase float32
|
||||
splitStartY float32
|
||||
phoneSpan int
|
||||
eyeIcon *widget.Icon
|
||||
eyeOffIcon *widget.Icon
|
||||
copyIcon *widget.Icon
|
||||
expandMoreIcon *widget.Icon
|
||||
expandLessIcon *widget.Icon
|
||||
chevronDownIcon *widget.Icon
|
||||
settingsIcon *widget.Icon
|
||||
clipboardWriter clipboard.Writer
|
||||
loadingMessage string
|
||||
loadingActionLabel string
|
||||
lifecycleMode string
|
||||
syncSourceMode syncSourceMode
|
||||
syncDirection syncDirection
|
||||
syncLocalPath widget.Editor
|
||||
syncRemoteBaseURL widget.Editor
|
||||
syncRemotePath widget.Editor
|
||||
syncRemoteUsername widget.Editor
|
||||
syncRemotePassword widget.Editor
|
||||
syncDialogOpen bool
|
||||
syncMenuOpen bool
|
||||
securityDialogOpen bool
|
||||
showSyncPassword bool
|
||||
keyboardFocus focusID
|
||||
defaultSaveAsPath string
|
||||
recentVaultsPath string
|
||||
uiPreferencesPath string
|
||||
recentRemotesPath string
|
||||
autofillCachePath string
|
||||
editingEntry bool
|
||||
groupControlsHidden bool
|
||||
lifecycleAdvancedHidden bool
|
||||
historyHidden bool
|
||||
recentVaults []string
|
||||
recentRemotes []recentRemoteRecord
|
||||
recentVaultGroups map[string][]string
|
||||
recentVaultUsedAt map[string]time.Time
|
||||
entriesState entriesSectionState
|
||||
deleteGroupPath []string
|
||||
apiPolicyGroupScope bool
|
||||
apiTokenSecret string
|
||||
selectedAuditIndex int
|
||||
statusExpiresAt time.Time
|
||||
now func() time.Time
|
||||
apiHost *api.Host
|
||||
auditLog *apiaudit.Log
|
||||
grpcAddress string
|
||||
backgroundResults chan backgroundActionResult
|
||||
backgroundActionSerial int
|
||||
activeBackgroundAction int
|
||||
lastLifecycleAction string
|
||||
requestMasterPassFocus bool
|
||||
invalidate func()
|
||||
}
|
||||
|
||||
type backgroundActionResult struct {
|
||||
@@ -2265,7 +2269,7 @@ func (u *ui) listEmptyState() emptyState {
|
||||
case appstate.SectionAPIAudit:
|
||||
return emptyState{
|
||||
Title: "No matching audit events",
|
||||
Body: fmt.Sprintf("No audit events match %q. Clear or refine Search vault to filter by token, decision, operation, or resource.", query),
|
||||
Body: fmt.Sprintf("No audit events match %q. Clear the search or try a different quick filter.", query),
|
||||
}
|
||||
case appstate.SectionTemplates:
|
||||
return emptyState{
|
||||
@@ -2293,7 +2297,7 @@ func (u *ui) listEmptyState() emptyState {
|
||||
case appstate.SectionAPIAudit:
|
||||
return emptyState{
|
||||
Title: "No API audit events yet",
|
||||
Body: "Approval prompts, denials, and token actions will appear here.",
|
||||
Body: "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity.",
|
||||
}
|
||||
case appstate.SectionTemplates:
|
||||
return emptyState{
|
||||
@@ -2332,7 +2336,7 @@ func (u *ui) detailPlaceholderMessage() string {
|
||||
case appstate.SectionAPITokens:
|
||||
return "Select an API token, issue a new one, or search to narrow the list."
|
||||
case appstate.SectionAPIAudit:
|
||||
return "Select an audit event to inspect it, or filter the list with Search vault."
|
||||
return "Select an audit event to inspect it, or use Search vault or the quick filters above."
|
||||
case appstate.SectionTemplates:
|
||||
return "Select a template or start a reusable entry."
|
||||
case appstate.SectionRecycleBin:
|
||||
|
||||
@@ -636,6 +636,70 @@ func TestUIAPIAuditSectionShowsRecordedEvents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAPIAuditEventsMatchFriendlyQuickFilters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
u.auditLog = apiaudit.New(10)
|
||||
u.auditLog.Record(apiaudit.Event{
|
||||
Type: apiaudit.EventApprovalAllowed,
|
||||
TokenName: "Browser Extension",
|
||||
Operation: apitokens.OperationCopyPassword,
|
||||
Message: "approved",
|
||||
})
|
||||
u.auditLog.Record(apiaudit.Event{
|
||||
Type: apiaudit.EventApprovalDenied,
|
||||
TokenName: "CLI",
|
||||
Operation: apitokens.OperationListEntries,
|
||||
Message: "denied",
|
||||
})
|
||||
|
||||
u.showAPIAuditSection()
|
||||
|
||||
u.search.SetText("Allowed")
|
||||
if got := u.apiAuditEvents(); len(got) != 1 || got[0].Type != apiaudit.EventApprovalAllowed {
|
||||
t.Fatalf("apiAuditEvents() with Allowed = %#v, want allowed event", got)
|
||||
}
|
||||
|
||||
u.search.SetText("copy password")
|
||||
if got := u.apiAuditEvents(); len(got) != 1 || got[0].Operation != apitokens.OperationCopyPassword {
|
||||
t.Fatalf("apiAuditEvents() with copy password = %#v, want copy_password event", got)
|
||||
}
|
||||
|
||||
u.search.SetText("CLI")
|
||||
if got := u.apiAuditEvents(); len(got) != 1 || got[0].TokenName != "CLI" {
|
||||
t.Fatalf("apiAuditEvents() with CLI = %#v, want CLI token event", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAPIAuditMessagesGuideQuickFilters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
u.showAPIAuditSection()
|
||||
|
||||
if got := u.listEmptyMessage(); got != "No API audit events yet. Connect a trusted client, respond to approval prompts, or issue a token to start recording activity." {
|
||||
t.Fatalf("listEmptyMessage() = %q, want updated API audit guidance", got)
|
||||
}
|
||||
if got := u.detailPlaceholderMessage(); got != "Select an audit event to inspect it, or use Search vault or the quick filters above." {
|
||||
t.Fatalf("detailPlaceholderMessage() = %q, want quick-filter guidance", got)
|
||||
}
|
||||
|
||||
u.search.SetText("allowed")
|
||||
if got := u.listEmptyMessage(); got != `No audit events match "allowed". Clear the search or try a different quick filter.` {
|
||||
t.Fatalf("listEmptyMessage() with search = %q, want quick-filter empty-state guidance", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUILifecycleSecuritySettingsSummaryMovesAdvancedFieldsOutOfOpenFlow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
if got := u.lifecycleSecuritySettingsSummary(); got != "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices." {
|
||||
t.Fatalf("lifecycleSecuritySettingsSummary() = %q, want focused lifecycle guidance", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUISelectedEntryFollowsApplicationStateSelection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -27,6 +28,130 @@ func apiOperations() []apitokens.Operation {
|
||||
}
|
||||
}
|
||||
|
||||
type apiAuditQuickFilter struct {
|
||||
Label string
|
||||
Query string
|
||||
}
|
||||
|
||||
func apiAuditDecisionLabel(eventType apiaudit.EventType) string {
|
||||
switch eventType {
|
||||
case apiaudit.EventApprovalRequested:
|
||||
return "Requested"
|
||||
case apiaudit.EventApprovalAllowed:
|
||||
return "Allowed"
|
||||
case apiaudit.EventApprovalDenied:
|
||||
return "Denied"
|
||||
case apiaudit.EventApprovalCanceled:
|
||||
return "Canceled"
|
||||
case apiaudit.EventApprovalTimedOut:
|
||||
return "Timed Out"
|
||||
case apiaudit.EventAuthRejected:
|
||||
return "Auth Rejected"
|
||||
default:
|
||||
return strings.ReplaceAll(string(eventType), "_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
func apiAuditOperationLabel(operation apitokens.Operation) string {
|
||||
if strings.TrimSpace(string(operation)) == "" {
|
||||
return "Other"
|
||||
}
|
||||
return strings.ReplaceAll(string(operation), "_", " ")
|
||||
}
|
||||
|
||||
func compactAuditFilterLabel(label string) string {
|
||||
label = strings.TrimSpace(label)
|
||||
if len(label) <= 22 {
|
||||
return label
|
||||
}
|
||||
return label[:19] + "..."
|
||||
}
|
||||
|
||||
func apiAuditEventSearchTerms(event apiaudit.Event) string {
|
||||
parts := []string{
|
||||
string(event.Type),
|
||||
apiAuditDecisionLabel(event.Type),
|
||||
event.TokenName,
|
||||
event.ClientName,
|
||||
string(event.Operation),
|
||||
apiAuditOperationLabel(event.Operation),
|
||||
strings.Join(event.Resource.Path, " / "),
|
||||
event.Resource.EntryID,
|
||||
event.Message,
|
||||
}
|
||||
switch event.Type {
|
||||
case apiaudit.EventApprovalAllowed:
|
||||
parts = append(parts, "allow approved")
|
||||
case apiaudit.EventApprovalDenied:
|
||||
parts = append(parts, "deny denied")
|
||||
case apiaudit.EventApprovalRequested:
|
||||
parts = append(parts, "prompt requested")
|
||||
case apiaudit.EventApprovalCanceled:
|
||||
parts = append(parts, "cancel canceled")
|
||||
case apiaudit.EventApprovalTimedOut:
|
||||
parts = append(parts, "timeout timed out")
|
||||
case apiaudit.EventAuthRejected:
|
||||
parts = append(parts, "rejected unauthorized")
|
||||
}
|
||||
return strings.ToLower(strings.Join(parts, " "))
|
||||
}
|
||||
|
||||
func apiAuditFilterButtons(clicks *[]widget.Clickable, filters []apiAuditQuickFilter) []widget.Clickable {
|
||||
if len(filters) == 0 {
|
||||
*clicks = nil
|
||||
return nil
|
||||
}
|
||||
if len(*clicks) < len(filters) {
|
||||
next := make([]widget.Clickable, len(filters))
|
||||
copy(next, *clicks)
|
||||
*clicks = next
|
||||
}
|
||||
return (*clicks)[:len(filters)]
|
||||
}
|
||||
|
||||
func (u *ui) apiAuditQuickFilters(events []apiaudit.Event) ([]apiAuditQuickFilter, []apiAuditQuickFilter, []apiAuditQuickFilter) {
|
||||
tokenSeen := map[string]struct{}{}
|
||||
decisionSeen := map[apiaudit.EventType]struct{}{}
|
||||
operationSeen := map[apitokens.Operation]struct{}{}
|
||||
var tokens []apiAuditQuickFilter
|
||||
var decisions []apiAuditQuickFilter
|
||||
var operations []apiAuditQuickFilter
|
||||
|
||||
for _, event := range events {
|
||||
if name := strings.TrimSpace(event.TokenName); name != "" {
|
||||
if _, ok := tokenSeen[name]; !ok {
|
||||
tokenSeen[name] = struct{}{}
|
||||
tokens = append(tokens, apiAuditQuickFilter{Label: name, Query: name})
|
||||
}
|
||||
}
|
||||
if _, ok := decisionSeen[event.Type]; !ok {
|
||||
decisionSeen[event.Type] = struct{}{}
|
||||
label := apiAuditDecisionLabel(event.Type)
|
||||
decisions = append(decisions, apiAuditQuickFilter{Label: label, Query: label})
|
||||
}
|
||||
if strings.TrimSpace(string(event.Operation)) == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := operationSeen[event.Operation]; ok {
|
||||
continue
|
||||
}
|
||||
operationSeen[event.Operation] = struct{}{}
|
||||
label := apiAuditOperationLabel(event.Operation)
|
||||
operations = append(operations, apiAuditQuickFilter{Label: label, Query: label})
|
||||
}
|
||||
|
||||
if len(tokens) > 4 {
|
||||
tokens = tokens[:4]
|
||||
}
|
||||
if len(decisions) > 5 {
|
||||
decisions = decisions[:5]
|
||||
}
|
||||
if len(operations) > 4 {
|
||||
operations = operations[:4]
|
||||
}
|
||||
return tokens, decisions, operations
|
||||
}
|
||||
|
||||
func (u *ui) apiTokens() []apitokens.Token {
|
||||
tokens, err := u.state.APITokens()
|
||||
if err != nil {
|
||||
@@ -274,15 +399,7 @@ func (u *ui) apiAuditEvents() []apiaudit.Event {
|
||||
}
|
||||
filtered := make([]apiaudit.Event, 0, len(events))
|
||||
for _, event := range events {
|
||||
haystack := strings.ToLower(strings.Join([]string{
|
||||
string(event.Type),
|
||||
event.TokenName,
|
||||
event.ClientName,
|
||||
string(event.Operation),
|
||||
strings.Join(event.Resource.Path, " / "),
|
||||
event.Resource.EntryID,
|
||||
event.Message,
|
||||
}, " "))
|
||||
haystack := apiAuditEventSearchTerms(event)
|
||||
if strings.Contains(haystack, query) {
|
||||
filtered = append(filtered, event)
|
||||
}
|
||||
@@ -516,6 +633,11 @@ func (u *ui) apiTokenListPanel(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions {
|
||||
events := u.apiAuditEvents()
|
||||
allEvents := []apiaudit.Event(nil)
|
||||
if u.auditLog != nil {
|
||||
allEvents = u.auditLog.Events()
|
||||
}
|
||||
tokenFilters, decisionFilters, operationFilters := u.apiAuditQuickFilters(allEvents)
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
text := "Local gRPC audit history"
|
||||
@@ -528,14 +650,18 @@ func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Filter by typing a token name, decision, operation, or resource in Search vault.")
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Filter by token, decision, or operation. Use the quick filters below or type a resource path in Search vault.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.apiAuditQuickFilterPanel(gtx, tokenFilters, decisionFilters, operationFilters)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
if len(events) == 0 {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), "No audit events yet. Approval prompts, denials, token changes, and filled requests will appear here.")
|
||||
lbl := material.Label(u.theme, unit.Sp(14), u.listEmptyMessage())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}
|
||||
@@ -546,6 +672,109 @@ func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) apiAuditQuickFilterPanel(gtx layout.Context, tokenFilters, decisionFilters, operationFilters []apiAuditQuickFilter) layout.Dimensions {
|
||||
hasTokens := len(tokenFilters) > 0
|
||||
hasDecisions := len(decisionFilters) > 0
|
||||
hasOperations := len(operationFilters) > 0
|
||||
query := strings.TrimSpace(u.search.Text())
|
||||
if !hasTokens && !hasDecisions && !hasOperations && query == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
|
||||
children := make([]layout.FlexChild, 0, 8)
|
||||
if query != "" {
|
||||
children = append(children,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.auditQuickFilterButton(gtx, &u.clearAPIAuditFilters, "Clear filters", false, "")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
)
|
||||
}
|
||||
if hasTokens {
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.apiAuditQuickFilterRow(gtx, "Tokens", tokenFilters, &u.apiAuditTokenFilters)
|
||||
}))
|
||||
}
|
||||
if hasDecisions {
|
||||
if len(children) > 0 {
|
||||
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
}
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.apiAuditQuickFilterRow(gtx, "Decisions", decisionFilters, &u.apiAuditDecisionFilters)
|
||||
}))
|
||||
}
|
||||
if hasOperations {
|
||||
if len(children) > 0 {
|
||||
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
}
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.apiAuditQuickFilterRow(gtx, "Operations", operationFilters, &u.apiAuditOperationFilters)
|
||||
}))
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||
}
|
||||
|
||||
func (u *ui) apiAuditQuickFilterRow(gtx layout.Context, title string, filters []apiAuditQuickFilter, clicks *[]widget.Clickable) layout.Dimensions {
|
||||
buttons := apiAuditFilterButtons(clicks, filters)
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), title)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if gtx.Constraints.Max.X <= gtx.Dp(unit.Dp(460)) {
|
||||
column := make([]layout.FlexChild, 0, len(filters)*2)
|
||||
for i := range filters {
|
||||
if i > 0 {
|
||||
column = append(column, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
}
|
||||
filter := filters[i]
|
||||
click := &buttons[i]
|
||||
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
||||
column = append(column, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.auditQuickFilterButton(gtx, click, compactAuditFilterLabel(filter.Label), selected, filter.Query)
|
||||
}))
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, column...)
|
||||
}
|
||||
flexChildren := make([]layout.FlexChild, 0, len(filters)*2)
|
||||
for i := range filters {
|
||||
if i > 0 {
|
||||
flexChildren = append(flexChildren, layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout))
|
||||
}
|
||||
filter := filters[i]
|
||||
click := &buttons[i]
|
||||
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
||||
flexChildren = append(flexChildren, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.auditQuickFilterButton(gtx, click, compactAuditFilterLabel(filter.Label), selected, filter.Query)
|
||||
}))
|
||||
}
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, flexChildren...)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) auditQuickFilterButton(gtx layout.Context, click *widget.Clickable, label string, selected bool, query string) layout.Dimensions {
|
||||
for click.Clicked(gtx) {
|
||||
u.search.SetText(strings.TrimSpace(query))
|
||||
u.filter()
|
||||
}
|
||||
btn := material.Button(u.theme, click, label)
|
||||
btn.CornerRadius = unit.Dp(10)
|
||||
btn.TextSize = unit.Sp(11)
|
||||
btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9}
|
||||
if selected {
|
||||
btn.Background = accentColor
|
||||
btn.Color = color.NRGBA{R: 255, G: 248, B: 238, A: 255}
|
||||
} else {
|
||||
btn.Background = color.NRGBA{R: 231, G: 224, B: 214, A: 255}
|
||||
btn.Color = accentColor
|
||||
}
|
||||
return btn.Layout(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
token, ok := u.selectedAPIToken()
|
||||
removeClicks := u.ensureAPIPolicyRemoveClickables(0)
|
||||
|
||||
+26
-5
@@ -269,11 +269,28 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
if busy || u.lifecycleAdvancedHidden {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Cipher", "Used for new vaults and future saves. Supported values: aes256, chacha20.", &u.securityCipher, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Used for new vaults and future saves. Supported values: aes-kdf, argon2.", &u.securityKDF, false)),
|
||||
)
|
||||
if u.lifecycleMode == "remote" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), "Vault settings")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), u.lifecycleSecuritySettingsSummary())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Open Vault Settings")
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -313,6 +330,10 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) lifecycleSecuritySettingsSummary() string {
|
||||
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
|
||||
}
|
||||
|
||||
func (u *ui) lifecycleAdvancedDisclosure(gtx layout.Context) layout.Dimensions {
|
||||
return u.toggleLifecycleAdvanced.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
Reference in New Issue
Block a user