Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
# baton-notion
`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.
`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.

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

# Getting Started

## Prerequisites

1. Notion account with a workspace
1. Enterprise Notion account with a workspace
2. Admin level access to the workspace
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).
5. Set capabilities:
- Read content
- Read user information including email addresses
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).
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`.
3. SCIM API Token

## brew

```
brew install conductorone/baton/baton conductorone/baton/baton-notion
baton-notion
baton-notion --scim-token <scim_token>
baton resources
```

Expand All @@ -37,17 +32,17 @@ docker run --rm -v $(pwd):/out ghcr.io/conductorone/baton:latest -f "/out/sync.c
go install github.com/conductorone/baton/cmd/baton@main
go install github.com/conductorone/baton-notion/cmd/baton-notion@main

BATON_API_KEY=apiKey
baton-notion --scim-token <scim_token>

baton resources
```

# Data Model

`baton-notion` pulls down information about the following Notion resources:
- Users
- Groups (only with Notion Enterprise Plan)
- Groups

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.

# Contributing, Support, and Issues

Expand All @@ -70,15 +65,14 @@ Available Commands:
help Help about any command

Flags:
--api-key string The Notion API key used to connect to the Notion API. ($BATON_API_KEY)
--scim-token string The Notion SCIM token used to connect to the Notion SCIM API. ($BATON_SCIM_TOKEN)
--client-id string The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID)
--client-secret string The client secret used to authenticate with ConductorOne ($BATON_CLIENT_SECRET)
-f, --file string The path to the c1z file to sync with ($BATON_FILE) (default "sync.c1z")
-h, --help help for baton-notion
--log-format string The output format for logs: json, console ($BATON_LOG_FORMAT) (default "json")
--log-level string The log level: debug, info, warn, error ($BATON_LOG_LEVEL) (default "info")
-p, --provisioning This must be set in order for provisioning actions to be enabled. ($BATON_PROVISIONING)
--scim-token string The Notion SCIM token used to connect to the Notion SCIM API. ($BATON_SCIM_TOKEN)
-v, --version version for baton-notion

Use "baton-notion [command] --help" for more information about a command.
Expand Down
9 changes: 1 addition & 8 deletions cmd/baton-notion/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,17 @@ import (
)

const (
apiKeyFlag = "api-key"
scimTokenFlag = "scim-token"
)

var (
APIKeyField = field.StringField(
apiKeyFlag,
field.WithRequired(true),
field.WithDescription("The Notion API key used to connect to the Notion API. ($BATON_API_KEY)"),
)

SCIMTokenField = field.StringField(
scimTokenFlag,
field.WithRequired(false),
field.WithDescription("The Notion SCIM token used to connect to the Notion SCIM API. ($BATON_SCIM_TOKEN)"),
)

ConfigurationFields = []field.SchemaField{APIKeyField, SCIMTokenField}
ConfigurationFields = []field.SchemaField{SCIMTokenField}
)

// ValidateConfig is run after the configuration is loaded, and should return an
Expand Down
3 changes: 1 addition & 2 deletions cmd/baton-notion/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ func getConnector(ctx context.Context, v *viper.Viper) (types.ConnectorServer, e
return nil, err
}

apiKey := v.GetString(apiKeyFlag)
scimToken := v.GetString(scimTokenFlag)

cb, err := connector.New(ctx, apiKey, scimToken)
cb, err := connector.New(ctx, scimToken)
if err != nil {
l.Error("error creating connector", zap.Error(err))
return nil, err
Expand Down
28 changes: 28 additions & 0 deletions docs/docs-info
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Connector capabilities

1. What resources does the connector sync?
- Notion connector syncs Users and Groups.

2. Can the connector provision any resources? If so, which ones?
- This connector can provision Accounts.

## Connector credentials

1. What credentials or information are needed to set up the connector? (For example, API key, client ID and secret, domain, etc.)
- An API Token for the SCIM API should be provided.

2. For each item in the list above:

* 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.
- 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.
- On the settings search for the 'Identity' tab on the left-panel and scroll down to the "SCIM Provisioning" section.
- In the "SCIM Provisioning" section, users should be able to create a Token to use the SCIM API.

* Does the credential need any specific scopes or permissions? If so, list them here.
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.

* If applicable: Is the list of scopes or permissions different to sync (read) versus provision (read-write)? If so, list the difference here.
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.

* 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.)
- It should be an Organization Owner.
198 changes: 198 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package client

import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"

"github.com/conductorone/baton-sdk/pkg/uhttp"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
)

const (
baseUrl = "https://www.notion.so/scim/v2"
DefaultUserSchema = "urn:ietf:params:scim:schemas:core:2.0:User"
)

type NotionClient struct {
client *uhttp.BaseHttpClient
scimToken string
}

func (c *NotionClient) GetUsers(ctx context.Context, pageOps PaginationOptions) ([]User, string, error) {
var nextPage string
requestURL := fmt.Sprint(baseUrl, "/Users")

var res UsersResponse
_, err := c.doRequest(
ctx,
http.MethodGet,
requestURL,
&res,
nil,
WithPageSize(pageOps.PerPage),
WithStartIndex(pageOps.StartIndex),
)
if err != nil {
return nil, "", err
}

if (int64(pageOps.StartIndex) + res.ItemsPerPage) < res.TotalResults {
nextPage = strconv.FormatInt(int64(pageOps.StartIndex)+res.ItemsPerPage, 10)
}

return res.Resources, nextPage, nil
}

