Skip to content

Commit 1e14654

Browse files
Merge pull request #10 from ConductorOne/feat/BB-766
[BB-766] User accounts provisioning and connector refactor
2 parents 914b520 + 890564d commit 1e14654

File tree

14 files changed

+629
-322
lines changed

14 files changed

+629
-322
lines changed

README.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
11
# baton-notion
2-
`baton-notion` is a connector for Notion built using the [Baton SDK](https://github.com/conductorone/baton-sdk). It communicates with the Notion API to sync data about users and groups.
2+
`baton-notion` is a connector for [Notion](https://www.notion.com) built using the [Baton SDK](https://github.com/conductorone/baton-sdk). It communicates with the Notion API to sync data about users and groups.
33

44
Check out [Baton](https://github.com/conductorone/baton) to learn more the project in general.
55

66
# Getting Started
77

88
## Prerequisites
99

10-
1. Notion account with a workspace
10+
1. Enterprise Notion account with a workspace
1111
2. Admin level access to the workspace
12-
3. Created integration with access to the workspace. More info [here](https://developers.notion.com/docs/create-a-notion-integration#step-1-create-an-integration).
13-
5. Set capabilities:
14-
- Read content
15-
- Read user information including email addresses
16-
4. Notion integration token, also called an API key used to communicate with Notion API. You can find it [here](https://www.notion.so/my-integrations).
17-
5. If you have Enterprise Plan you can generate SCIM API token which can be used to sync information about Notion groups. You can create the token by going to `Settings & members → Security & identity → SCIM configuration`.
12+
3. SCIM API Token
1813

1914
## brew
2015

2116
```
2217
brew install conductorone/baton/baton conductorone/baton/baton-notion
23-
baton-notion
18+
baton-notion --scim-token <scim_token>
2419
baton resources
2520
```
2621

@@ -37,17 +32,17 @@ docker run --rm -v $(pwd):/out ghcr.io/conductorone/baton:latest -f "/out/sync.c
3732
go install github.com/conductorone/baton/cmd/baton@main
3833
go install github.com/conductorone/baton-notion/cmd/baton-notion@main
3934
40-
BATON_API_KEY=apiKey
35+
baton-notion --scim-token <scim_token>
36+
4137
baton resources
4238
```
4339

4440
# Data Model
4541

4642
`baton-notion` pulls down information about the following Notion resources:
4743
- Users
48-
- Groups (only with Notion Enterprise Plan)
44+
- Groups
4945

50-
By default, `baton-notion` will only sync information about users. If you have an enterprise plan you can pass the SCIM token using the `--scim-token` flag and sync groups as well.
5146

5247
# Contributing, Support, and Issues
5348

@@ -70,15 +65,14 @@ Available Commands:
7065
help Help about any command
7166
7267
Flags:
73-
--api-key string The Notion API key used to connect to the Notion API. ($BATON_API_KEY)
68+
--scim-token string The Notion SCIM token used to connect to the Notion SCIM API. ($BATON_SCIM_TOKEN)
7469
--client-id string The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID)
7570
--client-secret string The client secret used to authenticate with ConductorOne ($BATON_CLIENT_SECRET)
7671
-f, --file string The path to the c1z file to sync with ($BATON_FILE) (default "sync.c1z")
7772
-h, --help help for baton-notion
7873
--log-format string The output format for logs: json, console ($BATON_LOG_FORMAT) (default "json")
7974
--log-level string The log level: debug, info, warn, error ($BATON_LOG_LEVEL) (default "info")
8075
-p, --provisioning This must be set in order for provisioning actions to be enabled. ($BATON_PROVISIONING)
81-
--scim-token string The Notion SCIM token used to connect to the Notion SCIM API. ($BATON_SCIM_TOKEN)
8276
-v, --version version for baton-notion
8377
8478
Use "baton-notion [command] --help" for more information about a command.

cmd/baton-notion/config.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,17 @@ import (
66
)
77

88
const (
9-
apiKeyFlag = "api-key"
109
scimTokenFlag = "scim-token"
1110
)
1211

1312
var (
14-
APIKeyField = field.StringField(
15-
apiKeyFlag,
16-
field.WithRequired(true),
17-
field.WithDescription("The Notion API key used to connect to the Notion API. ($BATON_API_KEY)"),
18-
)
19-
2013
SCIMTokenField = field.StringField(
2114
scimTokenFlag,
2215
field.WithRequired(false),
2316
field.WithDescription("The Notion SCIM token used to connect to the Notion SCIM API. ($BATON_SCIM_TOKEN)"),
2417
)
2518

26-
ConfigurationFields = []field.SchemaField{APIKeyField, SCIMTokenField}
19+
ConfigurationFields = []field.SchemaField{SCIMTokenField}
2720
)
2821

2922
// ValidateConfig is run after the configuration is loaded, and should return an

cmd/baton-notion/main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,9 @@ func getConnector(ctx context.Context, v *viper.Viper) (types.ConnectorServer, e
4848
return nil, err
4949
}
5050

51-
apiKey := v.GetString(apiKeyFlag)
5251
scimToken := v.GetString(scimTokenFlag)
5352

54-
cb, err := connector.New(ctx, apiKey, scimToken)
53+
cb, err := connector.New(ctx, scimToken)
5554
if err != nil {
5655
l.Error("error creating connector", zap.Error(err))
5756
return nil, err

docs/docs-info

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Connector capabilities
2+
3+
1. What resources does the connector sync?
4+
- Notion connector syncs Users and Groups.
5+
6+
2. Can the connector provision any resources? If so, which ones?
7+
- This connector can provision Accounts.
8+
9+
## Connector credentials
10+
11+
1. What credentials or information are needed to set up the connector? (For example, API key, client ID and secret, domain, etc.)
12+
- An API Token for the SCIM API should be provided.
13+
14+
2. For each item in the list above:
15+
16+
* How does a user create or look up that credential or info? Please include links to (non-gated) documentation, screenshots (of the UI or of gated docs), or a video of the process.
17+
- In order to generate an API Token for the SCIM API, a Organization Owner of an Enterprise Notion account should go to the settings panel.
18+
- On the settings search for the 'Identity' tab on the left-panel and scroll down to the "SCIM Provisioning" section.
19+
- In the "SCIM Provisioning" section, users should be able to create a Token to use the SCIM API.
20+
21+
* Does the credential need any specific scopes or permissions? If so, list them here.
22+
note: this isn't specified on the Notion docs and we didn't have the chance to test it 'cause we don't have an enterprise instance.
23+
24+
* If applicable: Is the list of scopes or permissions different to sync (read) versus provision (read-write)? If so, list the difference here.
25+
note: this isn't specified on the Notion docs and we didn't have the chance to test it 'cause we don't have an enterprise instance.
26+
27+
* What level of access or permissions does the user need in order to create the credentials? (For example, must be a super administrator, must have access to the admin console, etc.)
28+
- It should be an Organization Owner.

pkg/client/client.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"strconv"
9+
10+
"github.com/conductorone/baton-sdk/pkg/uhttp"
11+
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
12+
)
13+
14+
const (
15+
baseUrl = "https://www.notion.so/scim/v2"
16+
DefaultUserSchema = "urn:ietf:params:scim:schemas:core:2.0:User"
17+
)
18+
19+
type NotionClient struct {
20+
client *uhttp.BaseHttpClient
21+
scimToken string
22+
}
23+
24+
func (c *NotionClient) GetUsers(ctx context.Context, pageOps PaginationOptions) ([]User, string, error) {
25+
var nextPage string
26+
requestURL := fmt.Sprint(baseUrl, "/Users")
27+
28+
var res UsersResponse
29+
_, err := c.doRequest(
30+
ctx,
31+
http.MethodGet,
32+
requestURL,
33+
&res,
34+
nil,
35+
WithPageSize(pageOps.PerPage),
36+
WithStartIndex(pageOps.StartIndex),
37+
)
38+
if err != nil {
39+
return nil, "", err
40+
}
41+
42+
if (int64(pageOps.StartIndex) + res.ItemsPerPage) < res.TotalResults {
43+
nextPage = strconv.FormatInt(int64(pageOps.StartIndex)+res.ItemsPerPage, 10)
44+
}
45+
46+
return res.Resources, nextPage, nil
47+
}
48+
49+
// GetGroups returns all Notion groups.
50+
func (c *NotionClient) GetGroups(ctx context.Context, pageOps PaginationOptions) ([]Group, string, error) {
51+
var nextPage string
52+
requestURL := fmt.Sprint(baseUrl, "/Groups")
53+
54+
var res GroupsResponse
55+
_, err := c.doRequest(
56+
ctx,
57+
http.MethodGet,
58+
requestURL,
59+
&res,
60+
nil,
61+
WithPageSize(pageOps.PerPage),
62+
WithStartIndex(pageOps.StartIndex),
63+
)
64+
if err != nil {
65+
return nil, "", err
66+
}
67+
68+
if (int64(pageOps.StartIndex) + res.ItemsPerPage) < res.TotalResults {
69+
nextPage = strconv.FormatInt(int64(pageOps.StartIndex)+res.ItemsPerPage, 10)
70+
}
71+
72+
return res.Resources, nextPage, nil
73+
}
74+
75+
// GetGroup returns group details by group ID.
76+
func (c *NotionClient) GetGroup(ctx context.Context, groupId string) (Group, error) {
77+
requestURL := fmt.Sprint(baseUrl, "/Groups/", groupId)
78+
79+
var groupResponse Group
80+
_, err := c.doRequest(ctx, http.MethodGet, requestURL, &groupResponse, nil)
81+
if err != nil {
82+
return Group{}, err
83+
}
84+
85+
return groupResponse, nil
86+
}
87+
88+
func (c *NotionClient) GetUser(ctx context.Context, userID string) (*User, error) {
89+
var userData *User
90+
requestURL := fmt.Sprint(baseUrl, "/Users/", userID)
91+
92+
_, err := c.doRequest(ctx, http.MethodGet, requestURL, &userData, nil)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
return userData, nil
98+
}
99+
100+
func (c *NotionClient) CreateUser(ctx context.Context, user *User) (*User, error) {
101+
var newUser *User
102+
requestURL := fmt.Sprint(baseUrl, "/Users")
103+
104+
_, err := c.doRequest(ctx, http.MethodPost, requestURL, &newUser, user)
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
return newUser, nil
110+
}
111+
112+
func (c *NotionClient) DeleteUser(ctx context.Context, userID string) error {
113+
requestURL := fmt.Sprint(baseUrl, "/Users/", userID)
114+
115+
_, err := c.doRequest(ctx, http.MethodDelete, requestURL, nil, nil)
116+
if err != nil {
117+
return err
118+
}
119+
120+
return nil
121+
}
122+
123+
func (c *NotionClient) doRequest(
124+
ctx context.Context,
125+
method string,
126+
endpointUrl string,
127+
res interface{},
128+
body interface{},
129+
reqOpts ...ReqOpt,
130+
) (http.Header, error) {
131+
var resp *http.Response
132+
133+
urlAddress, err := url.Parse(endpointUrl)
134+
if err != nil {
135+
return nil, err
136+
}
137+
138+
for _, o := range reqOpts {
139+
o(urlAddress)
140+
}
141+
142+
opts := []uhttp.RequestOption{uhttp.WithBearerToken(c.scimToken)}
143+
if body != nil {
144+
opts = append(opts, uhttp.WithAcceptJSONHeader(), uhttp.WithContentTypeJSONHeader(), uhttp.WithJSONBody(body))
145+
}
146+
147+
req, err := c.client.NewRequest(
148+
ctx,
149+
method,
150+
urlAddress,
151+
opts...,
152+
)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
switch method {
158+
case http.MethodGet, http.MethodPut, http.MethodPost, http.MethodPatch:
159+
var doOptions []uhttp.DoOption
160+
if res != nil {
161+
doOptions = append(doOptions, uhttp.WithResponse(&res))
162+
}
163+
resp, err = c.client.Do(req, doOptions...)
164+
if resp != nil {
165+
defer resp.Body.Close()
166+
}
167+
168+
case http.MethodDelete:
169+
resp, err = c.client.Do(req)
170+
if resp != nil {
171+
defer resp.Body.Close()
172+
}
173+
}
174+
if err != nil {
175+
return nil, err
176+
}
177+
178+
return resp.Header, nil
179+
}
180+
181+
func New(ctx context.Context, scimToken string) (*NotionClient, error) {
182+
httpClient, err := uhttp.NewClient(ctx, uhttp.WithLogger(true, ctxzap.Extract(ctx)))
183+
if err != nil {
184+
return nil, err
185+
}
186+
187+
cli, err := uhttp.NewBaseHttpClientWithContext(ctx, httpClient)
188+
if err != nil {
189+
return nil, err
190+
}
191+
192+
notionClient := NotionClient{
193+
client: cli,
194+
scimToken: scimToken,
195+
}
196+
197+
return &notionClient, nil
198+
}

pkg/client/models.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package client
2+
3+
type Group struct {
4+
Schemas []string `json:"schemas"`
5+
ID string `json:"id"`
6+
DisplayName string `json:"displayName"`
7+
Members []Member `json:"members"`
8+
}
9+
10+
type GroupsResponse struct {
11+
TotalResults int64 `json:"totalResults"`
12+
Resources []Group `json:"Resources"`
13+
StartIndex int64 `json:"startIndex"`
14+
ItemsPerPage int64 `json:"itemsPerPage"`
15+
}
16+
17+
type Member struct {
18+
Value string `json:"value"`
19+
Ref string `json:"$ref"`
20+
Type string `json:"type"`
21+
}
22+
23+
type UsersResponse struct {
24+
TotalResults int64 `json:"totalResults"`
25+
Resources []User `json:"Resources"`
26+
StartIndex int64 `json:"startIndex"`
27+
ItemsPerPage int64 `json:"itemsPerPage"`
28+
}
29+
30+
type User struct {
31+
ID string `json:"id"`
32+
Schemas []string `json:"schemas"`
33+
UserName string `json:"userName"` // Username corresponds to the email of the account.
34+
Name struct {
35+
GivenName string `json:"givenName"`
36+
FamilyName string `json:"familyName"`
37+
Formatted string `json:"formatted"`
38+
} `json:"name"`
39+
Emails []struct {
40+
Primary bool `json:"primary"`
41+
Value string `json:"value"`
42+
Type string `json:"type"`
43+
} `json:"emails"`
44+
// Title string `json:"title"`
45+
Active bool `json:"active"`
46+
}

0 commit comments

Comments
 (0)