From c43445d487a64b45cfa82d9d9df108eed1acb575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:03:21 +0200 Subject: [PATCH] Add Notion workspace name resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notion was the only wired access-review connector without a name resolver, so the source kept the generic "Notion" placeholder. Fetch the workspace name from /v1/users/me (bot.workspace_name) following the same pattern as the other resolvers, and refresh the stale scope comment now that Notion participates in name resolution. Signed-off-by: AurĂ©lien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- pkg/accessreview/drivers/name_resolver.go | 39 +++++++ .../drivers/name_resolver_test.go | 100 ++++++++++++++++++ pkg/accessreview/drivers/oauth2_scopes.go | 5 +- pkg/accessreview/source_name_worker.go | 2 + 4 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 pkg/accessreview/drivers/name_resolver_test.go diff --git a/pkg/accessreview/drivers/name_resolver.go b/pkg/accessreview/drivers/name_resolver.go index 52bdd2619..f779fae6d 100644 --- a/pkg/accessreview/drivers/name_resolver.go +++ b/pkg/accessreview/drivers/name_resolver.go @@ -588,3 +588,42 @@ func NewResendNameResolver() NameResolver { func (r *resendNameResolver) ResolveInstanceName(_ context.Context) (string, error) { return "Resend", nil } + +// notionNameResolver resolves the Notion workspace name via /v1/users/me. +type notionNameResolver struct { + httpClient *http.Client +} + +func NewNotionNameResolver(httpClient *http.Client) NameResolver { + return ¬ionNameResolver{httpClient: httpClient} +} + +func (r *notionNameResolver) ResolveInstanceName(ctx context.Context) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.notion.com/v1/users/me", nil) + if err != nil { + return "", fmt.Errorf("cannot create notion users/me request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Notion-Version", notionAPIVersion) + + httpResp, err := r.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("cannot execute notion users/me request: %w", err) + } + defer func() { _ = httpResp.Body.Close() }() + + if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { + return "", fmt.Errorf("cannot fetch notion users/me: unexpected status %d", httpResp.StatusCode) + } + + var resp struct { + Bot struct { + WorkspaceName string `json:"workspace_name"` + } `json:"bot"` + } + if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { + return "", fmt.Errorf("cannot decode notion users/me response: %w", err) + } + + return resp.Bot.WorkspaceName, nil +} diff --git a/pkg/accessreview/drivers/name_resolver_test.go b/pkg/accessreview/drivers/name_resolver_test.go new file mode 100644 index 000000000..1f0b0a482 --- /dev/null +++ b/pkg/accessreview/drivers/name_resolver_test.go @@ -0,0 +1,100 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package drivers + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// hostRewriter redirects requests to the configured target host so that +// resolvers with hardcoded production URLs (api.notion.com, etc.) can be +// pointed at an httptest server. +type hostRewriter struct { + target string +} + +func (h *hostRewriter) RoundTrip(r *http.Request) (*http.Response, error) { + u, err := url.Parse(h.target) + if err != nil { + return nil, err + } + r2 := r.Clone(r.Context()) + r2.URL.Scheme = u.Scheme + r2.URL.Host = u.Host + return http.DefaultTransport.RoundTrip(r2) +} + +func TestNotionNameResolver(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + status int + body string + want string + wantErr bool + }{ + { + name: "bot with workspace_name", + status: http.StatusOK, + body: `{"type":"bot","bot":{"workspace_name":"Acme Inc"}}`, + want: "Acme Inc", + }, + { + name: "user token (no bot field)", + status: http.StatusOK, + body: `{"type":"person"}`, + want: "", + }, + { + name: "server error", + status: http.StatusInternalServerError, + body: `{"message":"boom"}`, + wantErr: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/v1/users/me", r.URL.Path) + assert.Equal(t, notionAPIVersion, r.Header.Get("Notion-Version")) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tc.status) + _, _ = w.Write([]byte(tc.body)) + })) + defer srv.Close() + + client := &http.Client{Transport: &hostRewriter{target: srv.URL}} + got, err := NewNotionNameResolver(client).ResolveInstanceName(context.Background()) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/pkg/accessreview/drivers/oauth2_scopes.go b/pkg/accessreview/drivers/oauth2_scopes.go index 4a4fa8d29..bc1db78a4 100644 --- a/pkg/accessreview/drivers/oauth2_scopes.go +++ b/pkg/accessreview/drivers/oauth2_scopes.go @@ -33,8 +33,9 @@ var providerOAuth2Scopes = map[coredata.ConnectorProvider][]string{ "https://www.googleapis.com/auth/admin.directory.group.member.readonly", "https://www.googleapis.com/auth/admin.directory.customer.readonly", }, - // Notion and Intercom intentionally omitted: Notion uses extra-auth-params - // instead of scopes, Intercom configures scopes at the app level. + // Notion and Intercom have no scopes here: Notion authorizes via + // extra-auth-params (owner=user), Intercom configures scopes at the app + // level. } // ProviderOAuth2Scopes returns the OAuth2 scopes the access review driver diff --git a/pkg/accessreview/source_name_worker.go b/pkg/accessreview/source_name_worker.go index 16da31ca1..818be2619 100644 --- a/pkg/accessreview/source_name_worker.go +++ b/pkg/accessreview/source_name_worker.go @@ -277,6 +277,8 @@ func (h *sourceNameHandler) buildResolver( return drivers.NewSupabaseNameResolver(supabaseSettings.OrganizationSlug) case coredata.ConnectorProviderIntercom: return drivers.NewIntercomNameResolver(httpClient) + case coredata.ConnectorProviderNotion: + return drivers.NewNotionNameResolver(httpClient) case coredata.ConnectorProviderResend: return drivers.NewResendNameResolver() default: