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
105 changes: 101 additions & 4 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ a configuration file. The priority order from highest to lowest is:
2. **Environment variables** - override config file values
3. **Configuration file** (active context) - overrides defaults
4. **Built-in defaults** - used when nothing else is specified
5. **Interactive prompt** - fallback for mandatory values (user, password) when no SNC config is present
5. **Interactive prompt** - fallback for mandatory values (user, password) when no SNC config is present and no valid OAuth token is cached

## Parameters

Expand Down Expand Up @@ -188,6 +188,23 @@ contexts:
password: prod-secret # overrides user
```

A connection that uses OAuth 2.0 instead of a password is defined the same
way, with three additional fields:

```yaml
connections:
my-cloud-system:
ashost: my-tenant.abap.eu10.hana.ondemand.com
client: "100"
port: 443
ssl: true
token_url: https://my-tenant.authentication.eu10.hana.ondemand.com
client_id: sb-abap!t12345
client_secret: my-client-secret
```

See [OAuth 2.0 authentication](#oauth-20-authentication) below for details.

### Field reference

#### `connections.<name>`
Expand All @@ -209,9 +226,15 @@ contexts:
| `snc_myname` | string | no | - | `SNC_MYNAME` |
| `snc_partnername` | string | no | - | `SNC_PARTNERNAME` |
| `snc_lib` | string | no | - | `SNC_LIB` |
| `token_url` | string | no | - | `SAP_TOKEN_URL` |
| `client_id` | string | no | - | `SAP_CLIENT_ID` |
| `client_secret` | string | no | - | `SAP_CLIENT_SECRET` |

(*) Either `ashost` or `mshost` must be provided.

The `token_url`, `client_id`, and `client_secret` fields enable OAuth 2.0
authentication. See [OAuth 2.0 authentication](#oauth-20-authentication) below.

#### `users.<name>`

| Field | Type | Required | Default | Env var equivalent |
Expand Down Expand Up @@ -249,12 +272,77 @@ fields (e.g. hostname). Define one base connection and override per context.
Storing passwords in plain text configuration files is a security concern.
The recommended approaches, in order of preference:

1. **Omit the password from config** - sapcli will prompt interactively
2. **Use environment variables** - `SAP_PASSWORD` overrides the config file; suitable for CI/CD pipelines
3. **Store in config file** - acceptable for local development if the file has restrictive permissions (`chmod 600`)
1. **Use OAuth 2.0** - if your system supports it (e.g. SAP cloud systems),
prefer OAuth over a stored password. See
[OAuth 2.0 authentication](#oauth-20-authentication) below.
2. **Omit the password from config** - sapcli will prompt interactively
3. **Use environment variables** - `SAP_PASSWORD` overrides the config file; suitable for CI/CD pipelines
4. **Store in config file** - acceptable for local development if the file has restrictive permissions (`chmod 600`)

sapcli will warn if the config file is world-readable and contains passwords.

The same caveats apply to `client_secret` when OAuth is used.

### OAuth 2.0 authentication

Some SAP systems — most notably SAP cloud systems such as SAP BTP ABAP
Environment ("Steampunk") — require OAuth 2.0 instead of a username/password
pair. sapcli can authenticate with OAuth and caches the obtained token
between commands so you do not need to log in every time.

#### Enabling OAuth

OAuth is enabled by setting three values on the connection definition, in
addition to your usual `--user`/`SAP_USER`:

| Field on `connections.<name>` | Env var | Description |
|---|---|---|
| `token_url` | `SAP_TOKEN_URL` | Base URL of the OAuth authorization server. sapcli appends `/oauth/token` automatically — provide the base, not the full endpoint. |
| `client_id` | `SAP_CLIENT_ID` | OAuth client ID issued by the system administrator. |
| `client_secret` | `SAP_CLIENT_SECRET` | OAuth client secret issued by the system administrator. |

These three values are not exposed as **global** command-line flags. They can
be provided via environment variables, written directly into the configuration
file under `connections.<name>` (see the YAML example in [Schema](#schema)),
or set with `sapcli config set-connection`:

```bash
sapcli config set-connection my-cloud-system \
--token-url https://my-tenant.authentication.eu10.hana.ondemand.com \
--client-id sb-abap!t12345 \
--client-secret <secret>
```

These fields describe the OAuth **application** registration on the target
system, not the individual user — that is why they sit under `connections:`
alongside `ashost`/`port`, while your user name still belongs under `users:`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use username consistently for wording polish.

These lines currently use “user name”; “username” is the more consistent form in technical docs.

Also applies to: 325-325, 336-336

🧰 Tools
🪛 LanguageTool

[style] ~318-~318: It’s more common nowadays to write this noun as one word.
Context: ...alongsideashost/port, while your user name still belongs under users:`. A typical...

