Improve autofill status feedback
This commit is contained in:
@@ -49,6 +49,7 @@ const (
|
||||
const (
|
||||
maxAttachmentBytes = 10 << 20
|
||||
statusBannerDuration = 2600 * time.Millisecond
|
||||
autofillStatusTTL = 12 * time.Second
|
||||
)
|
||||
|
||||
type bannerKind string
|
||||
@@ -67,6 +68,23 @@ type uiBanner struct {
|
||||
Dismissable bool
|
||||
}
|
||||
|
||||
type autofillStatusKind string
|
||||
|
||||
const (
|
||||
autofillStatusNone autofillStatusKind = ""
|
||||
autofillStatusFound autofillStatusKind = "found"
|
||||
autofillStatusAmbiguous autofillStatusKind = "ambiguous"
|
||||
autofillStatusBlocked autofillStatusKind = "blocked"
|
||||
autofillStatusAwaitingApproval autofillStatusKind = "awaiting_approval"
|
||||
)
|
||||
|
||||
type uiAutofillStatus struct {
|
||||
Kind autofillStatusKind
|
||||
Title string
|
||||
Message string
|
||||
Detail string
|
||||
}
|
||||
|
||||
type uiSurface struct {
|
||||
Title string
|
||||
Message string
|
||||
@@ -1809,6 +1827,152 @@ func (u *ui) statusToastSurface() uiBanner {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) autofillStatusSurface() uiAutofillStatus {
|
||||
if request, ok := u.pendingAutofillApproval(); ok {
|
||||
detail := approvalResourceText(request)
|
||||
if strings.TrimSpace(detail) == "" {
|
||||
detail = "Review the request to allow or deny this fill attempt."
|
||||
}
|
||||
return uiAutofillStatus{
|
||||
Kind: autofillStatusAwaitingApproval,
|
||||
Title: "Autofill approval needed",
|
||||
Message: formatAutofillRequester(request.ClientName, request.TokenName) + " is waiting to fill credentials.",
|
||||
Detail: detail,
|
||||
}
|
||||
}
|
||||
if u.auditLog == nil {
|
||||
return uiAutofillStatus{}
|
||||
}
|
||||
for _, event := range u.auditLog.Events() {
|
||||
if status, ok := autofillStatusFromAuditEvent(event, u.now()); ok {
|
||||
return status
|
||||
}
|
||||
}
|
||||
return uiAutofillStatus{}
|
||||
}
|
||||
|
||||
func (u *ui) pendingAutofillApproval() (apiapproval.Request, bool) {
|
||||
for _, request := range u.state.PendingApprovals() {
|
||||
if isAutofillOperation(request.Operation) {
|
||||
return request, true
|
||||
}
|
||||
}
|
||||
return apiapproval.Request{}, false
|
||||
}
|
||||
|
||||
func autofillStatusFromAuditEvent(event apiaudit.Event, now time.Time) (uiAutofillStatus, bool) {
|
||||
if !event.At.IsZero() && !now.Before(event.At) && now.Sub(event.At) > autofillStatusTTL {
|
||||
return uiAutofillStatus{}, false
|
||||
}
|
||||
|
||||
requester := formatAutofillRequester(event.ClientName, event.TokenName)
|
||||
switch event.Type {
|
||||
case apiaudit.EventAutofillFound:
|
||||
return uiAutofillStatus{
|
||||
Kind: autofillStatusFound,
|
||||
Title: "Autofill match ready",
|
||||
Message: defaultAutofillMessage(event.Message, requester+" found a credential to fill."),
|
||||
Detail: autofillEventDetail(event),
|
||||
}, true
|
||||
case apiaudit.EventAutofillAmbiguous:
|
||||
return uiAutofillStatus{
|
||||
Kind: autofillStatusAmbiguous,
|
||||
Title: "Autofill needs a narrower match",
|
||||
Message: defaultAutofillMessage(event.Message, requester+" found more than one matching credential."),
|
||||
Detail: autofillEventDetail(event),
|
||||
}, true
|
||||
case apiaudit.EventAutofillBlocked:
|
||||
return uiAutofillStatus{
|
||||
Kind: autofillStatusBlocked,
|
||||
Title: "Autofill is blocked",
|
||||
Message: defaultAutofillMessage(event.Message, requester+" could not fill this target."),
|
||||
Detail: autofillEventDetail(event),
|
||||
}, true
|
||||
case apiaudit.EventApprovalAllowed:
|
||||
if !isAutofillOperation(event.Operation) {
|
||||
return uiAutofillStatus{}, false
|
||||
}
|
||||
return uiAutofillStatus{
|
||||
Kind: autofillStatusFound,
|
||||
Title: "Autofill approved",
|
||||
Message: defaultAutofillMessage(event.Message, requester+" can fill this target now."),
|
||||
Detail: autofillEventDetail(event),
|
||||
}, true
|
||||
case apiaudit.EventApprovalDenied, apiaudit.EventApprovalCanceled, apiaudit.EventApprovalTimedOut:
|
||||
if !isAutofillOperation(event.Operation) {
|
||||
return uiAutofillStatus{}, false
|
||||
}
|
||||
return uiAutofillStatus{
|
||||
Kind: autofillStatusBlocked,
|
||||
Title: "Autofill was not allowed",
|
||||
Message: defaultAutofillMessage(event.Message, autofillBlockedMessage(event.Type, requester)),
|
||||
Detail: autofillEventDetail(event),
|
||||
}, true
|
||||
default:
|
||||
return uiAutofillStatus{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func autofillEventDetail(event apiaudit.Event) string {
|
||||
return strings.TrimSpace(resourceDetailText(event.Resource))
|
||||
}
|
||||
|
||||
func resourceDetailText(resource apitokens.Resource) string {
|
||||
switch resource.Kind {
|
||||
case apitokens.ResourceEntry:
|
||||
if entryID := strings.TrimSpace(resource.EntryID); entryID != "" {
|
||||
return "Entry ID: " + entryID
|
||||
}
|
||||
case apitokens.ResourceGroup:
|
||||
if len(resource.Path) > 0 {
|
||||
return "Group: " + strings.Join(resource.Path, " / ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatAutofillRequester(clientName, tokenName string) string {
|
||||
switch {
|
||||
case strings.TrimSpace(clientName) != "" && strings.TrimSpace(tokenName) != "":
|
||||
return strings.TrimSpace(clientName) + " (" + strings.TrimSpace(tokenName) + ")"
|
||||
case strings.TrimSpace(clientName) != "":
|
||||
return strings.TrimSpace(clientName)
|
||||
case strings.TrimSpace(tokenName) != "":
|
||||
return strings.TrimSpace(tokenName)
|
||||
default:
|
||||
return "A trusted client"
|
||||
}
|
||||
}
|
||||
|
||||
func defaultAutofillMessage(value, fallback string) string {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func autofillBlockedMessage(eventType apiaudit.EventType, requester string) string {
|
||||
switch eventType {
|
||||
case apiaudit.EventApprovalDenied:
|
||||
return requester + " was denied for this fill request."
|
||||
case apiaudit.EventApprovalCanceled:
|
||||
return requester + " canceled this fill request."
|
||||
case apiaudit.EventApprovalTimedOut:
|
||||
return requester + " timed out while waiting for approval."
|
||||
default:
|
||||
return requester + " could not fill this target."
|
||||
}
|
||||
}
|
||||
|
||||
func isAutofillOperation(operation apitokens.Operation) bool {
|
||||
switch operation {
|
||||
case apitokens.OperationReadEntry, apitokens.OperationCopyUsername, apitokens.OperationCopyPassword, apitokens.OperationCopyURL:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) loadingDetailMessage() string {
|
||||
if !u.shouldShowLifecycleSetup() {
|
||||
return ""
|
||||
@@ -2429,6 +2593,19 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.bannerSurface().Kind != bannerNone {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.autofillStatusSurface().Kind == autofillStatusNone {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(u.autofillStatusCard),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
if u.shouldShowLifecycleSetup() {
|
||||
return u.lifecycleScreen(gtx)
|
||||
@@ -3667,6 +3844,69 @@ func (u *ui) statusToast(gtx layout.Context) layout.Dimensions {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) autofillStatusCard(gtx layout.Context) layout.Dimensions {
|
||||
status := u.autofillStatusSurface()
|
||||
if status.Kind == autofillStatusNone {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
|
||||
bg := color.NRGBA{R: 233, G: 241, B: 237, A: 255}
|
||||
accent := accentColor
|
||||
switch status.Kind {
|
||||
case autofillStatusAmbiguous:
|
||||
bg = color.NRGBA{R: 245, G: 239, B: 223, A: 255}
|
||||
accent = color.NRGBA{R: 117, G: 88, B: 24, A: 255}
|
||||
case autofillStatusBlocked:
|
||||
bg = color.NRGBA{R: 247, G: 232, B: 228, A: 255}
|
||||
accent = color.NRGBA{R: 125, G: 40, B: 30, A: 255}
|
||||
case autofillStatusAwaitingApproval:
|
||||
bg = color.NRGBA{R: 229, G: 236, B: 244, A: 255}
|
||||
accent = color.NRGBA{R: 30, G: 76, B: 128, A: 255}
|
||||
}
|
||||
|
||||
return layout.Background{}.Layout(gtx, fill(bg), func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Start}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Right: unit.Dp(12), Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Label(u.theme, unit.Sp(12), "Autofill")
|
||||
label.Color = accent
|
||||
label.Font.Weight = 600
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
}),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Label(u.theme, unit.Sp(14), status.Title)
|
||||
label.Color = accent
|
||||
label.Font.Weight = 600
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Label(u.theme, unit.Sp(12), status.Message)
|
||||
label.Color = color.NRGBA{R: 52, G: 50, B: 46, A: 255}
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if strings.TrimSpace(status.Detail) == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Label(u.theme, unit.Sp(11), status.Detail)
|
||||
label.Color = mutedColor
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) historyPanel(gtx layout.Context) layout.Dimensions {
|
||||
history := u.visibleHistory()
|
||||
u.ensureHistoryClickables()
|
||||
|
||||
Reference in New Issue
Block a user