Refine API token management UI
This commit is contained in:
@@ -182,6 +182,7 @@ type ui struct {
|
||||
list widget.List
|
||||
groupList widget.List
|
||||
detailList widget.List
|
||||
apiPolicyList widget.List
|
||||
lifecycleList widget.List
|
||||
copyUser widget.Clickable
|
||||
copyPass widget.Clickable
|
||||
@@ -431,6 +432,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
detailList: widget.List{
|
||||
List: layout.List{Axis: layout.Vertical},
|
||||
},
|
||||
apiPolicyList: widget.List{
|
||||
List: layout.List{Axis: layout.Vertical},
|
||||
},
|
||||
lifecycleList: widget.List{
|
||||
List: layout.List{Axis: layout.Vertical},
|
||||
},
|
||||
|
||||
@@ -300,6 +300,103 @@ func TestUIAPITokenPolicyRulesCanBeAddedAndRemoved(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPITokenStatusSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expiresAt := time.Date(2026, 4, 1, 15, 4, 5, 0, time.UTC)
|
||||
revokedAt := time.Date(2026, 4, 2, 12, 0, 0, 0, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
token apitokens.Token
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "active non expiring",
|
||||
token: apitokens.Token{},
|
||||
want: "Active · No expiration · 0 policy rules",
|
||||
},
|
||||
{
|
||||
name: "disabled expiring single rule",
|
||||
token: apitokens.Token{
|
||||
Disabled: true,
|
||||
ExpiresAt: &expiresAt,
|
||||
Policies: []apitokens.PolicyRule{{}},
|
||||
},
|
||||
want: "Disabled · Expires " + expiresAt.Local().Format(time.RFC3339) + " · 1 policy rule",
|
||||
},
|
||||
{
|
||||
name: "revoked overrides disabled",
|
||||
token: apitokens.Token{
|
||||
Disabled: true,
|
||||
RevokedAt: &revokedAt,
|
||||
Policies: []apitokens.PolicyRule{{}, {}},
|
||||
},
|
||||
want: "Revoked · No expiration · 2 policy rules",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := apiTokenStatusSummary(tt.token); got != tt.want {
|
||||
t.Fatalf("apiTokenStatusSummary(%#v) = %q, want %q", tt.token, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPITokenManagementSummaryText(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
token := apitokens.Token{
|
||||
Name: "Browser Extension",
|
||||
ClientName: "firefox",
|
||||
}
|
||||
|
||||
if got := apiTokenManagementTitle(apitokens.Token{}, false); got != "Issue API Token" {
|
||||
t.Fatalf("apiTokenManagementTitle(no selection) = %q, want %q", got, "Issue API Token")
|
||||
}
|
||||
if got := apiTokenManagementTitle(token, true); got != "Browser Extension" {
|
||||
t.Fatalf("apiTokenManagementTitle(%#v) = %q, want %q", token, got, "Browser Extension")
|
||||
}
|
||||
if got := apiTokenManagementSubtitle(apitokens.Token{}, false); got != "Create a scoped gRPC credential, then select it here to inspect identity, lifecycle, and policy rules." {
|
||||
t.Fatalf("apiTokenManagementSubtitle(no selection) = %q, want default management guidance", got)
|
||||
}
|
||||
if got := apiTokenManagementSubtitle(token, true); got != "firefox · Active · No expiration · 0 policy rules" {
|
||||
t.Fatalf("apiTokenManagementSubtitle(%#v) = %q, want %q", token, got, "firefox · Active · No expiration · 0 policy rules")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyRulePartsFormatsGroupAndEntryResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
groupRule := apitokens.PolicyRule{
|
||||
Effect: apitokens.EffectAllow,
|
||||
Operation: apitokens.OperationListEntries,
|
||||
Resource: apitokens.Resource{
|
||||
Kind: apitokens.ResourceGroup,
|
||||
Path: []string{"Root", "Internet"},
|
||||
},
|
||||
}
|
||||
entryRule := apitokens.PolicyRule{
|
||||
Effect: apitokens.EffectDeny,
|
||||
Operation: apitokens.OperationCopyPassword,
|
||||
Resource: apitokens.Resource{
|
||||
Kind: apitokens.ResourceEntry,
|
||||
EntryID: "vault-console",
|
||||
},
|
||||
}
|
||||
|
||||
if effect, operation, resource := policyRuleParts(groupRule); effect != "ALLOW" || operation != string(apitokens.OperationListEntries) || resource != "Root / Internet" {
|
||||
t.Fatalf("policyRuleParts(groupRule) = (%q, %q, %q), want (%q, %q, %q)", effect, operation, resource, "ALLOW", apitokens.OperationListEntries, "Root / Internet")
|
||||
}
|
||||
if effect, operation, resource := policyRuleParts(entryRule); effect != "DENY" || operation != string(apitokens.OperationCopyPassword) || resource != "Entry: vault-console" {
|
||||
t.Fatalf("policyRuleParts(entryRule) = (%q, %q, %q), want (%q, %q, %q)", effect, operation, resource, "DENY", apitokens.OperationCopyPassword, "Entry: vault-console")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAPITokenDetailPanelHandlesMissingRemoveClickables(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ func (u *ui) apiAuditEvents() []apiaudit.Event {
|
||||
func policyRuleParts(rule apitokens.PolicyRule) (string, string, string) {
|
||||
effect := strings.ToUpper(string(rule.Effect))
|
||||
operation := string(rule.Operation)
|
||||
resource := "/"
|
||||
resource := "Vault root"
|
||||
if rule.Resource.Kind == apitokens.ResourceEntry {
|
||||
resource = "Entry: " + rule.Resource.EntryID
|
||||
} else if len(rule.Resource.Path) > 0 {
|
||||
@@ -305,6 +305,48 @@ func policyRuleParts(rule apitokens.PolicyRule) (string, string, string) {
|
||||
return effect, operation, resource
|
||||
}
|
||||
|
||||
func apiTokenStatusSummary(token apitokens.Token) string {
|
||||
parts := []string{"Active"}
|
||||
if token.Disabled {
|
||||
parts[0] = "Disabled"
|
||||
}
|
||||
if token.RevokedAt != nil {
|
||||
parts[0] = "Revoked"
|
||||
}
|
||||
if token.ExpiresAt != nil {
|
||||
parts = append(parts, "Expires "+token.ExpiresAt.Local().Format(time.RFC3339))
|
||||
} else {
|
||||
parts = append(parts, "No expiration")
|
||||
}
|
||||
if len(token.Policies) == 1 {
|
||||
parts = append(parts, "1 policy rule")
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("%d policy rules", len(token.Policies)))
|
||||
}
|
||||
return strings.Join(parts, " · ")
|
||||
}
|
||||
|
||||
func apiTokenManagementTitle(token apitokens.Token, ok bool) string {
|
||||
if !ok {
|
||||
return "Issue API Token"
|
||||
}
|
||||
if strings.TrimSpace(token.Name) != "" {
|
||||
return token.Name
|
||||
}
|
||||
return "Unnamed API Token"
|
||||
}
|
||||
|
||||
func apiTokenManagementSubtitle(token apitokens.Token, ok bool) string {
|
||||
if !ok {
|
||||
return "Create a scoped gRPC credential, then select it here to inspect identity, lifecycle, and policy rules."
|
||||
}
|
||||
client := strings.TrimSpace(token.ClientName)
|
||||
if client == "" {
|
||||
client = "No client name"
|
||||
}
|
||||
return client + " · " + apiTokenStatusSummary(token)
|
||||
}
|
||||
|
||||
func uiHasPolicyRule(rules []apitokens.PolicyRule, target apitokens.PolicyRule) bool {
|
||||
for _, rule := range rules {
|
||||
if rule.Effect != target.Effect || rule.Operation != target.Operation {
|
||||
@@ -336,23 +378,21 @@ func (u *ui) apiTokenRow(gtx layout.Context, click *widget.Clickable, idx int, t
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), token.ClientName)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
text := "Non-expiring"
|
||||
if token.ExpiresAt != nil {
|
||||
text = "Expires " + token.ExpiresAt.Local().Format(time.RFC3339)
|
||||
}
|
||||
if token.Disabled {
|
||||
text = "Disabled · " + text
|
||||
}
|
||||
if token.RevokedAt != nil {
|
||||
text = "Revoked · " + text
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(12), text)
|
||||
lbl := material.Label(u.theme, unit.Sp(12), apiTokenStatusSummary(token))
|
||||
lbl.Color = mutedColor
|
||||
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(11), "ID "+token.ID)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
@@ -427,9 +467,40 @@ func (u *ui) apiTokenListPanel(gtx layout.Context) layout.Dimensions {
|
||||
tokens := u.apiTokens()
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Grant scoped gRPC access to external tools. Search matches token name, client, or expiration.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
selected, ok := u.selectedAPIToken()
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(16), "Token Directory")
|
||||
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), "Grant scoped gRPC access to external tools. Search matches token name, client, token id, and policy details.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
title := fmt.Sprintf("%d tokens managed", len(tokens))
|
||||
if ok {
|
||||
title = "Selected: " + apiTokenManagementTitle(selected, true)
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(12), title)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !ok {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(11), apiTokenManagementSubtitle(selected, true))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -483,189 +554,234 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
rows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(20), "API Token")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(8)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Identity")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(4)}.Layout,
|
||||
labeledEditor(u.theme, "Name", &u.apiTokenName, false),
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
labeledEditor(u.theme, "Client Name", &u.apiTokenClientName, false),
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
labeledEditorHelp(u.theme, "Expires At", "Optional RFC3339 timestamp, for example 2026-04-01T15:04:05Z.", &u.apiTokenExpiresAt, false),
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return material.CheckBox(u.theme, &u.apiTokenDisabled, "Disabled").Layout(gtx)
|
||||
return card(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(20), "API Token Management")
|
||||
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(16), apiTokenManagementTitle(token, ok))
|
||||
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), apiTokenManagementSubtitle(token, ok))
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
rows = append(rows,
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
content := []layout.FlexChild{
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), "Identity")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(labeledEditor(u.theme, "Name", &u.apiTokenName, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditor(u.theme, "Client Name", &u.apiTokenClientName, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Expires At", "Optional RFC3339 timestamp, for example 2026-04-01T15:04:05Z.", &u.apiTokenExpiresAt, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return material.CheckBox(u.theme, &u.apiTokenDisabled, "Disabled").Layout(gtx)
|
||||
}),
|
||||
}
|
||||
if ok {
|
||||
content = append(content,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(detailLine(u.theme, "Token ID", token.ID)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(detailLine(u.theme, "Created", token.CreatedAt.Local().Format(time.RFC3339))),
|
||||
)
|
||||
if token.RevokedAt != nil {
|
||||
content = append(content,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(detailLine(u.theme, "Revoked", token.RevokedAt.Local().Format(time.RFC3339))),
|
||||
)
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, content...)
|
||||
})
|
||||
},
|
||||
)
|
||||
if ok {
|
||||
rows = append(rows,
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
detailLine(u.theme, "Token ID", token.ID),
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
detailLine(u.theme, "Created", token.CreatedAt.Local().Format(time.RFC3339)),
|
||||
)
|
||||
if token.RevokedAt != nil {
|
||||
rows = append(rows,
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
detailLine(u.theme, "Revoked", token.RevokedAt.Local().Format(time.RFC3339)),
|
||||
)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(u.apiTokenSecret) != "" {
|
||||
rows = append(rows,
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return card(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), "ONE-TIME SECRET")
|
||||
lbl.Color = mutedColor
|
||||
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(15), u.apiTokenSecret)
|
||||
lbl := material.Label(u.theme, unit.Sp(14), "Lifecycle")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Save updates, rotate secret material, or shut a token down without leaving this surface.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.saveAPIToken, "Save Token")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.rotateAPIToken, "Rotate")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.disableAPIToken, "Disable")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.copyAPITokenSecret, "Copy Secret")
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.revokeAPIToken, "Revoke")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.deleteAPIToken, "Delete")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if strings.TrimSpace(u.apiTokenSecret) == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Inset{Top: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) 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(12), "ONE-TIME SECRET")
|
||||
lbl.Color = mutedColor
|
||||
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(15), u.apiTokenSecret)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.copyAPITokenSecret, "Copy Secret")
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
sectionRows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), "Policy Rules")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(4)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Effect, operation, and resource are separated so rules scan quickly while you review token scope.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
}
|
||||
if len(token.Policies) > 0 {
|
||||
for i, rule := range token.Policies {
|
||||
index := i
|
||||
effect, operation, resource := policyRuleParts(rule)
|
||||
sectionRows = append(sectionRows,
|
||||
func(gtx layout.Context) 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 {
|
||||
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, &removeClicks[index], "Remove")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(detailLine(u.theme, "Operation", operation)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(detailLine(u.theme, "Resource", resource)),
|
||||
)
|
||||
})
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
sectionRows = append(sectionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), "No explicit rules yet. Approval prompts can create permanent rules.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
}
|
||||
return material.List(u.theme, &u.apiPolicyList).Layout(gtx, len(sectionRows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
return sectionRows[i](gtx)
|
||||
})
|
||||
})
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
)
|
||||
}
|
||||
rows = append(rows,
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Token actions")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.saveAPIToken, "Save Token")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.rotateAPIToken, "Rotate")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.disableAPIToken, "Disable")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.revokeAPIToken, "Revoke")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.deleteAPIToken, "Delete")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(14)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(16), "Policy Rules")
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(8)}.Layout,
|
||||
)
|
||||
if ok && len(token.Policies) > 0 {
|
||||
for i, rule := range token.Policies {
|
||||
index := i
|
||||
effect, operation, resource := policyRuleParts(rule)
|
||||
rows = append(rows,
|
||||
func(gtx layout.Context) 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 {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), effect)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), operation)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &removeClicks[index], "Remove")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
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), resource)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), "No explicit rules yet. Approval prompts can create permanent rules.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
}
|
||||
rows = append(rows,
|
||||
layout.Spacer{Height: unit.Dp(10)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "New rule")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Rules are evaluated per operation. Explicit deny rules override allow rules.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return material.CheckBox(u.theme, &u.apiPolicyAllow, "Allow rule (unchecked means deny rule)").Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return material.CheckBox(u.theme, &u.apiPolicyGroupScopeW, "Group scope (unchecked means exact entry scope)").Layout(gtx)
|
||||
},
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
labeledEditorHelp(u.theme, "Operation", "Valid operations: "+strings.Join(stringOps(apiOperations()), ", "), &u.apiPolicyOperation, false),
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
labeledEditorHelp(u.theme, "Group Path", "Used when group scope is enabled.", &u.apiPolicyPath, false),
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
labeledEditorHelp(u.theme, "Entry ID", "Used when group scope is disabled.", &u.apiPolicyEntryID, false),
|
||||
layout.Spacer{Height: unit.Dp(6)}.Layout,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.addAPIPolicyRule, "Add Rule")
|
||||
return card(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(14), "Policy Composer")
|
||||
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.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return material.CheckBox(u.theme, &u.apiPolicyAllow, "Allow rule (unchecked means deny rule)").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return material.CheckBox(u.theme, &u.apiPolicyGroupScopeW, "Group scope (unchecked means exact entry scope)").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Operation", "Valid operations: "+strings.Join(stringOps(apiOperations()), ", "), &u.apiPolicyOperation, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Group Path", "Used when group scope is enabled.", &u.apiPolicyPath, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Entry ID", "Used when group scope is disabled.", &u.apiPolicyEntryID, false)),
|
||||
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 material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
|
||||
Reference in New Issue
Block a user