(RECOMMENDED_COMPOUNDS)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/configuration.md` at line 318, Replace the phrase "user name" with the
single-word "username" throughout the configuration text where it refers to the
config key and descriptions (e.g., the sentence that mentions "alongside
`ashost`/`port`, while your user name still belongs under `users:`" and the
other instances), ensuring inline code and prose use "username" consistently and
update any nearby occurrences that describe the same concept for uniform
wording.

A typical setup has one OAuth client per tenant, shared by all team members,
each with their own `users.<name>.user`.

#### How sapcli obtains a token

The first time you run a command against an OAuth-enabled connection, sapcli
asks the OAuth server for a token using your user name and password. This is
the only step that needs your password. After that, the token is cached in
`~/.sapcli/tokens.json` (file permissions `0600`) and reused by all subsequent
commands. When the token approaches expiration, sapcli refreshes it
transparently using a refresh token — no password is needed for the refresh.

If a valid cached token exists, sapcli does **not** prompt for a password,
even if `SAP_PASSWORD` is unset and the configuration file contains none.

If the OAuth server rejects your credentials or is unreachable, sapcli prints
an `OAuthTokenError` with the HTTP status code and the server's response
body. Verify `token_url`, `client_id`, `client_secret`, your user name, and
your password.

To force a fresh login (e.g. after rotating credentials), delete the cache
file:

```bash
rm ~/.sapcli/tokens.json
```

## Config management commands

```bash
Expand Down Expand Up @@ -295,6 +383,12 @@ sapcli config set-connection dev-server --ashost dev.example.com --client 100 --
# Update an existing connection (only specified fields change, others preserved)
sapcli config set-connection dev-server --port 8443

# Add OAuth 2.0 credentials to a connection (see "OAuth 2.0 authentication" below)
sapcli config set-connection cloud-srv \
--token-url https://auth.example.com \
--client-id sb-app!t12345 \
--client-secret my-client-secret

# List all connections
sapcli config get-connections

Expand Down Expand Up @@ -403,6 +497,9 @@ targeting different systems. It also composes well with tools like
- `SAP_PASSWORD` : default value for the command line parameter --password
- `SAP_SSL_SERVER_CERT` : path to the public unencrypted server SSL certificate
- `SAP_SSL_VERIFY` : if "no", SSL server certificate is no validated - this works only when SAP_SSL_SERVER_CERT is not configured
- `SAP_TOKEN_URL` : base URL of the OAuth 2.0 authorization server; corresponds to `connections.<name>.token_url` (enables OAuth authentication; see [OAuth 2.0 authentication](#oauth-20-authentication))
- `SAP_CLIENT_ID` : OAuth 2.0 client ID; corresponds to `connections.<name>.client_id`
- `SAP_CLIENT_SECRET` : OAuth 2.0 client secret; corresponds to `connections.<name>.client_secret`
- `SAP_CORRNR` : if a sapcli command accepts parameter '--corrnr', you can provide default value via this environment variable
- `SAPCLI_CONFIG` : path to the configuration file (overrides the default `~/.sapcli/config.yml`)
- `SAPCLI_CONTEXT` : name of the context to use (overrides `current-context` in the config file; overridden by `--context` CLI flag)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies = [
"requests>=2.20.0",
"pyodata>=1.7.0",
"PyYAML>=6.0.1",
"platformdirs>=4.5.1",
]

[project.urls]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
requests>=2.20.0
pyodata==1.7.0
PyYAML==6.0.1
platformdirs>=4.5.1
49 changes: 48 additions & 1 deletion sap/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from types import SimpleNamespace
from sap import rfc
from sap.config import SAPCliConfigError
from sap.errors import SAPCliError


class CommandsCache:
Expand Down Expand Up @@ -128,10 +129,38 @@ def adt_connection_from_args(args):

import sap.adt

session_initializer = _build_session_initializer(args)

return sap.adt.Connection(
args.ashost, args.client, args.user, args.password,
port=args.port, ssl=args.ssl, verify=args.verify,
ssl_server_cert=args.ssl_server_cert)
ssl_server_cert=args.ssl_server_cert,
session_initializer=session_initializer)


def _build_session_initializer(args):
"""Build an OAuthHTTPSessionInitializer when args.token_url is set,
otherwise return None so HTTPClient falls back to BasicAuth.
"""

token_url = args.token_url
client_id = args.client_id
client_secret = args.client_secret
if not token_url and not client_id and not client_secret:
return None

if not token_url or not client_id or not client_secret:
raise SAPCliError('Invalid OAuth configuration: must set all three: token_url, client_id, client_secret')

from sap.http.oauth import OAuthHTTPSessionInitializer

return OAuthHTTPSessionInitializer(
token_url,
client_id,
client_secret,
args.user,
args.password,
)


def rfc_connection_from_args(args):
Expand Down Expand Up @@ -204,6 +233,9 @@ def build_empty_connection_values():
ssl_server_cert=None,
user=None,
password=None,
token_url=None,
client_id=None,
client_secret=None,
)


