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
167 changes: 140 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,64 @@ Display the current version of the package.
#### `datacustomcode configure`
Configure credentials for connecting to Data Cloud.

**Prerequisites:**
- A [connected app](#creating-a-connected-app) with OAuth settings configured
- For OAuth Tokens authentication: [refresh token and core token](#obtaining-refresh-token-and-core-token)

Options:
- `--profile TEXT`: Credential profile name (default: "default")
- `--auth-type TEXT`: Authentication method (`oauth_tokens` or `username_password`, default: `oauth_tokens`)
- `--login-url TEXT`: Salesforce login URL

For Username/Password authentication:
- `--username TEXT`: Salesforce username
- `--password TEXT`: Salesforce password
- `--client-id TEXT`: Connected App Client ID
- `--client-secret TEXT`: Connected App Client Secret
- `--login-url TEXT`: Salesforce login URL

For OAuth Tokens authentication:
- `--client-id TEXT`: Connected App Client ID
- `--client-secret TEXT`: Connected App Client Secret
- `--refresh-token TEXT`: OAuth refresh token (see [Obtaining Refresh Token](#obtaining-refresh-token-and-core-token))
- `--core-token TEXT`: (Optional) OAuth core/access token - if not provided, it will be obtained using the refresh token

##### Using Environment Variables (Alternative)

Instead of using `datacustomcode configure`, you can also set credentials via environment variables.

> [!NOTE]
> Environment variables take precedence over the credentials INI file when both are present.

**Common (required for all auth types):**
| Variable | Description |
|----------|-------------|
| `SFDC_LOGIN_URL` | Salesforce login URL (e.g., `https://login.salesforce.com`) |
| `SFDC_CLIENT_ID` | External Client App Client ID |
| `SFDC_AUTH_TYPE` | Authentication type: `oauth_tokens` (default) or `username_password` |

**For OAuth Tokens authentication (`SFDC_AUTH_TYPE=oauth_tokens`):**
| Variable | Description |
|----------|-------------|
| `SFDC_CLIENT_SECRET` | External Client App Client Secret |
| `SFDC_REFRESH_TOKEN` | OAuth refresh token |
| `SFDC_CORE_TOKEN` | (Optional) OAuth core/access token |

**For Username/Password authentication (`SFDC_AUTH_TYPE=username_password`):**
| Variable | Description |
|----------|-------------|
| `SFDC_USERNAME` | Salesforce username |
| `SFDC_PASSWORD` | Salesforce password |
| `SFDC_CLIENT_SECRET` | External Client App Client Secret |

Example usage:
```bash
export SFDC_LOGIN_URL="https://login.salesforce.com"
export SFDC_CLIENT_ID="your_client_id"
export SFDC_CLIENT_SECRET="your_client_secret"
export SFDC_REFRESH_TOKEN="your_refresh_token"

datacustomcode run ./payload/entrypoint.py
```


#### `datacustomcode init`
Expand Down Expand Up @@ -295,35 +346,97 @@ You can read more about Jupyter Notebooks here: https://jupyter.org/

## Prerequisite details

### Creating a connected app

1. Log in to salesforce as an admin. In the top right corner, click on the gear icon and go to `Setup`
2. In the left hand column search for `oauth`
3. Select `OAuth and OpenID Connect Settings`
4. Toggle on `Allow OAuth Username-Password Flows` and accept the dialog box that pops up
5. Clear the search bar
6. Expand `Apps`, expand `External Client Apps`, click `Settings`
7. Toggle on `Allow access to External Client App consumer secrets via REST API`
8. Toggle on `Allow creation of connected apps`
9. Click `Enable` in the warning box
10. Click `New Connected App` button
11. Fill in the required fields within the `Basic Information` section
12. Under the `API (Enable OAuth Settings)` section:
a. Click on the checkbox to Enable OAuth Settings.
b. Provide a callback URL like http://localhost:55555/callback
c. In the Selected OAuth Scopes, make sure that `refresh_token`, `api`, `cdp_query_api`, `cdp_profile_api` is selected.
d. Click on Save to save the connected app
13. From the detail page that opens up afterwards, click the `Manage Consumer Details` button to find your client id and client secret
14. Click `Cancel` button once complete
15. Click `Manage` button
16. Click `Edit Policies`
17. Under `IP Relaxation` select `Relax IP restrictions`
18. Click `Save`
19. Logout
20. Use the URL of the login page as the `login_url` value when setting up the SDK
### Creating an External Client app

1. Log in to Salesforce as an admin. In the top right corner, click on the gear icon and go to `Setup`
2. On the left sidebar, expand `Apps`, expand `External Client Apps`, click `Settings`
3. Expand `Apps`, expand `External Client Apps`, click `External Client App Manager`
4. Click `New External Client App` button
5. Fill in the required fields within the `Basic Information` section
6. Under the `API (Enable OAuth Settings)` section:
1. Click on the checkbox to Enable OAuth Settings
2. Provide a callback URL like `http://localhost:5555/callback`
3. In the Selected OAuth Scopes, make sure that `refresh_token`, `api`, `cdp_query_api`, `cdp_profile_api` are selected
4. Check the following:
- Enable Authorization Code and Credentials Flow
- Require user credentials in the POST body for Authorization Code and Credentials Flow
5. Uncheck `Require Proof Key for Code Exchange (PKCE) extension for Supported Authorization Flows`
6. Click on `Create` button
7. On your newly created External Client App page, on the `Policies` tab:
1. In the `App Authorization` section, choose an appropriate Refresh Token Policy as per your expected usage and preference.
2. Under `App Authorization`, set IP Relaxation to `Relax IP restrictions` unless otherwise needed
8. Click `Save`
9. Go to the `Settings` tab, under `OAuth Settings`. There, you can click on the `Consumer Key and Secret` button which will open a new tab. There you can copy the `client_id` and `client_secret` values which are to be used during configuring credentials using this SDK.
10. Logout
11. Use the URL of the login page as the `login_url` value when setting up the SDK

You now have all fields necessary for the `datacustomcode configure` command.

### Obtaining Refresh Token and Core Token

If you're using OAuth Tokens authentication (instead of Username/Password), follow these steps to obtain your refresh token and core token (access token).

#### Step 1: Note Connected App Details

From your connected app, note down the following:
- **Client ID**
- **Client Secret**
- **Callback URL** (e.g., `http://localhost:55555/callback`)

#### Step 2: Obtain Authorization Code

1. Open a browser and navigate to the following URL (replace placeholders with your values):

```
<LOGIN_URL>/services/oauth2/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>
```

2. After authenticating, you'll be redirected to your callback URL. The redirected URL will be in the form:
```
<CALLBACK_URL>?code=<CODE>
```

3. Extract the `<CODE>` from the address bar. If the address bar doesn't show it, check the **Network tab** in your browser's developer tools.

#### Step 3: Exchange Code for Tokens

Make a POST request to exchange the authorization code for tokens. You can use `curl` or Postman:

```bash
curl --location --request POST '<LOGIN_URL>/services/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=<CODE>' \
--data-urlencode 'client_id=<CLIENT_ID>' \
--data-urlencode 'client_secret=<CLIENT_SECRET>' \
--data-urlencode 'redirect_uri=<CALLBACK_URL>'
```

The response will be a JSON object containing:

```json
{
"access_token": "<access_token>",
"refresh_token": "<refresh_token>",
"signature": "<signature>",
"scope": "refresh_token cdp_query_api api cdp_profile_api cdp_api full",
"id_token": "<id_token>",
"instance_url": "https://your-instance.my.salesforce.com",
"id": "https://login.salesforce.com/id/00DSB.../005SB...",
"token_type": "Bearer",
"issued_at": "1767743916187"
}
```

The key fields you need are:
| Field | Description |
|-------|-------------|
| `access_token` | The **core token** (also called access token) |
| `refresh_token` | The **refresh token** for obtaining new access tokens |
| `instance_url` | Your Salesforce instance URL |

Use the `refresh_token` value when running `datacustomcode configure` with OAuth Tokens authentication.

## Other docs

- [Troubleshooting](./docs/troubleshooting.md)
Expand Down
9 changes: 8 additions & 1 deletion src/datacustomcode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@
# limitations under the License.

from datacustomcode.client import Client
from datacustomcode.credentials import AuthType, Credentials
from datacustomcode.io.reader.query_api import QueryAPIDataCloudReader
from datacustomcode.io.writer.print import PrintDataCloudWriter

__all__ = ["Client", "QueryAPIDataCloudReader", "PrintDataCloudWriter"]
__all__ = [
"AuthType",
"Client",
"Credentials",
"PrintDataCloudWriter",
"QueryAPIDataCloudReader",
]
93 changes: 77 additions & 16 deletions src/datacustomcode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,91 @@ def version():
click.echo("Version information not available")


@cli.command()
@click.option("--profile", default="default")
@click.option("--username", prompt=True)
@click.option("--password", prompt=True, hide_input=True)
@click.option("--client-id", prompt=True)
@click.option("--client-secret", prompt=True)
@click.option("--login-url", prompt=True)
def configure(
username: str,
password: str,
client_id: str,
client_secret: str,
def _configure_username_password(
login_url: str,
client_id: str,
profile: str,
) -> None:
from datacustomcode.credentials import Credentials
"""Configure credentials for Username/Password authentication."""
from datacustomcode.credentials import AuthType, Credentials

username = click.prompt("Username")
password = click.prompt("Password", hide_input=True)
client_secret = click.prompt("Client Secret")

Credentials(
credentials = Credentials(
login_url=login_url,
client_id=client_id,
auth_type=AuthType.USERNAME_PASSWORD,
username=username,
password=password,
client_id=client_id,
client_secret=client_secret,
)
credentials.update_ini(profile=profile)
click.secho(
f"Username/Password credentials saved to profile '{profile}' successfully",
fg="green",
)


def _configure_oauth_tokens(
login_url: str,
client_id: str,
profile: str,
) -> None:
"""Configure credentials for OAuth Tokens authentication."""
from datacustomcode.credentials import AuthType, Credentials

client_secret = click.prompt("Client Secret")
refresh_token = click.prompt("Refresh Token")
core_token = click.prompt(
"Core Token (optional, press Enter to skip)",
default="",
show_default=False,
)

credentials = Credentials(
login_url=login_url,
).update_ini(profile=profile)
client_id=client_id,
auth_type=AuthType.OAUTH_TOKENS,
client_secret=client_secret,
refresh_token=refresh_token,
core_token=core_token if core_token else None,
)
credentials.update_ini(profile=profile)
click.secho(
f"OAuth Tokens credentials saved to profile '{profile}' successfully",
fg="green",
)


@cli.command()
@click.option("--profile", default="default", help="Credential profile name")
@click.option(
"--auth-type",
type=click.Choice(["oauth_tokens", "username_password"]),
default="oauth_tokens",
help="""Authentication method to use.

\b
oauth_tokens - OAuth tokens (refresh_token/core_token) authentication [DEFAULT]
username_password - Traditional username/password OAuth flow
""",
)
def configure(profile: str, auth_type: str) -> None:
"""Configure credentials for connecting to Data Cloud."""
from datacustomcode.credentials import AuthType

# Common fields for all auth types
click.echo(f"\nConfiguring {auth_type} authentication for profile '{profile}':\n")
login_url = click.prompt("Login URL")
client_id = click.prompt("Client ID")

# Route to appropriate handler based on auth type
if auth_type == AuthType.USERNAME_PASSWORD.value:
_configure_username_password(login_url, client_id, profile)
elif auth_type == AuthType.OAUTH_TOKENS.value:
_configure_oauth_tokens(login_url, client_id, profile)


@cli.command()
Expand Down
Loading