feat: add oauth login auth command
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from wf_cli.context import load_cli_context_from_typer
|
||||
from wf_cli.context import config_path_from_context, load_cli_context_from_typer
|
||||
from wf_cli.formats import ListOutputFormat, emit_list_payload
|
||||
from wf_cli.io import emit_json
|
||||
from wf_cli.oauth import OAuthCodeLoginFlow, OAuthLoginResult, build_oauth_record
|
||||
from wf_cli.remote_errors import run_cli_operation
|
||||
|
||||
app = typer.Typer(
|
||||
@@ -140,3 +142,80 @@ def delete_auth_record(
|
||||
context = load_cli_context_from_typer(ctx)
|
||||
result = run_cli_operation(context, context.admin.delete_auth_record(auth_ref))
|
||||
emit_json(result)
|
||||
|
||||
|
||||
async def _login_with_pasted_response(
|
||||
*,
|
||||
provider,
|
||||
client_id: str,
|
||||
client_secret: str | None,
|
||||
authorization_response: str,
|
||||
) -> OAuthLoginResult:
|
||||
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
||||
|
||||
flow = OAuthCodeLoginFlow(client_factory=AsyncOAuth2Client) # type: ignore[arg-type]
|
||||
return await flow.login_with_authorization_response(
|
||||
provider=provider,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
authorization_response=authorization_response,
|
||||
)
|
||||
|
||||
|
||||
@app.command("oauth-login")
|
||||
def oauth_login(
|
||||
ctx: typer.Context,
|
||||
provider_name: Annotated[str, typer.Argument(help="Auth provider profile name.")],
|
||||
auth_ref: Annotated[str, typer.Option("--id", help="Auth record id/ref to save.")],
|
||||
authorization_response: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--authorization-response",
|
||||
help="Full redirected callback URL after login.",
|
||||
),
|
||||
],
|
||||
) -> None:
|
||||
"""Run an OAuth login flow and save the resulting refresh token as an auth record."""
|
||||
from wf_config import load_workflow_config
|
||||
|
||||
config_path = Path(config_path_from_context(ctx))
|
||||
config = load_workflow_config(config_path)
|
||||
provider = config.auth.providers.get(provider_name)
|
||||
if provider is None:
|
||||
raise typer.BadParameter(f"unknown auth provider {provider_name!r}")
|
||||
client_id = os.environ.get(provider.client_id_env)
|
||||
if not client_id:
|
||||
raise typer.BadParameter(f"missing env var {provider.client_id_env}")
|
||||
client_secret = (
|
||||
os.environ.get(provider.client_secret_env)
|
||||
if provider.client_secret_env is not None
|
||||
else None
|
||||
)
|
||||
result = run_cli_operation(
|
||||
load_cli_context_from_typer(ctx),
|
||||
_login_with_pasted_response(
|
||||
provider=provider,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
authorization_response=authorization_response,
|
||||
),
|
||||
)
|
||||
record = build_oauth_record(
|
||||
auth_ref=auth_ref,
|
||||
provider_name=provider_name,
|
||||
provider=provider,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
result=result,
|
||||
)
|
||||
context = load_cli_context_from_typer(ctx)
|
||||
saved = run_cli_operation(
|
||||
context,
|
||||
context.admin.save_auth_record(
|
||||
auth_ref=record.id,
|
||||
scheme="oauth_refresh_token",
|
||||
payload=record.auth.model_dump(mode="json", exclude={"kind"}),
|
||||
metadata=record.metadata,
|
||||
),
|
||||
)
|
||||
emit_json(saved)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from wf_api.auth import OAuthRefreshTokenAuth
|
||||
from wf_cli.app import app
|
||||
from wf_cli.oauth import OAuthCodeLoginFlow, OAuthLoginResult, build_oauth_record
|
||||
from wf_config import OAuthProviderConfig
|
||||
|
||||
@@ -97,3 +101,69 @@ async def test_oauth_code_login_flow_uses_injected_client() -> None:
|
||||
assert result.refresh_token == "refresh"
|
||||
assert result.scopes == ("https://www.googleapis.com/auth/drive.readonly",)
|
||||
assert client.fetch_calls == ["http://127.0.0.1/callback?code=abc&state=state-123"]
|
||||
|
||||
|
||||
def test_auth_oauth_login_saves_record_from_provider_profile(monkeypatch, tmp_path) -> None:
|
||||
saved: list[object] = []
|
||||
|
||||
class _FakeAdmin:
|
||||
async def save_auth_record(self, **kwargs: object) -> dict[str, object]:
|
||||
saved.append(kwargs)
|
||||
return {"id": kwargs["auth_ref"], "scheme": "oauth_refresh_token"}
|
||||
|
||||
class _FakeContext:
|
||||
admin = _FakeAdmin()
|
||||
|
||||
async def _fake_login(*args: object, **kwargs: object) -> OAuthLoginResult:
|
||||
return OAuthLoginResult(refresh_token="refresh")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"wf_cli.commands.auth_admin.load_cli_context_from_typer",
|
||||
lambda ctx: _FakeContext(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"wf_cli.commands.auth_admin._login_with_pasted_response",
|
||||
_fake_login,
|
||||
)
|
||||
|
||||
config_path = tmp_path / "wf.config.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"auth": {
|
||||
"providers": {
|
||||
"google": {
|
||||
"kind": "oauth_authorization_code_pkce",
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"client_id_env": "GOOGLE_OAUTH_CLIENT_ID",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive.readonly"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("GOOGLE_OAUTH_CLIENT_ID", "client")
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"admin",
|
||||
"auth",
|
||||
"oauth-login",
|
||||
"google",
|
||||
"--id",
|
||||
"google.drive.personal",
|
||||
"--authorization-response",
|
||||
"http://127.0.0.1/callback?code=abc&state=state",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert saved
|
||||
assert saved[0]["auth_ref"] == "google.drive.personal"
|
||||
assert saved[0]["scheme"] == "oauth_refresh_token"
|
||||
|
||||
Reference in New Issue
Block a user