feat: an account pane on Azure account rows: fields and groups to move through and copy
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f6c4f5f6f6
commit
b37caa5a25
13 files changed
+396
-27
No files matched your search
@@ -1,10 +1,15 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
)
|
||||
|
||||
@@ -158,3 +163,68 @@ func directoryRoles(wids any) string {
|
||||
slices.Sort(names)
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// graphGroups lists the signed-in user's groups, nested ones too, from
|
||||
// Microsoft Graph with az's token for it.
|
||||
const graphGroups = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$select=displayName&$top=999"
|
||||
|
||||
var _ provider.AccountGroups = (*Provider)(nil)
|
||||
|
||||
// Groups lists the groups the account is a member of, by name.
|
||||
func (p *Provider) Groups(ctx context.Context) ([]string, error) {
|
||||
if p.account == nil || (p.account.cloud != "" && p.account.cloud != "AzureCloud") {
|
||||
return nil, provider.ErrUnsupported
|
||||
}
|
||||
tok, err := p.cred.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{"https://graph.microsoft.com/.default"}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
for next := graphGroups; next != ""; {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, next, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+tok.Token)
|
||||
resp, err := p.http().Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var page struct {
|
||||
Value []struct {
|
||||
Type string `json:"@odata.type"`
|
||||
Name string `json:"displayName"`
|
||||
} `json:"value"`
|
||||
Next string `json:"@odata.nextLink"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&page)
|
||||
resp.Body.Close()
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("Microsoft Graph: %w", err)
|
||||
case page.Error != nil:
|
||||
return nil, fmt.Errorf("Microsoft Graph: %s", page.Error.Message)
|
||||
case resp.StatusCode != http.StatusOK:
|
||||
return nil, fmt.Errorf("Microsoft Graph: %s", resp.Status)
|
||||
}
|
||||
for _, v := range page.Value {
|
||||
if v.Type == "#microsoft.graph.group" && v.Name != "" {
|
||||
names = append(names, v.Name)
|
||||
}
|
||||
}
|
||||
next = page.Next
|
||||
}
|
||||
slices.SortFunc(names, func(a, b string) int { return strings.Compare(strings.ToLower(a), strings.ToLower(b)) })
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// http is the client for Microsoft Graph; tests replace it.
|
||||
func (p *Provider) http() *http.Client {
|
||||
if p.graphClient != nil {
|
||||
return p.graphClient
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
@@ -1,7 +1,17 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
)
|
||||
@@ -65,3 +75,40 @@ func TestSignInMethods(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type staticToken struct{}
|
||||
|
||||
func (staticToken) GetToken(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) {
|
||||
return azcore.AccessToken{Token: "tok", ExpiresOn: time.Now().Add(time.Hour)}, nil
|
||||
}
|
||||
|
||||
type graphPages map[string]string // by URL
|
||||
|
||||
func (g graphPages) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
body, ok := g[r.URL.String()]
|
||||
if !ok || r.Header.Get("Authorization") != "Bearer tok" {
|
||||
return &http.Response{StatusCode: 404, Status: "404 Not Found", Body: io.NopCloser(strings.NewReader(`{}`))}, nil
|
||||
}
|
||||
return &http.Response{StatusCode: 200, Status: "200 OK", Body: io.NopCloser(strings.NewReader(body))}, nil
|
||||
}
|
||||
|
||||
func TestGroups(t *testing.T) {
|
||||
next := "https://graph.microsoft.com/v1.0/next"
|
||||
p := &Provider{
|
||||
cred: staticToken{},
|
||||
account: &accountDetails{account: account{user: "alice@contoso.com"}},
|
||||
graphClient: &http.Client{Transport: graphPages{
|
||||
graphGroups: `{"value":[{"@odata.type":"#microsoft.graph.group","displayName":"vault readers"},
|
||||
{"@odata.type":"#microsoft.graph.directoryRole","displayName":"Global Reader"}],"@odata.nextLink":"` + next + `"}`,
|
||||
next: `{"value":[{"@odata.type":"#microsoft.graph.group","displayName":"Platform team"}]}`,
|
||||
}},
|
||||
}
|
||||
got, err := p.Groups(context.Background())
|
||||
if err != nil || !slices.Equal(got, []string{"Platform team", "vault readers"}) {
|
||||
t.Errorf("groups %q, %v", got, err)
|
||||
}
|
||||
p.account.cloud = "AzureChinaCloud"
|
||||
if _, err := p.Groups(context.Background()); !errors.Is(err, provider.ErrUnsupported) {
|
||||
t.Errorf("other cloud: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -36,11 +37,12 @@ var objectTypes = []provider.ObjectType{provider.TypeSecret, provider.TypeKey, p
|
||||
type Provider struct {
|
||||
id, label string // "" for the single-account defaults
|
||||
|
||||
cred azcore.TokenCredential
|
||||
opts options
|
||||
account *accountDetails // nil for a service principal from AZURE_*
|
||||
subs *armsubscriptions.Client
|
||||
graph *armresourcegraph.Client
|
||||
cred azcore.TokenCredential
|
||||
opts options
|
||||
account *accountDetails // nil for a service principal from AZURE_*
|
||||
graphClient *http.Client // for Groups; nil for the default
|
||||
subs *armsubscriptions.Client
|
||||
graph *armresourcegraph.Client
|
||||
|
||||
mu sync.Mutex
|
||||
vaults map[string]*vaultClients // by vault URL
|
||||
|
||||
Reference in new issue
Block a user