From 0e4093815805daaef6493658b7e18e886769e280 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Thu, 28 May 2026 20:12:24 -0700 Subject: [PATCH] Fix metabase list user Signed-off-by: Bryan Frimin --- pkg/accessreview/drivers/metabase.go | 99 ++++++++++++++++++- .../drivers/testdata/metabase.yaml | 8 +- pkg/connector/provider/metabase.go | 20 ++++ 3 files changed, 122 insertions(+), 5 deletions(-) diff --git a/pkg/accessreview/drivers/metabase.go b/pkg/accessreview/drivers/metabase.go index fe0f16d1d..88edbb0c4 100644 --- a/pkg/accessreview/drivers/metabase.go +++ b/pkg/accessreview/drivers/metabase.go @@ -46,6 +46,15 @@ type metabaseUser struct { DateJoined string `json:"date_joined"` } +type metabaseUsersResponse struct { + Data []metabaseUser `json:"data"` + Total int `json:"total"` + Limit *int `json:"limit"` + Offset *int `json:"offset"` +} + +const metabaseUsersPageLimit = 50 + func NewMetabaseDriver(httpClient *http.Client, instanceURL string) *MetabaseDriver { return &MetabaseDriver{ httpClient: httpClient, @@ -98,9 +107,36 @@ func (d *MetabaseDriver) queryUsers(ctx context.Context) ([]metabaseUser, error) return nil, fmt.Errorf("cannot parse metabase instance url: %w", err) } + var users []metabaseUser + offset := 0 + + for { + page, err := d.queryUsersPage(ctx, baseURL, offset) + if err != nil { + return nil, err + } + + users = append(users, page.Data...) + + offset += len(page.Data) + if len(page.Data) == 0 || offset >= page.Total { + break + } + } + + return users, nil +} + +func (d *MetabaseDriver) queryUsersPage( + ctx context.Context, + baseURL *url.URL, + offset int, +) (*metabaseUsersResponse, error) { endpoint := baseURL.JoinPath("api", "user") q := endpoint.Query() q.Set("status", "all") + q.Set("limit", strconv.Itoa(metabaseUsersPageLimit)) + q.Set("offset", strconv.Itoa(offset)) endpoint.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) @@ -123,12 +159,12 @@ func (d *MetabaseDriver) queryUsers(ctx context.Context) ([]metabaseUser, error) return nil, fmt.Errorf("cannot fetch metabase users: unexpected status %d", httpResp.StatusCode) } - var users []metabaseUser - if err := json.NewDecoder(httpResp.Body).Decode(&users); err != nil { + var resp metabaseUsersResponse + if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { return nil, fmt.Errorf("cannot decode metabase users response: %w", err) } - return users, nil + return &resp, nil } func metabaseFullName(u metabaseUser) string { @@ -147,6 +183,63 @@ func metabaseRole(isSuperuser bool) string { return "User" } +// metabaseNameResolver resolves the Metabase site name by querying +// /api/session/properties on the configured Metabase instance, which +// exposes the site-name setting to any authenticated session. +type metabaseNameResolver struct { + httpClient *http.Client + instanceURL string +} + +var _ NameResolver = (*metabaseNameResolver)(nil) + +type metabaseSessionProperties struct { + SiteName string `json:"site-name"` +} + +func NewMetabaseNameResolver(httpClient *http.Client, instanceURL string) NameResolver { + return &metabaseNameResolver{ + httpClient: httpClient, + instanceURL: instanceURL, + } +} + +func (r *metabaseNameResolver) ResolveInstanceName(ctx context.Context) (string, error) { + baseURL, err := url.Parse(r.instanceURL) + if err != nil { + return "", fmt.Errorf("cannot parse metabase instance url: %w", err) + } + + endpoint := baseURL.JoinPath("api", "session", "properties") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return "", fmt.Errorf("cannot create metabase session properties request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + httpResp, err := r.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("cannot execute metabase session properties request: %w", err) + } + + defer func() { + _ = httpResp.Body.Close() + }() + + if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { + return "", fmt.Errorf("cannot fetch metabase session properties: unexpected status %d", httpResp.StatusCode) + } + + var props metabaseSessionProperties + if err := json.NewDecoder(httpResp.Body).Decode(&props); err != nil { + return "", fmt.Errorf("cannot decode metabase session properties response: %w", err) + } + + return strings.TrimSpace(props.SiteName), nil +} + func parseMetabaseTimestamp(value string) (time.Time, bool) { if value == "" { return time.Time{}, false diff --git a/pkg/accessreview/drivers/testdata/metabase.yaml b/pkg/accessreview/drivers/testdata/metabase.yaml index 941b81c19..8fe0c7548 100644 --- a/pkg/accessreview/drivers/testdata/metabase.yaml +++ b/pkg/accessreview/drivers/testdata/metabase.yaml @@ -9,12 +9,16 @@ interactions: content_length: 0 host: k7.metabaseapp.com form: + limit: + - "50" + offset: + - "0" status: - all headers: Accept: - application/json - url: https://k7.metabaseapp.com/api/user?status=all + url: https://k7.metabaseapp.com/api/user?limit=50&offset=0&status=all method: GET response: proto: HTTP/2.0 @@ -22,7 +26,7 @@ interactions: proto_minor: 0 content_length: -1 uncompressed: true - body: '[{"id":1,"email":"alice@example.com","first_name":"Alice","last_name":"Admin","common_name":"Alice A.","is_active":true,"is_superuser":true,"last_login":"2026-05-20T10:11:12.345678Z","date_joined":"2026-01-02T03:04:05Z"},{"id":2,"email":"bob@example.com","first_name":"Bob","last_name":"Builder","is_active":false,"is_superuser":false,"last_login":"","date_joined":"2026-02-03T04:05:06Z"},{"id":3,"email":"","first_name":"No","last_name":"Email","is_active":true,"is_superuser":false}]' + body: '{"data":[{"id":1,"email":"alice@example.com","first_name":"Alice","last_name":"Admin","common_name":"Alice A.","is_active":true,"is_superuser":true,"last_login":"2026-05-20T10:11:12.345678Z","date_joined":"2026-01-02T03:04:05Z"},{"id":2,"email":"bob@example.com","first_name":"Bob","last_name":"Builder","is_active":false,"is_superuser":false,"last_login":"","date_joined":"2026-02-03T04:05:06Z"},{"id":3,"email":"","first_name":"No","last_name":"Email","is_active":true,"is_superuser":false}],"total":3,"limit":50,"offset":0}' headers: Content-Type: - application/json; charset=utf-8 diff --git a/pkg/connector/provider/metabase.go b/pkg/connector/provider/metabase.go index cd913817b..813a44061 100644 --- a/pkg/connector/provider/metabase.go +++ b/pkg/connector/provider/metabase.go @@ -52,6 +52,26 @@ func metabaseRegistration() *Registration { return drivers.NewMetabaseDriver(c, instanceURL), nil }, + NewNameResolver: func(ctx context.Context, c *http.Client, conn *coredata.Connector, logger *log.Logger) drivers.NameResolver { + settings, err := coredata.ConnectorSettings[coredata.MetabaseConnectorSettings](conn) + if err != nil { + logger.ErrorCtx(ctx, "cannot read metabase connector settings", log.Error(err)) + return nil + } + + instanceURL := strings.TrimSpace(settings.InstanceURL) + if instanceURL == "" { + logger.ErrorCtx(ctx, "missing metabase instance url in connector settings") + return nil + } + + if err := validateMetabaseInstanceURL(instanceURL); err != nil { + logger.ErrorCtx(ctx, "invalid metabase instance url in connector settings", log.Error(err)) + return nil + } + + return drivers.NewMetabaseNameResolver(c, instanceURL) + }, } }