http: introduce auth-plugin protocol module#161
Conversation
Adds sap/http/auth_plugin.py: the request/response shape and the
run_plugin() driver for kubectl-style external authentication helpers.
This is step 1 of the auth-plugin track in features/auth_plugins.md and
is independent of CLI wiring and the response cache - those land in
follow-up commits.
Why a kubectl-style plugin (vs. building auth methods in-tree):
sapcli must support SAML2/SSO and Windows-cert SSO without taking on
browser-automation or Win32-API dependencies. Spawning an external
command and exchanging JSON over stdin/stdout keeps sapcli's
dependency surface minimal and lets each plugin be implemented in
whatever language fits the platform (e.g. playwright on Windows).
Design decisions:
- subprocess.run with check=False and no timeout. Browser-based plugins
legitimately wait minutes for the user to log in; a default timeout
would kill them mid-flow. check=False so we surface our own
AuthPluginError with stdout+stderr included instead of a bare
CalledProcessError.
- AuthPluginError derives from SAPCliError so the CLI entry point
prints a friendly message instead of a stack trace, matching every
other user-visible error in the project.
- OSError catches both FileNotFoundError (plugin not on PATH) and
PermissionError (plugin not executable); wrapping at OSError keeps
the failure path uniform without enumerating each subclass.
- Response validation happens here for the envelope (message, content,
optional expiration) but does NOT inspect content's internal shape -
that is the session-initializer's job, which dispatches on
content.type. Keeps this module's contract narrow.
- Expiration normalises a trailing 'Z' to '+00:00' before
datetime.fromisoformat. Python 3.10's parser does not understand the
Z suffix; doing it here lets the rest of the codebase stay portable.
- ConnectionInfo carries port as str (matching the wire format in the
spec, e.g. "44300") rather than int. The CLI parses port as int, so
the caller is responsible for coercion - we do not silently mutate
user input.
- ConnectionInfo.sysnr is Optional[str] (default None). HTTP-only
plugins have no use for the SAP system / instance number; making it
required would force every caller to pass fake data. RFC-aware
plugins (which need it to derive the gateway port 33<sysnr>) set
it explicitly. The field is always emitted in to_dict (as null when
unset) so plugin authors see a stable wire shape.
- parameters defaults to {} when None is passed, so callers do not
have to special-case "no plugin parameters" before calling.
31 unit tests cover serialization (including sysnr present/absent),
response parsing (happy path plus schema-drift edge cases like
array-instead-of-object, null/empty expiration, Z-suffix and offset
forms), the subprocess invocation contract (argv shape, stdin payload,
capture_output, encoding, no-timeout, no-check), and every failure
branch (missing executable, permission denied, non-zero exit, invalid
JSON, missing required fields).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis pull request introduces a new external authentication plugin protocol for sapcli. It defines typed data models ( ChangesExternal Auth Plugin Protocol
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Adds sap/http/auth_plugin.py: the request/response shape and the run_plugin() driver for kubectl-style external authentication helpers. This is step 1 of the auth-plugin track in features/auth_plugins.md and is independent of CLI wiring and the response cache - those land in follow-up commits.
Why a kubectl-style plugin (vs. building auth methods in-tree): sapcli must support SAML2/SSO and Windows-cert SSO without taking on browser-automation or Win32-API dependencies. Spawning an external command and exchanging JSON over stdin/stdout keeps sapcli's dependency surface minimal and lets each plugin be implemented in whatever language fits the platform (e.g. playwright on Windows).
Design decisions:
subprocess.run with check=False and no timeout. Browser-based plugins legitimately wait minutes for the user to log in; a default timeout would kill them mid-flow. check=False so we surface our own AuthPluginError with stdout+stderr included instead of a bare CalledProcessError.
AuthPluginError derives from SAPCliError so the CLI entry point prints a friendly message instead of a stack trace, matching every other user-visible error in the project.
OSError catches both FileNotFoundError (plugin not on PATH) and PermissionError (plugin not executable); wrapping at OSError keeps the failure path uniform without enumerating each subclass.
Response validation happens here for the envelope (message, content, optional expiration) but does NOT inspect content's internal shape - that is the session-initializer's job, which dispatches on content.type. Keeps this module's contract narrow.
Expiration normalises a trailing 'Z' to '+00:00' before datetime.fromisoformat. Python 3.10's parser does not understand the Z suffix; doing it here lets the rest of the codebase stay portable.
ConnectionInfo carries port as str (matching the wire format in the spec, e.g. "44300") rather than int. The CLI parses port as int, so the caller is responsible for coercion - we do not silently mutate user input.
ConnectionInfo.sysnr is Optional[str] (default None). HTTP-only plugins have no use for the SAP system / instance number; making it required would force every caller to pass fake data. RFC-aware plugins (which need it to derive the gateway port 33) set it explicitly. The field is always emitted in to_dict (as null when unset) so plugin authors see a stable wire shape.
parameters defaults to {} when None is passed, so callers do not have to special-case "no plugin parameters" before calling.