Expand Down Expand Up @@ -287,6 +319,8 @@ def resolve_default_connection_values(args):
if not args.password:
args.password = os.getenv('SAP_PASSWORD') or config_values.get('password')

_resolve_oauth_defaults(args, config_values)

if hasattr(args, 'corrnr') and args.corrnr is None:
args.corrnr = os.getenv('SAP_CORRNR')

Expand All @@ -295,6 +329,19 @@ def resolve_default_connection_values(args):
_apply_config_extra_params(args, config_values)


def _resolve_oauth_defaults(args, config_values):
"""Resolve OAuth-specific connection defaults from env vars and config file."""

if not getattr(args, 'token_url', None):
args.token_url = os.getenv('SAP_TOKEN_URL') or config_values.get('token_url')

if not getattr(args, 'client_id', None):
args.client_id = os.getenv('SAP_CLIENT_ID') or config_values.get('client_id')

if not getattr(args, 'client_secret', None):
args.client_secret = os.getenv('SAP_CLIENT_SECRET') or config_values.get('client_secret')


def _get_config_context_values(args):
"""Load config file and resolve the active context to a flat dict."""

Expand Down
5 changes: 4 additions & 1 deletion sap/cli/_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import sap.rfc
from sap.config import ConfigFile
from sap.http import TimedOutRequestError as HttpTimedOutRequestError
import sap.http.oauth
from sap.odata.errors import TimedOutRequestError as ODataTimedOutRequestError

# pylint: disable=invalid-name
Expand Down Expand Up @@ -157,7 +158,9 @@ def parse_command_line(argv):
if not args.user:
args.user = input('Login:')

if not args.password:
oauth_needs_password = sap.http.oauth.password_required(args.token_url, args.client_id)

if not args.password and oauth_needs_password:
args.password = getpass.getpass()

return args
Expand Down
6 changes: 6 additions & 0 deletions sap/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ def _collect_fields(args, field_names):

# -- set-connection -----------------------------------------------------------

@CommandGroup.argument('--client-secret', dest='client_secret', default=None,
help='OAuth 2.0 client secret')
@CommandGroup.argument('--client-id', dest='client_id', default=None,
help='OAuth 2.0 client ID')
@CommandGroup.argument('--token-url', dest='token_url', default=None,
help='OAuth 2.0 authorization server base URL')
@CommandGroup.argument('--snc-lib', dest='snc_lib', default=None,
help='Path to SNC library')
@CommandGroup.argument('--snc-partnername', dest='snc_partnername', default=None,
Expand Down
1 change: 1 addition & 0 deletions sap/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SAPCliConfigError(SAPCliError):
'ashost', 'sysnr', 'client', 'port', 'ssl', 'ssl_verify',
'ssl_server_cert', 'mshost', 'msserv', 'sysid', 'group',
'snc_qop', 'snc_myname', 'snc_partnername', 'snc_lib',
'token_url', 'client_id', 'client_secret',
)

USER_FIELDS = (
Expand Down
Loading
Loading