Refine audit UI and vault settings placement
This commit is contained in:
@@ -165,210 +165,214 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ui struct {
|
type ui struct {
|
||||||
mode string
|
mode string
|
||||||
theme *material.Theme
|
theme *material.Theme
|
||||||
logoHorizontal paint.ImageOp
|
logoHorizontal paint.ImageOp
|
||||||
splashSquare paint.ImageOp
|
splashSquare paint.ImageOp
|
||||||
search widget.Editor
|
search widget.Editor
|
||||||
vaultPath widget.Editor
|
vaultPath widget.Editor
|
||||||
saveAsPath widget.Editor
|
saveAsPath widget.Editor
|
||||||
remoteBaseURL widget.Editor
|
remoteBaseURL widget.Editor
|
||||||
remotePath widget.Editor
|
remotePath widget.Editor
|
||||||
remoteUsername widget.Editor
|
remoteUsername widget.Editor
|
||||||
remotePassword widget.Editor
|
remotePassword widget.Editor
|
||||||
masterPassword widget.Editor
|
masterPassword widget.Editor
|
||||||
keyFilePath widget.Editor
|
keyFilePath widget.Editor
|
||||||
apiTokenName widget.Editor
|
apiTokenName widget.Editor
|
||||||
apiTokenClientName widget.Editor
|
apiTokenClientName widget.Editor
|
||||||
apiTokenExpiresAt widget.Editor
|
apiTokenExpiresAt widget.Editor
|
||||||
apiPolicyOperation widget.Editor
|
apiPolicyOperation widget.Editor
|
||||||
apiPolicyPath widget.Editor
|
apiPolicyPath widget.Editor
|
||||||
apiPolicyEntryID widget.Editor
|
apiPolicyEntryID widget.Editor
|
||||||
securityCipher widget.Editor
|
securityCipher widget.Editor
|
||||||
securityKDF widget.Editor
|
securityKDF widget.Editor
|
||||||
entryID widget.Editor
|
entryID widget.Editor
|
||||||
entryTitle widget.Editor
|
entryTitle widget.Editor
|
||||||
entryUsername widget.Editor
|
entryUsername widget.Editor
|
||||||
entryPassword widget.Editor
|
entryPassword widget.Editor
|
||||||
entryURL widget.Editor
|
entryURL widget.Editor
|
||||||
entryNotes widget.Editor
|
entryNotes widget.Editor
|
||||||
entryTags widget.Editor
|
entryTags widget.Editor
|
||||||
entryPath widget.Editor
|
entryPath widget.Editor
|
||||||
entryFields widget.Editor
|
entryFields widget.Editor
|
||||||
customFieldKeys []widget.Editor
|
customFieldKeys []widget.Editor
|
||||||
customFieldValues []widget.Editor
|
customFieldValues []widget.Editor
|
||||||
historyIndex widget.Editor
|
historyIndex widget.Editor
|
||||||
groupName widget.Editor
|
groupName widget.Editor
|
||||||
groupParentPath widget.Editor
|
groupParentPath widget.Editor
|
||||||
passwordProfile widget.Editor
|
passwordProfile widget.Editor
|
||||||
attachmentName widget.Editor
|
attachmentName widget.Editor
|
||||||
attachmentPath widget.Editor
|
attachmentPath widget.Editor
|
||||||
exportAttachmentPath widget.Editor
|
exportAttachmentPath widget.Editor
|
||||||
list widget.List
|
list widget.List
|
||||||
groupList widget.List
|
groupList widget.List
|
||||||
detailList widget.List
|
detailList widget.List
|
||||||
apiPolicyList widget.List
|
apiPolicyList widget.List
|
||||||
lifecycleList widget.List
|
lifecycleList widget.List
|
||||||
copyUser widget.Clickable
|
copyUser widget.Clickable
|
||||||
copyPass widget.Clickable
|
copyPass widget.Clickable
|
||||||
copyURL widget.Clickable
|
copyURL widget.Clickable
|
||||||
lockVault widget.Clickable
|
lockVault widget.Clickable
|
||||||
unlockVault widget.Clickable
|
unlockVault widget.Clickable
|
||||||
createVault widget.Clickable
|
createVault widget.Clickable
|
||||||
openVault widget.Clickable
|
openVault widget.Clickable
|
||||||
saveVault widget.Clickable
|
saveVault widget.Clickable
|
||||||
saveAsVault widget.Clickable
|
saveAsVault widget.Clickable
|
||||||
openRemote widget.Clickable
|
openRemote widget.Clickable
|
||||||
changeMasterKey widget.Clickable
|
changeMasterKey widget.Clickable
|
||||||
synchronizeVault widget.Clickable
|
synchronizeVault widget.Clickable
|
||||||
toggleSyncMenu widget.Clickable
|
toggleSyncMenu widget.Clickable
|
||||||
openAdvancedSync widget.Clickable
|
openAdvancedSync widget.Clickable
|
||||||
openSecuritySettings widget.Clickable
|
openSecuritySettings widget.Clickable
|
||||||
closeAdvancedSync widget.Clickable
|
closeAdvancedSync widget.Clickable
|
||||||
closeSecuritySettings widget.Clickable
|
closeSecuritySettings widget.Clickable
|
||||||
runAdvancedSync widget.Clickable
|
runAdvancedSync widget.Clickable
|
||||||
saveSecuritySettings widget.Clickable
|
saveSecuritySettings widget.Clickable
|
||||||
editEntry widget.Clickable
|
editEntry widget.Clickable
|
||||||
cancelEdit widget.Clickable
|
cancelEdit widget.Clickable
|
||||||
pickVaultPath widget.Clickable
|
pickVaultPath widget.Clickable
|
||||||
pickKeyFile widget.Clickable
|
pickKeyFile widget.Clickable
|
||||||
pickSyncLocalPath widget.Clickable
|
pickSyncLocalPath widget.Clickable
|
||||||
clearVaultSelection widget.Clickable
|
clearVaultSelection widget.Clickable
|
||||||
clearRemoteSelection widget.Clickable
|
clearRemoteSelection widget.Clickable
|
||||||
dismissBanner widget.Clickable
|
dismissBanner widget.Clickable
|
||||||
addEntry widget.Clickable
|
addEntry widget.Clickable
|
||||||
saveEntry widget.Clickable
|
saveEntry widget.Clickable
|
||||||
duplicateEntry widget.Clickable
|
duplicateEntry widget.Clickable
|
||||||
deleteEntry widget.Clickable
|
deleteEntry widget.Clickable
|
||||||
restoreEntry widget.Clickable
|
restoreEntry widget.Clickable
|
||||||
saveTemplate widget.Clickable
|
saveTemplate widget.Clickable
|
||||||
deleteTemplate widget.Clickable
|
deleteTemplate widget.Clickable
|
||||||
instantiateTemplate widget.Clickable
|
instantiateTemplate widget.Clickable
|
||||||
addAttachment widget.Clickable
|
addAttachment widget.Clickable
|
||||||
replaceAttachment widget.Clickable
|
replaceAttachment widget.Clickable
|
||||||
removeAttachment widget.Clickable
|
removeAttachment widget.Clickable
|
||||||
exportAttachment widget.Clickable
|
exportAttachment widget.Clickable
|
||||||
restoreHistory widget.Clickable
|
restoreHistory widget.Clickable
|
||||||
generatePassword widget.Clickable
|
generatePassword widget.Clickable
|
||||||
goToRootGroup widget.Clickable
|
goToRootGroup widget.Clickable
|
||||||
goToParentGroup widget.Clickable
|
goToParentGroup widget.Clickable
|
||||||
createGroup widget.Clickable
|
createGroup widget.Clickable
|
||||||
moveGroup widget.Clickable
|
moveGroup widget.Clickable
|
||||||
renameGroup widget.Clickable
|
renameGroup widget.Clickable
|
||||||
deleteGroup widget.Clickable
|
deleteGroup widget.Clickable
|
||||||
confirmDeleteGroup widget.Clickable
|
confirmDeleteGroup widget.Clickable
|
||||||
cancelDeleteGroup widget.Clickable
|
cancelDeleteGroup widget.Clickable
|
||||||
addCustomField widget.Clickable
|
addCustomField widget.Clickable
|
||||||
toggleGroupControls widget.Clickable
|
toggleGroupControls widget.Clickable
|
||||||
toggleLifecycleAdvanced widget.Clickable
|
toggleLifecycleAdvanced widget.Clickable
|
||||||
toggleHistory widget.Clickable
|
toggleHistory widget.Clickable
|
||||||
togglePasswordInline widget.Clickable
|
togglePasswordInline widget.Clickable
|
||||||
toggleSyncPassword widget.Clickable
|
toggleSyncPassword widget.Clickable
|
||||||
showEntries widget.Clickable
|
showEntries widget.Clickable
|
||||||
showTemplates widget.Clickable
|
showTemplates widget.Clickable
|
||||||
showRecycle widget.Clickable
|
showRecycle widget.Clickable
|
||||||
showAPITokens widget.Clickable
|
showAPITokens widget.Clickable
|
||||||
showAPIAudit widget.Clickable
|
showAPIAudit widget.Clickable
|
||||||
showLocalLifecycle widget.Clickable
|
showLocalLifecycle widget.Clickable
|
||||||
showRemoteLifecycle widget.Clickable
|
showRemoteLifecycle widget.Clickable
|
||||||
showSyncLocal widget.Clickable
|
showSyncLocal widget.Clickable
|
||||||
showSyncRemote widget.Clickable
|
showSyncRemote widget.Clickable
|
||||||
showSyncPull widget.Clickable
|
showSyncPull widget.Clickable
|
||||||
showSyncPush widget.Clickable
|
showSyncPush widget.Clickable
|
||||||
allowApproval widget.Clickable
|
allowApproval widget.Clickable
|
||||||
denyApproval widget.Clickable
|
denyApproval widget.Clickable
|
||||||
cancelApproval widget.Clickable
|
cancelApproval widget.Clickable
|
||||||
cancelLifecycleProgress widget.Clickable
|
cancelLifecycleProgress widget.Clickable
|
||||||
retryLifecycleOpen widget.Clickable
|
retryLifecycleOpen widget.Clickable
|
||||||
approvalPermanent widget.Bool
|
approvalPermanent widget.Bool
|
||||||
rememberRemoteAuth widget.Bool
|
rememberRemoteAuth widget.Bool
|
||||||
apiPolicyAllow widget.Bool
|
apiPolicyAllow widget.Bool
|
||||||
apiPolicyGroupScopeW widget.Bool
|
apiPolicyGroupScopeW widget.Bool
|
||||||
apiTokenDisabled widget.Bool
|
apiTokenDisabled widget.Bool
|
||||||
entryClicks []widget.Clickable
|
entryClicks []widget.Clickable
|
||||||
apiTokenClicks []widget.Clickable
|
apiTokenClicks []widget.Clickable
|
||||||
apiPolicyRemoves []widget.Clickable
|
apiPolicyRemoves []widget.Clickable
|
||||||
apiAuditClicks []widget.Clickable
|
apiAuditClicks []widget.Clickable
|
||||||
historyClicks []widget.Clickable
|
apiAuditTokenFilters []widget.Clickable
|
||||||
attachmentClicks []widget.Clickable
|
apiAuditDecisionFilters []widget.Clickable
|
||||||
breadcrumbs []widget.Clickable
|
apiAuditOperationFilters []widget.Clickable
|
||||||
groupClicks []widget.Clickable
|
clearAPIAuditFilters widget.Clickable
|
||||||
recentVaultClicks []widget.Clickable
|
historyClicks []widget.Clickable
|
||||||
recentRemoteClicks []widget.Clickable
|
attachmentClicks []widget.Clickable
|
||||||
removeCustomFields []widget.Clickable
|
breadcrumbs []widget.Clickable
|
||||||
state appstate.State
|
groupClicks []widget.Clickable
|
||||||
visible []entry
|
recentVaultClicks []widget.Clickable
|
||||||
currentPath []string
|
recentRemoteClicks []widget.Clickable
|
||||||
syncedPath []string
|
removeCustomFields []widget.Clickable
|
||||||
selectedHistoryIndex int
|
state appstate.State
|
||||||
showPassword bool
|
visible []entry
|
||||||
generatedPasswordDraft bool
|
currentPath []string
|
||||||
togglePassword widget.Clickable
|
syncedPath []string
|
||||||
copyAPITokenSecret widget.Clickable
|
selectedHistoryIndex int
|
||||||
issueAPIToken widget.Clickable
|
showPassword bool
|
||||||
saveAPIToken widget.Clickable
|
generatedPasswordDraft bool
|
||||||
rotateAPIToken widget.Clickable
|
togglePassword widget.Clickable
|
||||||
disableAPIToken widget.Clickable
|
copyAPITokenSecret widget.Clickable
|
||||||
revokeAPIToken widget.Clickable
|
issueAPIToken widget.Clickable
|
||||||
deleteAPIToken widget.Clickable
|
saveAPIToken widget.Clickable
|
||||||
addAPIPolicyRule widget.Clickable
|
rotateAPIToken widget.Clickable
|
||||||
phoneSplit widget.Float
|
disableAPIToken widget.Clickable
|
||||||
splitDrag gesture.Drag
|
revokeAPIToken widget.Clickable
|
||||||
splitBase float32
|
deleteAPIToken widget.Clickable
|
||||||
splitStartY float32
|
addAPIPolicyRule widget.Clickable
|
||||||
phoneSpan int
|
phoneSplit widget.Float
|
||||||
eyeIcon *widget.Icon
|
splitDrag gesture.Drag
|
||||||
eyeOffIcon *widget.Icon
|
splitBase float32
|
||||||
copyIcon *widget.Icon
|
splitStartY float32
|
||||||
expandMoreIcon *widget.Icon
|
phoneSpan int
|
||||||
expandLessIcon *widget.Icon
|
eyeIcon *widget.Icon
|
||||||
chevronDownIcon *widget.Icon
|
eyeOffIcon *widget.Icon
|
||||||
settingsIcon *widget.Icon
|
copyIcon *widget.Icon
|
||||||
clipboardWriter clipboard.Writer
|
expandMoreIcon *widget.Icon
|
||||||
loadingMessage string
|
expandLessIcon *widget.Icon
|
||||||
loadingActionLabel string
|
chevronDownIcon *widget.Icon
|
||||||
lifecycleMode string
|
settingsIcon *widget.Icon
|
||||||
syncSourceMode syncSourceMode
|
clipboardWriter clipboard.Writer
|
||||||
syncDirection syncDirection
|
loadingMessage string
|
||||||
syncLocalPath widget.Editor
|
loadingActionLabel string
|
||||||
syncRemoteBaseURL widget.Editor
|
lifecycleMode string
|
||||||
syncRemotePath widget.Editor
|
syncSourceMode syncSourceMode
|
||||||
syncRemoteUsername widget.Editor
|
syncDirection syncDirection
|
||||||
syncRemotePassword widget.Editor
|
syncLocalPath widget.Editor
|
||||||
syncDialogOpen bool
|
syncRemoteBaseURL widget.Editor
|
||||||
syncMenuOpen bool
|
syncRemotePath widget.Editor
|
||||||
securityDialogOpen bool
|
syncRemoteUsername widget.Editor
|
||||||
showSyncPassword bool
|
syncRemotePassword widget.Editor
|
||||||
keyboardFocus focusID
|
syncDialogOpen bool
|
||||||
defaultSaveAsPath string
|
syncMenuOpen bool
|
||||||
recentVaultsPath string
|
securityDialogOpen bool
|
||||||
uiPreferencesPath string
|
showSyncPassword bool
|
||||||
recentRemotesPath string
|
keyboardFocus focusID
|
||||||
autofillCachePath string
|
defaultSaveAsPath string
|
||||||
editingEntry bool
|
recentVaultsPath string
|
||||||
groupControlsHidden bool
|
uiPreferencesPath string
|
||||||
lifecycleAdvancedHidden bool
|
recentRemotesPath string
|
||||||
historyHidden bool
|
autofillCachePath string
|
||||||
recentVaults []string
|
editingEntry bool
|
||||||
recentRemotes []recentRemoteRecord
|
groupControlsHidden bool
|
||||||
recentVaultGroups map[string][]string
|
lifecycleAdvancedHidden bool
|
||||||
recentVaultUsedAt map[string]time.Time
|
historyHidden bool
|
||||||
entriesState entriesSectionState
|
recentVaults []string
|
||||||
deleteGroupPath []string
|
recentRemotes []recentRemoteRecord
|
||||||
apiPolicyGroupScope bool
|
recentVaultGroups map[string][]string
|
||||||
apiTokenSecret string
|
recentVaultUsedAt map[string]time.Time
|
||||||
selectedAuditIndex int
|
entriesState entriesSectionState
|
||||||
statusExpiresAt time.Time
|
deleteGroupPath []string
|
||||||
now func() time.Time
|
apiPolicyGroupScope bool
|
||||||
apiHost *api.Host
|
apiTokenSecret string
|
||||||
auditLog *apiaudit.Log
|
selectedAuditIndex int
|
||||||
grpcAddress string
|
statusExpiresAt time.Time
|
||||||
backgroundResults chan backgroundActionResult
|
now func() time.Time
|
||||||
backgroundActionSerial int
|
apiHost *api.Host
|
||||||
activeBackgroundAction int
|
auditLog *apiaudit.Log
|
||||||
lastLifecycleAction string
|
grpcAddress string
|
||||||
requestMasterPassFocus bool
|
backgroundResults chan backgroundActionResult
|
||||||
invalidate func()
|
backgroundActionSerial int
|
||||||
|
activeBackgroundAction int
|
||||||
|
lastLifecycleAction string
|
||||||
|
requestMasterPassFocus bool
|
||||||
|
invalidate func()
|
||||||
}
|
}
|
||||||
|
|
||||||
type backgroundActionResult struct {
|
type backgroundActionResult struct {
|
||||||
@@ -2265,7 +2269,7 @@ func (u *ui) listEmptyState() emptyState {
|
|||||||
case appstate.SectionAPIAudit:
|
case appstate.SectionAPIAudit:
|
||||||
return emptyState{
|
return emptyState{
|
||||||
Title: "No matching audit events",
|
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:
|
case appstate.SectionTemplates:
|
||||||
return emptyState{
|
return emptyState{
|
||||||
@@ -2293,7 +2297,7 @@ func (u *ui) listEmptyState() emptyState {
|
|||||||
case appstate.SectionAPIAudit:
|
case appstate.SectionAPIAudit:
|
||||||
return emptyState{
|
return emptyState{
|
||||||
Title: "No API audit events yet",
|
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:
|
case appstate.SectionTemplates:
|
||||||
return emptyState{
|
return emptyState{
|
||||||
@@ -2332,7 +2336,7 @@ func (u *ui) detailPlaceholderMessage() string {
|
|||||||
case appstate.SectionAPITokens:
|
case appstate.SectionAPITokens:
|
||||||
return "Select an API token, issue a new one, or search to narrow the list."
|
return "Select an API token, issue a new one, or search to narrow the list."
|
||||||
case appstate.SectionAPIAudit:
|
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:
|
case appstate.SectionTemplates:
|
||||||
return "Select a template or start a reusable entry."
|
return "Select a template or start a reusable entry."
|
||||||
case appstate.SectionRecycleBin:
|
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) {
|
func TestUISelectedEntryFollowsApplicationStateSelection(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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 {
|
func (u *ui) apiTokens() []apitokens.Token {
|
||||||
tokens, err := u.state.APITokens()
|
tokens, err := u.state.APITokens()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -274,15 +399,7 @@ func (u *ui) apiAuditEvents() []apiaudit.Event {
|
|||||||
}
|
}
|
||||||
filtered := make([]apiaudit.Event, 0, len(events))
|
filtered := make([]apiaudit.Event, 0, len(events))
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
haystack := strings.ToLower(strings.Join([]string{
|
haystack := apiAuditEventSearchTerms(event)
|
||||||
string(event.Type),
|
|
||||||
event.TokenName,
|
|
||||||
event.ClientName,
|
|
||||||
string(event.Operation),
|
|
||||||
strings.Join(event.Resource.Path, " / "),
|
|
||||||
event.Resource.EntryID,
|
|
||||||
event.Message,
|
|
||||||
}, " "))
|
|
||||||
if strings.Contains(haystack, query) {
|
if strings.Contains(haystack, query) {
|
||||||
filtered = append(filtered, event)
|
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 {
|
func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions {
|
||||||
events := u.apiAuditEvents()
|
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,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
text := "Local gRPC audit history"
|
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(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
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
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
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 {
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
if len(events) == 0 {
|
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
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
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 {
|
func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||||
token, ok := u.selectedAPIToken()
|
token, ok := u.selectedAPIToken()
|
||||||
removeClicks := u.ensureAPIPolicyRemoveClickables(0)
|
removeClicks := u.ensureAPIPolicyRemoveClickables(0)
|
||||||
|
|||||||
+26
-5
@@ -269,11 +269,28 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
if busy || u.lifecycleAdvancedHidden {
|
if busy || u.lifecycleAdvancedHidden {
|
||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
if u.lifecycleMode == "remote" {
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "Cipher", "Used for new vaults and future saves. Supported values: aes256, chacha20.", &u.securityCipher, false)),
|
return layout.Dimensions{}
|
||||||
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)),
|
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(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
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 {
|
func (u *ui) lifecycleAdvancedDisclosure(gtx layout.Context) layout.Dimensions {
|
||||||
return u.toggleLifecycleAdvanced.Layout(gtx, func(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 {
|
return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
|||||||
Reference in New Issue
Block a user