Complete API token authz UI flows
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"git.julianfamily.org/keepassgo/internal/apitokens"
|
||||
"git.julianfamily.org/keepassgo/internal/passwords"
|
||||
"git.julianfamily.org/keepassgo/internal/session"
|
||||
"git.julianfamily.org/keepassgo/internal/vault"
|
||||
@@ -19,7 +20,16 @@ func TestStartHostServesVaultLifecycleAndSyncsSessionState(t *testing.T) {
|
||||
lifecycle := &session.Manager{}
|
||||
if err := lifecycle.Create(vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
testAPITokenEntry(t),
|
||||
testAPITokenEntry(t,
|
||||
apitokens.PolicyRule{
|
||||
Effect: apitokens.EffectAllow,
|
||||
Operation: apitokens.OperationManageVault,
|
||||
Resource: apitokens.Resource{
|
||||
Kind: apitokens.ResourceGroup,
|
||||
Path: []string{"Root"},
|
||||
},
|
||||
},
|
||||
),
|
||||
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
||||
},
|
||||
}, vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
|
||||
|
||||
+127
-11
@@ -129,7 +129,22 @@ func (u *ui) ensureAPIPolicyRemoveClickables(count int) []widget.Clickable {
|
||||
return clicks
|
||||
}
|
||||
|
||||
func (u *ui) ensureAPIPolicyEditClickables(count int) []widget.Clickable {
|
||||
if count <= 0 {
|
||||
u.apiPolicyEdits = nil
|
||||
return nil
|
||||
}
|
||||
if len(u.apiPolicyEdits) == count {
|
||||
return u.apiPolicyEdits
|
||||
}
|
||||
clicks := make([]widget.Clickable, count)
|
||||
copy(clicks, u.apiPolicyEdits)
|
||||
u.apiPolicyEdits = clicks
|
||||
return clicks
|
||||
}
|
||||
|
||||
func (u *ui) loadSelectedAPITokenIntoEditor() {
|
||||
u.selectedAPIPolicyIndex = -1
|
||||
token, ok := u.selectedAPIToken()
|
||||
if !ok {
|
||||
u.apiTokenSecret = ""
|
||||
@@ -143,6 +158,7 @@ func (u *ui) loadSelectedAPITokenIntoEditor() {
|
||||
u.apiPolicyAllow.Value = true
|
||||
u.apiPolicyGroupScope = true
|
||||
u.apiPolicyGroupScopeW.Value = true
|
||||
u.ensureAPIPolicyEditClickables(0)
|
||||
u.ensureAPIPolicyRemoveClickables(0)
|
||||
return
|
||||
}
|
||||
@@ -154,6 +170,7 @@ func (u *ui) loadSelectedAPITokenIntoEditor() {
|
||||
u.apiTokenExpiresAt.SetText("")
|
||||
}
|
||||
u.apiTokenDisabled.Value = token.Disabled
|
||||
u.ensureAPIPolicyEditClickables(len(token.Policies))
|
||||
u.ensureAPIPolicyRemoveClickables(len(token.Policies))
|
||||
}
|
||||
|
||||
@@ -250,14 +267,10 @@ func parseAPIPolicyOperation(text string) (apitokens.Operation, error) {
|
||||
return "", fmt.Errorf("unknown API operation %q", text)
|
||||
}
|
||||
|
||||
func (u *ui) addAPIPolicyRuleAction() error {
|
||||
token, ok := u.selectedAPIToken()
|
||||
if !ok {
|
||||
return fmt.Errorf("no API token selected")
|
||||
}
|
||||
func (u *ui) apiPolicyRuleFromEditor() (apitokens.PolicyRule, error) {
|
||||
operation, err := parseAPIPolicyOperation(u.apiPolicyOperation.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
return apitokens.PolicyRule{}, err
|
||||
}
|
||||
rule := apitokens.PolicyRule{
|
||||
Operation: operation,
|
||||
@@ -270,16 +283,28 @@ func (u *ui) addAPIPolicyRuleAction() error {
|
||||
if u.apiPolicyGroupScope {
|
||||
path := parsePath(u.apiPolicyPath.Text())
|
||||
if len(path) == 0 {
|
||||
return fmt.Errorf("policy path is required for group scope")
|
||||
return apitokens.PolicyRule{}, fmt.Errorf("policy path is required for group scope")
|
||||
}
|
||||
rule.Resource = apitokens.Resource{Kind: apitokens.ResourceGroup, Path: path}
|
||||
} else {
|
||||
entryID := strings.TrimSpace(u.apiPolicyEntryID.Text())
|
||||
if entryID == "" {
|
||||
return fmt.Errorf("entry id is required for entry scope")
|
||||
return apitokens.PolicyRule{}, fmt.Errorf("entry id is required for entry scope")
|
||||
}
|
||||
rule.Resource = apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: entryID}
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func (u *ui) addAPIPolicyRuleAction() error {
|
||||
token, ok := u.selectedAPIToken()
|
||||
if !ok {
|
||||
return fmt.Errorf("no API token selected")
|
||||
}
|
||||
rule, err := u.apiPolicyRuleFromEditor()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !uiHasPolicyRule(token.Policies, rule) {
|
||||
token.Policies = append(token.Policies, rule)
|
||||
}
|
||||
@@ -290,6 +315,63 @@ func (u *ui) addAPIPolicyRuleAction() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) editAPIPolicyRuleAction(index int) error {
|
||||
token, ok := u.selectedAPIToken()
|
||||
if !ok {
|
||||
return fmt.Errorf("no API token selected")
|
||||
}
|
||||
if index < 0 || index >= len(token.Policies) {
|
||||
return fmt.Errorf("policy index %d out of range", index)
|
||||
}
|
||||
rule := token.Policies[index]
|
||||
u.selectedAPIPolicyIndex = index
|
||||
u.apiPolicyOperation.SetText(string(rule.Operation))
|
||||
u.apiPolicyAllow.Value = rule.Effect == apitokens.EffectAllow
|
||||
if rule.Resource.Kind == apitokens.ResourceEntry {
|
||||
u.apiPolicyGroupScope = false
|
||||
u.apiPolicyGroupScopeW.Value = false
|
||||
u.apiPolicyEntryID.SetText(strings.TrimSpace(rule.Resource.EntryID))
|
||||
u.apiPolicyPath.SetText("")
|
||||
return nil
|
||||
}
|
||||
u.apiPolicyGroupScope = true
|
||||
u.apiPolicyGroupScopeW.Value = true
|
||||
u.apiPolicyPath.SetText(strings.Join(rule.Resource.Path, " / "))
|
||||
u.apiPolicyEntryID.SetText("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) saveAPIPolicyRuleAction() error {
|
||||
token, ok := u.selectedAPIToken()
|
||||
if !ok {
|
||||
return fmt.Errorf("no API token selected")
|
||||
}
|
||||
index := u.selectedAPIPolicyIndex
|
||||
if index < 0 || index >= len(token.Policies) {
|
||||
return fmt.Errorf("no API policy rule selected")
|
||||
}
|
||||
rule, err := u.apiPolicyRuleFromEditor()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, existing := range token.Policies {
|
||||
if i != index && uiHasPolicyRule([]apitokens.PolicyRule{existing}, rule) {
|
||||
token.Policies = append(token.Policies[:index], token.Policies[index+1:]...)
|
||||
if err := u.state.UpsertAPIToken(token); err != nil {
|
||||
return err
|
||||
}
|
||||
u.loadSelectedAPITokenIntoEditor()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
token.Policies[index] = rule
|
||||
if err := u.state.UpsertAPIToken(token); err != nil {
|
||||
return err
|
||||
}
|
||||
u.loadSelectedAPITokenIntoEditor()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) apiPolicyGroupPathSummary() string {
|
||||
path := parsePath(u.apiPolicyPath.Text())
|
||||
if len(path) == 0 {
|
||||
@@ -357,6 +439,11 @@ func (u *ui) removeAPIPolicyRuleAction(index int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) cancelAPIPolicyEditAction() error {
|
||||
u.loadSelectedAPITokenIntoEditor()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) apiAuditEvents() []apiaudit.Event {
|
||||
if u.auditLog == nil {
|
||||
return nil
|
||||
@@ -749,8 +836,10 @@ func (u *ui) auditQuickFilterButton(gtx layout.Context, click *widget.Clickable,
|
||||
|
||||
func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
token, ok := u.selectedAPIToken()
|
||||
editClicks := u.ensureAPIPolicyEditClickables(0)
|
||||
removeClicks := u.ensureAPIPolicyRemoveClickables(0)
|
||||
if ok {
|
||||
editClicks = u.ensureAPIPolicyEditClickables(len(token.Policies))
|
||||
removeClicks = u.ensureAPIPolicyRemoveClickables(len(token.Policies))
|
||||
}
|
||||
rows := []layout.Widget{
|
||||
@@ -918,6 +1007,10 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(1, detailLine(u.theme, "Effect", effect)),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &editClicks[index], "Edit")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &removeClicks[index], "Remove")
|
||||
}),
|
||||
@@ -951,15 +1044,23 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
rows = append(rows,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
actionLabel := "Add Rule"
|
||||
title := "Policy Composer"
|
||||
description := "Rules are evaluated per operation. Explicit deny rules override allow rules."
|
||||
if 0 <= u.selectedAPIPolicyIndex {
|
||||
actionLabel = "Save Rule"
|
||||
title = "Policy Editor"
|
||||
description = "Editing an existing rule. Save the updated scope or cancel to return to a blank composer."
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), "Policy Composer")
|
||||
lbl := material.Label(u.theme, unit.Sp(14), title)
|
||||
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), "Rules are evaluated per operation. Explicit deny rules override allow rules.")
|
||||
lbl := material.Label(u.theme, unit.Sp(12), description)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
@@ -1014,7 +1115,22 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.addAPIPolicyRule, "Add Rule")
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if 0 <= u.selectedAPIPolicyIndex {
|
||||
return tonedButton(gtx, u.theme, &u.saveAPIPolicyRule, actionLabel)
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.addAPIPolicyRule, actionLabel)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.selectedAPIPolicyIndex < 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Inset{Left: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.cancelAPIPolicyEdit, "Cancel Edit")
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
+30
-14
@@ -364,12 +364,13 @@ type ui struct {
|
||||
showAutofillApprovalAsk widget.Clickable
|
||||
showAutofillApprovalAllow widget.Clickable
|
||||
showAutofillApprovalBlock widget.Clickable
|
||||
allowApproval widget.Clickable
|
||||
denyApproval widget.Clickable
|
||||
allowApprovalOnce widget.Clickable
|
||||
allowApprovalPermanent widget.Clickable
|
||||
denyApprovalOnce widget.Clickable
|
||||
denyApprovalPermanent widget.Clickable
|
||||
cancelApproval widget.Clickable
|
||||
cancelLifecycleProgress widget.Clickable
|
||||
retryLifecycleOpen widget.Clickable
|
||||
approvalPermanent widget.Bool
|
||||
syncSetupAutomatic widget.Bool
|
||||
apiPolicyAllow widget.Bool
|
||||
apiPolicyGroupScopeW widget.Bool
|
||||
@@ -381,6 +382,7 @@ type ui struct {
|
||||
settingsDebugHeaderBounds widget.Bool
|
||||
entryClicks []widget.Clickable
|
||||
apiTokenClicks []widget.Clickable
|
||||
apiPolicyEdits []widget.Clickable
|
||||
apiPolicyRemoves []widget.Clickable
|
||||
apiAuditClicks []widget.Clickable
|
||||
apiAuditTokenFilters []widget.Clickable
|
||||
@@ -416,6 +418,8 @@ type ui struct {
|
||||
useSelectedEntryForPolicy widget.Clickable
|
||||
clearAPIPolicyTarget widget.Clickable
|
||||
addAPIPolicyRule widget.Clickable
|
||||
saveAPIPolicyRule widget.Clickable
|
||||
cancelAPIPolicyEdit widget.Clickable
|
||||
phoneSplit widget.Float
|
||||
splitDrag gesture.Drag
|
||||
splitBase float32
|
||||
@@ -488,6 +492,7 @@ type ui struct {
|
||||
entriesState entriesSectionState
|
||||
deleteGroupPath []string
|
||||
apiPolicyGroupScope bool
|
||||
selectedAPIPolicyIndex int
|
||||
apiTokenSecret string
|
||||
phoneSyncMenuOrigin image.Point
|
||||
phoneMainMenuOrigin image.Point
|
||||
@@ -665,6 +670,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
vaultSharer: platform.NewVaultSharer(runtime.GOOS),
|
||||
backgroundResults: make(chan backgroundActionResult, 8),
|
||||
phoneGroupBrowserExpanded: true,
|
||||
selectedAPIPolicyIndex: -1,
|
||||
}
|
||||
if mode == "phone" {
|
||||
u.groupControlsHidden = true
|
||||
@@ -1431,23 +1437,33 @@ func (u *ui) approvalDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return approvalFact(u.theme, "Operation", string(request.Operation), resourceText)(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
check := material.CheckBox(u.theme, &u.approvalPermanent, "Make this decision permanent")
|
||||
check.Color = accentColor
|
||||
return check.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.allowApproval, "Allow")
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.allowApprovalOnce, "Allow Once")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.allowApprovalPermanent, "Allow Permanently")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.denyApproval, "Deny")
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.denyApprovalOnce, "Deny Once")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.denyApprovalPermanent, "Deny Permanently")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.cancelApproval, "Cancel")
|
||||
}),
|
||||
|
||||
+27
-19
@@ -928,33 +928,29 @@ func (u *ui) handleApprovalAndAPIClicks(gtx layout.Context) {
|
||||
}
|
||||
|
||||
func (u *ui) handleApprovalClicks(gtx layout.Context) {
|
||||
for u.allowApproval.Clicked(gtx) {
|
||||
for u.allowApprovalOnce.Clicked(gtx) {
|
||||
u.runAction("allow API request", func() error {
|
||||
outcome := apiapproval.OutcomeAllowOnce
|
||||
if u.approvalPermanent.Value {
|
||||
outcome = apiapproval.OutcomeAllowPermanent
|
||||
}
|
||||
err := u.resolvePendingApproval(outcome)
|
||||
u.approvalPermanent.Value = false
|
||||
return err
|
||||
return u.resolvePendingApproval(apiapproval.OutcomeAllowOnce)
|
||||
})
|
||||
}
|
||||
for u.denyApproval.Clicked(gtx) {
|
||||
for u.allowApprovalPermanent.Clicked(gtx) {
|
||||
u.runAction("allow API request permanently", func() error {
|
||||
return u.resolvePendingApproval(apiapproval.OutcomeAllowPermanent)
|
||||
})
|
||||
}
|
||||
for u.denyApprovalOnce.Clicked(gtx) {
|
||||
u.runAction("deny API request", func() error {
|
||||
outcome := apiapproval.OutcomeDenyOnce
|
||||
if u.approvalPermanent.Value {
|
||||
outcome = apiapproval.OutcomeDenyPermanent
|
||||
}
|
||||
err := u.resolvePendingApproval(outcome)
|
||||
u.approvalPermanent.Value = false
|
||||
return err
|
||||
return u.resolvePendingApproval(apiapproval.OutcomeDenyOnce)
|
||||
})
|
||||
}
|
||||
for u.denyApprovalPermanent.Clicked(gtx) {
|
||||
u.runAction("deny API request permanently", func() error {
|
||||
return u.resolvePendingApproval(apiapproval.OutcomeDenyPermanent)
|
||||
})
|
||||
}
|
||||
for u.cancelApproval.Clicked(gtx) {
|
||||
u.runAction("cancel API request", func() error {
|
||||
err := u.resolvePendingApproval(apiapproval.OutcomeCancel)
|
||||
u.approvalPermanent.Value = false
|
||||
return err
|
||||
return u.resolvePendingApproval(apiapproval.OutcomeCancel)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -996,6 +992,12 @@ func (u *ui) handleAPIPolicyClicks(gtx layout.Context) {
|
||||
for u.addAPIPolicyRule.Clicked(gtx) {
|
||||
u.runAction("add API policy rule", u.addAPIPolicyRuleAction)
|
||||
}
|
||||
for u.saveAPIPolicyRule.Clicked(gtx) {
|
||||
u.runAction("save API policy rule", u.saveAPIPolicyRuleAction)
|
||||
}
|
||||
for u.cancelAPIPolicyEdit.Clicked(gtx) {
|
||||
u.runAction("cancel API policy edit", u.cancelAPIPolicyEditAction)
|
||||
}
|
||||
for u.useCurrentGroupForPolicy.Clicked(gtx) {
|
||||
u.runAction("use current group for API policy", u.useCurrentGroupForPolicyAction)
|
||||
}
|
||||
@@ -1005,6 +1007,12 @@ func (u *ui) handleAPIPolicyClicks(gtx layout.Context) {
|
||||
for u.clearAPIPolicyTarget.Clicked(gtx) {
|
||||
u.runAction("clear API policy target", u.clearAPIPolicyTargetAction)
|
||||
}
|
||||
for i := range u.apiPolicyEdits {
|
||||
for u.apiPolicyEdits[i].Clicked(gtx) {
|
||||
index := i
|
||||
u.runAction("edit API policy rule", func() error { return u.editAPIPolicyRuleAction(index) })
|
||||
}
|
||||
}
|
||||
for i := range u.apiPolicyRemoves {
|
||||
for u.apiPolicyRemoves[i].Clicked(gtx) {
|
||||
index := i
|
||||
|
||||
+159
-1
@@ -754,6 +754,23 @@ func TestUIAPITokenLifecycleManagement(t *testing.T) {
|
||||
t.Fatal("apiTokenSecret after rotate = empty, want one-time secret")
|
||||
}
|
||||
|
||||
u.apiTokenName.SetText("Browser Extension Updated")
|
||||
u.apiTokenClientName.SetText("firefox-desktop")
|
||||
u.apiTokenExpiresAt.SetText("2026-05-01T00:00:00Z")
|
||||
if err := u.saveAPITokenAction(); err != nil {
|
||||
t.Fatalf("saveAPITokenAction() error = %v", err)
|
||||
}
|
||||
updated, ok := u.selectedAPIToken()
|
||||
if !ok {
|
||||
t.Fatal("selectedAPIToken() ok = false, want true after save")
|
||||
}
|
||||
if updated.Name != "Browser Extension Updated" || updated.ClientName != "firefox-desktop" {
|
||||
t.Fatalf("updated token = %#v, want renamed/firefox-desktop", updated)
|
||||
}
|
||||
if updated.ExpiresAt == nil || updated.ExpiresAt.UTC().Format(time.RFC3339) != "2026-05-01T00:00:00Z" {
|
||||
t.Fatalf("updated.ExpiresAt = %#v, want 2026-05-01T00:00:00Z", updated.ExpiresAt)
|
||||
}
|
||||
|
||||
if err := u.disableAPITokenAction(); err != nil {
|
||||
t.Fatalf("disableAPITokenAction() error = %v", err)
|
||||
}
|
||||
@@ -761,9 +778,17 @@ func TestUIAPITokenLifecycleManagement(t *testing.T) {
|
||||
if !ok || !disabled.Disabled {
|
||||
t.Fatalf("selectedAPIToken() = %#v, want disabled token", disabled)
|
||||
}
|
||||
|
||||
if err := u.revokeAPITokenAction(); err != nil {
|
||||
t.Fatalf("revokeAPITokenAction() error = %v", err)
|
||||
}
|
||||
revoked, ok := u.selectedAPIToken()
|
||||
if !ok || revoked.RevokedAt == nil {
|
||||
t.Fatalf("selectedAPIToken() = %#v, want revoked token", revoked)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAPITokenPolicyRulesCanBeAddedAndRemoved(t *testing.T) {
|
||||
func TestUIAPITokenPolicyRulesCanBeCreatedUpdatedAndRemoved(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
|
||||
@@ -799,6 +824,33 @@ func TestUIAPITokenPolicyRulesCanBeAddedAndRemoved(t *testing.T) {
|
||||
if token.Policies[0].Resource.Kind != apitokens.ResourceGroup {
|
||||
t.Fatalf("rule kind = %q, want group", token.Policies[0].Resource.Kind)
|
||||
}
|
||||
if len(u.apiPolicyEdits) != 1 {
|
||||
t.Fatalf("len(apiPolicyEdits) = %d, want 1", len(u.apiPolicyEdits))
|
||||
}
|
||||
|
||||
u.apiPolicyEdits[0].Click()
|
||||
u.handleAPIPolicyClicks(layout.Context{})
|
||||
if u.selectedAPIPolicyIndex != 0 {
|
||||
t.Fatalf("selectedAPIPolicyIndex = %d, want 0 after edit click", u.selectedAPIPolicyIndex)
|
||||
}
|
||||
if got := u.apiPolicyPath.Text(); got != "Root / Internet" {
|
||||
t.Fatalf("apiPolicyPath = %q, want Root / Internet after edit load", got)
|
||||
}
|
||||
|
||||
u.apiPolicyPath.SetText("Root / Security")
|
||||
u.saveAPIPolicyRule.Click()
|
||||
u.handleAPIPolicyClicks(layout.Context{})
|
||||
|
||||
token, ok = u.selectedAPIToken()
|
||||
if !ok || len(token.Policies) != 1 {
|
||||
t.Fatalf("selectedAPIToken().Policies after save = %#v, want 1 rule", token.Policies)
|
||||
}
|
||||
if got := strings.Join(token.Policies[0].Resource.Path, " / "); got != "Root / Security" {
|
||||
t.Fatalf("updated policy path = %q, want Root / Security", got)
|
||||
}
|
||||
if u.selectedAPIPolicyIndex != -1 {
|
||||
t.Fatalf("selectedAPIPolicyIndex after save = %d, want -1", u.selectedAPIPolicyIndex)
|
||||
}
|
||||
|
||||
if err := u.removeAPIPolicyRuleAction(0); err != nil {
|
||||
t.Fatalf("removeAPIPolicyRuleAction() error = %v", err)
|
||||
@@ -4858,6 +4910,112 @@ func TestUIResolvePendingApprovalDelegatesToApprovalManager(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIApprovalDialogVisibleForPendingRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
u.state.Approvals = &mainStubApprovalManager{
|
||||
pending: []apiapproval.Request{{
|
||||
ID: "approval-1",
|
||||
TokenName: "CLI",
|
||||
ClientName: "grpc-cli",
|
||||
Operation: apitokens.OperationReadEntry,
|
||||
Resource: apitokens.Resource{
|
||||
Kind: apitokens.ResourceEntry,
|
||||
EntryID: "vault-console",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
dims := u.approvalDialogContent(layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Exact(image.Pt(800, 600)),
|
||||
})
|
||||
if dims.Size.X == 0 || dims.Size.Y == 0 {
|
||||
t.Fatalf("approvalDialogContent() = %v, want visible dimensions for pending approval", dims.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIApprovalButtonsResolveAllOutcomes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
click func(*ui)
|
||||
want apiapproval.Outcome
|
||||
wantMsg string
|
||||
}{
|
||||
{
|
||||
name: "allow once",
|
||||
click: func(u *ui) {
|
||||
u.allowApprovalOnce.Click()
|
||||
},
|
||||
want: apiapproval.OutcomeAllowOnce,
|
||||
wantMsg: "allow API request complete",
|
||||
},
|
||||
{
|
||||
name: "allow permanently",
|
||||
click: func(u *ui) {
|
||||
u.allowApprovalPermanent.Click()
|
||||
},
|
||||
want: apiapproval.OutcomeAllowPermanent,
|
||||
wantMsg: "allow API request permanently complete",
|
||||
},
|
||||
{
|
||||
name: "deny once",
|
||||
click: func(u *ui) {
|
||||
u.denyApprovalOnce.Click()
|
||||
},
|
||||
want: apiapproval.OutcomeDenyOnce,
|
||||
wantMsg: "deny API request complete",
|
||||
},
|
||||
{
|
||||
name: "deny permanently",
|
||||
click: func(u *ui) {
|
||||
u.denyApprovalPermanent.Click()
|
||||
},
|
||||
want: apiapproval.OutcomeDenyPermanent,
|
||||
wantMsg: "deny API request permanently complete",
|
||||
},
|
||||
{
|
||||
name: "cancel",
|
||||
click: func(u *ui) {
|
||||
u.cancelApproval.Click()
|
||||
},
|
||||
want: apiapproval.OutcomeCancel,
|
||||
wantMsg: "cancel API request complete",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
manager := &mainStubApprovalManager{
|
||||
pending: []apiapproval.Request{{
|
||||
ID: "approval-1",
|
||||
TokenName: "CLI",
|
||||
ClientName: "grpc-cli",
|
||||
Operation: apitokens.OperationReadEntry,
|
||||
}},
|
||||
}
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
u.state.Approvals = manager
|
||||
|
||||
tt.click(u)
|
||||
u.handleApprovalClicks(layout.Context{})
|
||||
|
||||
if manager.lastID != "approval-1" || manager.lastOutcome != tt.want {
|
||||
t.Fatalf("handleApprovalClicks() delegated (%q, %q), want (approval-1, %q)", manager.lastID, manager.lastOutcome, tt.want)
|
||||
}
|
||||
if got := u.state.StatusMessage; got != tt.wantMsg {
|
||||
t.Fatalf("state.StatusMessage = %q, want %q", got, tt.wantMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIRequiresExplicitEditModeForEntryEditor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user