feat: show who an Azure account signs in as, in Details on its row (#5)
On an Azure account's group row, the Details pane now describes the account: | field | from | |---|---| | name | token `name` | | user (and "service principal") | az's account list | | tenant: name · ID | az's account list (the name is borrowed from another entry in the same tenant when az left it out) | | object ID | token `oid`; `y` on the row copies it | | sign-in: MFA or not, and how | token `amr` | | directory roles | token `wids`: built-in roles by name, others as `role 1a2b3c4d`, the default member role left out | | subs: count · default | az's account list | | cloud | az's account list, when not AzureCloud | Nothing is fetched for it: the claims are the ones from the token lazysecrets already gets for the account's own tenant, read without verification. The token itself is never stored or shown. Before the first token, only the az-list fields show. A failing account shows its sign-in problem there instead (from #4). New optional `provider.Accounts` interface; `azure.Provider` implements it. Tests: the fields from claims and az's list, sign-in methods, the UI showing it on the group row only, and `y` copying the object ID. Checked against a real signed-in account (name, MFA · password, default subscription) and an expired one (az-list fields only, since there's no token). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com> Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
1 parent
a82f6bff27
commit
52b6257dd5
12 files changed
+369
-20
No files matched your search
@@ -723,6 +723,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.askHandlerSetup()
|
||||
m.keys.Login.SetEnabled(m.loginFor() != nil)
|
||||
m.detail.SetMessage(m.selectedProblem())
|
||||
acct, _ := m.selectedAccount()
|
||||
m.detail.SetAccount(acct.Fields)
|
||||
if m.anyLoading() && !m.spinning {
|
||||
m.spinning = true
|
||||
cmds = append(cmds, m.spin.Tick)
|
||||
@@ -1453,6 +1455,9 @@ func (m *Model) describe(o provider.Object) tea.Cmd {
|
||||
func (m *Model) copyValue() tea.Cmd {
|
||||
o, ok := m.detail.Secret()
|
||||
if !ok {
|
||||
if a, ok := m.selectedAccount(); ok && a.Copy != "" && m.focus == paneSources {
|
||||
return m.copyText(a.CopyLabel, a.Copy, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if v, ok := m.detail.Value(); ok {
|
||||
@@ -1674,3 +1679,20 @@ func (m Model) loginFor() *provider.AuthError {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectedAccount describes the account of the selected provider row, if
|
||||
// its provider can.
|
||||
func (m Model) selectedAccount() (provider.Account, bool) {
|
||||
r, ok := m.sources.Selected()
|
||||
if !ok || r.kind != rowGroup {
|
||||
return provider.Account{}, false
|
||||
}
|
||||
if res := m.sourceResults[r.provider]; res.err != nil {
|
||||
return provider.Account{}, false // the problem shows instead
|
||||
}
|
||||
a, ok := m.byID[r.provider].(provider.Accounts)
|
||||
if !ok {
|
||||
return provider.Account{}, false
|
||||
}
|
||||
return a.Account()
|
||||
}
|
||||
@@ -1069,3 +1069,33 @@ func lastLines(scr string, n int) string {
|
||||
lines := strings.Split(strings.TrimRight(scr, "\n"), "\n")
|
||||
return strings.Join(lines[max(0, len(lines)-n):], "\n")
|
||||
}
|
||||
|
||||
// described is an account that can say who it is.
|
||||
type described struct{ provider.Provider }
|
||||
|
||||
func (described) Account() (provider.Account, bool) {
|
||||
return provider.Account{
|
||||
Fields: []provider.Attr{{Label: "name", Value: "Alice Example"}, {Label: "object ID", Value: "oid-123"}},
|
||||
Copy: "oid-123", CopyLabel: "object ID",
|
||||
}, true
|
||||
}
|
||||
|
||||
func TestAccountInDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriver(t, described{fake.Azure(0)})
|
||||
d.keys("g") // the group's row
|
||||
if r, _ := d.m.sources.Selected(); r.kind != rowGroup {
|
||||
t.Fatalf("not on the group: %+v", r)
|
||||
}
|
||||
if scr := d.screen(); !strings.Contains(scr, "Alice Example") || !strings.Contains(scr, "oid-123") {
|
||||
t.Errorf("account not in Details:\n%s", scr)
|
||||
}
|
||||
d.keys("y")
|
||||
if got := d.clip().content; got != "oid-123" {
|
||||
t.Errorf("y copied %q", got)
|
||||
}
|
||||
d.keys("j")
|
||||
if strings.Contains(d.screen(), "Alice Example") {
|
||||
t.Errorf("still shown on a subscription:\n%s", d.screen())
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ type Detail struct {
|
||||
versions []provider.Object // newest first; nil while unknown
|
||||
note string // the user's note on what's shown
|
||||
message string // shown instead of the hint when nothing is selected: an error
|
||||
account []provider.Attr // shown instead of the hint on a provider's row: who it signs in as
|
||||
|
||||
offset int
|
||||
w, h int
|
||||
@@ -76,6 +77,10 @@ func (d *Detail) Clear() {
|
||||
// selected, such as why an account has nothing to show; "" for the hint.
|
||||
func (d *Detail) SetMessage(s string) { d.message = s }
|
||||
|
||||
// SetAccount sets the account shown on a provider's row when nothing is
|
||||
// selected; nil for the hint.
|
||||
func (d *Detail) SetAccount(fields []provider.Attr) { d.account = fields }
|
||||
|
||||
// SetDescribing marks extra metadata as being fetched.
|
||||
func (d *Detail) SetDescribing() { d.describing, d.describeErr = true, nil }
|
||||
|
||||
@@ -191,6 +196,12 @@ func (d Detail) lines(st styles.Styles) []string {
|
||||
muted := st.Muted.Render
|
||||
|
||||
if d.container == nil {
|
||||
if d.message == "" && len(d.account) > 0 {
|
||||
for _, a := range d.account {
|
||||
field(a.Label, a.Value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
if d.message != "" {
|
||||
for _, l := range strings.Split(ansi.Hardwrap(d.message, max(1, inner-2), true), "\n") {
|
||||
out = append(out, " "+st.Error.Render(l))
|
||||
|
||||
Reference in new issue
Block a user