@@ -46,6 +46,15 @@ type metabaseUser struct {
|
|||||||
DateJoined string `json:"date_joined"`
|
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 {
|
func NewMetabaseDriver(httpClient *http.Client, instanceURL string) *MetabaseDriver {
|
||||||
return &MetabaseDriver{
|
return &MetabaseDriver{
|
||||||
httpClient: httpClient,
|
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)
|
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")
|
endpoint := baseURL.JoinPath("api", "user")
|
||||||
q := endpoint.Query()
|
q := endpoint.Query()
|
||||||
q.Set("status", "all")
|
q.Set("status", "all")
|
||||||
|
q.Set("limit", strconv.Itoa(metabaseUsersPageLimit))
|
||||||
|
q.Set("offset", strconv.Itoa(offset))
|
||||||
endpoint.RawQuery = q.Encode()
|
endpoint.RawQuery = q.Encode()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
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)
|
return nil, fmt.Errorf("cannot fetch metabase users: unexpected status %d", httpResp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var users []metabaseUser
|
var resp metabaseUsersResponse
|
||||||
if err := json.NewDecoder(httpResp.Body).Decode(&users); err != nil {
|
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
|
||||||
return nil, fmt.Errorf("cannot decode metabase users response: %w", err)
|
return nil, fmt.Errorf("cannot decode metabase users response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func metabaseFullName(u metabaseUser) string {
|
func metabaseFullName(u metabaseUser) string {
|
||||||
@@ -147,6 +183,63 @@ func metabaseRole(isSuperuser bool) string {
|
|||||||
return "User"
|
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) {
|
func parseMetabaseTimestamp(value string) (time.Time, bool) {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return time.Time{}, false
|
return time.Time{}, false
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ interactions:
|
|||||||
content_length: 0
|
content_length: 0
|
||||||
host: k7.metabaseapp.com
|
host: k7.metabaseapp.com
|
||||||
form:
|
form:
|
||||||
|
limit:
|
||||||
|
- "50"
|
||||||
|
offset:
|
||||||
|
- "0"
|
||||||
status:
|
status:
|
||||||
- all
|
- all
|
||||||
headers:
|
headers:
|
||||||
Accept:
|
Accept:
|
||||||
- application/json
|
- 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
|
method: GET
|
||||||
response:
|
response:
|
||||||
proto: HTTP/2.0
|
proto: HTTP/2.0
|
||||||
@@ -22,7 +26,7 @@ interactions:
|
|||||||
proto_minor: 0
|
proto_minor: 0
|
||||||
content_length: -1
|
content_length: -1
|
||||||
uncompressed: true
|
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:
|
headers:
|
||||||
Content-Type:
|
Content-Type:
|
||||||
- application/json; charset=utf-8
|
- application/json; charset=utf-8
|
||||||
|
|||||||
@@ -52,6 +52,26 @@ func metabaseRegistration() *Registration {
|
|||||||
|
|
||||||
return drivers.NewMetabaseDriver(c, instanceURL), nil
|
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)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user