// GetGroups returns all Notion groups.
func (c *NotionClient) GetGroups(ctx context.Context, pageOps PaginationOptions) ([]Group, string, error) {
var nextPage string
requestURL := fmt.Sprint(baseUrl, "/Groups")

var res GroupsResponse
_, err := c.doRequest(
ctx,
http.MethodGet,
requestURL,
&res,
nil,
WithPageSize(pageOps.PerPage),
WithStartIndex(pageOps.StartIndex),
)
if err != nil {
return nil, "", err
}

if (int64(pageOps.StartIndex) + res.ItemsPerPage) < res.TotalResults {
nextPage = strconv.FormatInt(int64(pageOps.StartIndex)+res.ItemsPerPage, 10)
}

return res.Resources, nextPage, nil
}

// GetGroup returns group details by group ID.
func (c *NotionClient) GetGroup(ctx context.Context, groupId string) (Group, error) {
requestURL := fmt.Sprint(baseUrl, "/Groups/", groupId)

var groupResponse Group
_, err := c.doRequest(ctx, http.MethodGet, requestURL, &groupResponse, nil)
if err != nil {
return Group{}, err
}

return groupResponse, nil
}

func (c *NotionClient) GetUser(ctx context.Context, userID string) (*User, error) {
var userData *User
requestURL := fmt.Sprint(baseUrl, "/Users/", userID)

_, err := c.doRequest(ctx, http.MethodGet, requestURL, &userData, nil)
if err != nil {
return nil, err
}

return userData, nil
}

func (c *NotionClient) CreateUser(ctx context.Context, user *User) (*User, error) {
var newUser *User
requestURL := fmt.Sprint(baseUrl, "/Users")

_, err := c.doRequest(ctx, http.MethodPost, requestURL, &newUser, user)
if err != nil {
return nil, err
}

return newUser, nil
}

func (c *NotionClient) DeleteUser(ctx context.Context, userID string) error {
requestURL := fmt.Sprint(baseUrl, "/Users/", userID)

_, err := c.doRequest(ctx, http.MethodDelete, requestURL, nil, nil)
if err != nil {
return err
}

return nil
}

func (c *NotionClient) doRequest(
ctx context.Context,
method string,
endpointUrl string,
res interface{},
body interface{},
reqOpts ...ReqOpt,
) (http.Header, error) {
var resp *http.Response

urlAddress, err := url.Parse(endpointUrl)
if err != nil {
return nil, err
}

for _, o := range reqOpts {
o(urlAddress)
}

opts := []uhttp.RequestOption{uhttp.WithBearerToken(c.scimToken)}
if body != nil {
opts = append(opts, uhttp.WithAcceptJSONHeader(), uhttp.WithContentTypeJSONHeader(), uhttp.WithJSONBody(body))
}

req, err := c.client.NewRequest(
ctx,
method,
urlAddress,
opts...,
)
if err != nil {
return nil, err
}

switch method {
case http.MethodGet, http.MethodPut, http.MethodPost, http.MethodPatch:
var doOptions []uhttp.DoOption
if res != nil {
doOptions = append(doOptions, uhttp.WithResponse(&res))
}
resp, err = c.client.Do(req, doOptions...)
if resp != nil {
defer resp.Body.Close()
}

case http.MethodDelete:
resp, err = c.client.Do(req)
if resp != nil {
defer resp.Body.Close()
}
}
if err != nil {
return nil, err
}

return resp.Header, nil
}

func New(ctx context.Context, scimToken string) (*NotionClient, error) {
httpClient, err := uhttp.NewClient(ctx, uhttp.WithLogger(true, ctxzap.Extract(ctx)))
if err != nil {
return nil, err
}

cli, err := uhttp.NewBaseHttpClientWithContext(ctx, httpClient)
if err != nil {
return nil, err
}

notionClient := NotionClient{
client: cli,
scimToken: scimToken,
}

return &notionClient, nil
}
46 changes: 46 additions & 0 deletions pkg/client/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package client

type Group struct {
Schemas []string `json:"schemas"`
ID string `json:"id"`
DisplayName string `json:"displayName"`
Members []Member `json:"members"`
}

type GroupsResponse struct {
TotalResults int64 `json:"totalResults"`
Resources []Group `json:"Resources"`
StartIndex int64 `json:"startIndex"`
ItemsPerPage int64 `json:"itemsPerPage"`
}

type Member struct {
Value string `json:"value"`
Ref string `json:"$ref"`
Type string `json:"type"`
}

type UsersResponse struct {
TotalResults int64 `json:"totalResults"`
Resources []User `json:"Resources"`
StartIndex int64 `json:"startIndex"`
ItemsPerPage int64 `json:"itemsPerPage"`
}

type User struct {
ID string `json:"id"`
Schemas []string `json:"schemas"`
UserName string `json:"userName"` // Username corresponds to the email of the account.
Name struct {
GivenName string `json:"givenName"`
FamilyName string `json:"familyName"`
Formatted string `json:"formatted"`
} `json:"name"`
Emails []struct {
Primary bool `json:"primary"`
Value string `json:"value"`
Type string `json:"type"`
} `json:"emails"`
// Title string `json:"title"`
Active bool `json:"active"`
}
Loading
Loading