Refine audit UI and vault settings placement
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user