Skip to content

Commit

Permalink
Merge pull request #42 from canvas-medical/michela/auth-scope
Browse files Browse the repository at this point in the history
Add plugin scope to oauth token request. and tests
  • Loading branch information
aduane authored May 14, 2024
2 parents 8e8b635 + f0243bb commit bec6c7c
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 94 deletions.
223 changes: 142 additions & 81 deletions canvas_cli/apps/auth/tests.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,142 @@
# def test_get_api_token_without_existing_host_or_client_credentials_raises_exception() -> None:
# """Test getting an api token with no default host or client credentials."""

# runner.invoke(app, "auth remove-api-client-credentials http://george.com")

# result_without_host = runner.invoke(app, "auth get-api-token")
# assert result_without_host.exit_code == 2
# assert (
# "Invalid value: Please specify a host or set a default via the `auth` command"
# in result_without_host.stdout
# )

# result_without_client_id = runner.invoke(app, "auth get-api-token --host http://george.com")
# assert result_without_client_id.exit_code == 2
# print(result_without_client_id.stdout)
# assert (
# "Invalid value: Please specify a client_id and client_secret or add them via"
# in result_without_client_id.stdout
# )

# result_without_client_secret = runner.invoke(
# app, "auth get-api-token --host http://george.com --client-id mock-client-id"
# )
# assert result_without_client_secret.exit_code == 2
# assert (
# "Invalid value: Please specify a client_id and client_secret or add them via"
# in result_without_client_secret.stdout
# )


# @patch("requests.post")
# def test_get_api_token_requests_token_from_the_host_if_not_stored_in_context(
# mock_post: MagicMock,
# ) -> None:
# class FakeResponse:
# status_code = 200

# def json(self) -> dict:
# return {"access_token": "a-valid-api-token", "expires_in": 3600}

# mock_post.return_value = FakeResponse()

# result = runner.invoke(
# app,
# "auth get-api-token --host http://george.com --client-id mock-client-id --client-secret mock-client-secret",
# )
# mock_post.assert_called_once()
# assert result.exit_code == 0
# assert '{"success": true, "token": "a-valid-api-token"}' in result.stdout
# assert context.token_expiration_date is not None
# assert datetime.fromisoformat(context.token_expiration_date) > datetime.now()


# @patch("keyring.get_password")
# @patch("requests.post")
# def test_get_api_token_uses_token_stored_in_context_first(
# mock_post: MagicMock,
# mock_get_password: MagicMock,
# ) -> None:
# mock_get_password.return_value = "a-valid-api-token"
# result = runner.invoke(
# app,
# "auth get-api-token --host http://george.com --client-id mock-client-id --client-secret mock-client-secret",
# )
# assert result.exit_code == 0
# mock_get_password.assert_called_once_with(
# "canvas_cli.apps.auth.utils", "http://george.com|token"
# )
# mock_post.assert_not_called()


# def test_get_api_token_uses_credentials_stored_in_context() -> None:
# runner.invoke(
# app,
# "auth add-api-client-credentials --host http://george.com --client-id mock-client-id --client-secret mock-client-secret --is-default",
# )
# assert context.default_host == "http://george.com"

# result = runner.invoke(app, "auth get-api-token")
# assert result.exit_code == 0
# assert '{"success": true, "token": "a-valid-api-token"}' in result.stdout
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from canvas_cli.apps.auth import get_or_request_api_token


@pytest.fixture
def valid_token_response() -> Any:
class TokenResponse:
status_code = 200

def json(self) -> dict:
return {"access_token": "a-valid-api-token", "expires_in": 3600}

return TokenResponse()


@pytest.fixture
def error_token_response() -> Any:
class TokenResponse:
status_code = 500

return TokenResponse()


@pytest.fixture
def expired_token_response() -> Any:
class TokenResponse:
status_code = 200

def json(self) -> dict:
return {"access_token": "a-valid-api-token", "expires_in": -1}

return TokenResponse()


@patch("keyring.get_password")
@patch("requests.Session.post")
@patch("canvas_cli.apps.auth.utils.is_token_valid")
def test_get_or_request_api_token_uses_stored_token(
mock_is_token_valid: MagicMock,
mock_post: MagicMock,
mock_get_password: MagicMock,
valid_token_response: Any,
) -> None:
mock_is_token_valid.return_value = True
mock_get_password.return_value = "a-stored-valid-token"
mock_post.return_value = valid_token_response

token = get_or_request_api_token("http://localhost:8000")

assert token == "a-stored-valid-token"
mock_post.assert_not_called()


@patch("keyring.set_password")
@patch("keyring.get_password")
@patch("requests.Session.post")
@patch("canvas_cli.apps.auth.utils.get_api_client_credentials")
def test_get_or_request_api_token_requests_token_if_none_stored(
mock_client_credentials: MagicMock,
mock_post: MagicMock,
mock_get_password: MagicMock,
mock_set_password: MagicMock,
valid_token_response: Any,
) -> None:
mock_client_credentials.return_value = "client_id=id&client_secret=secret"
mock_get_password.return_value = None
mock_post.return_value = valid_token_response

token = get_or_request_api_token("http://localhost:8000")

assert token == "a-valid-api-token"
mock_post.assert_called_once_with(
"http://localhost:8000/auth/token/",
headers={"Content-Type": "application/x-www-form-urlencoded"},
json=None,
data="grant_type=client_credentials&scope=system/Plugins.*&client_id=id&client_secret=secret",
)
mock_set_password.assert_called_with(
"canvas_cli.apps.auth.utils",
username="http://localhost:8000|token",
password="a-valid-api-token",
)


@patch("keyring.get_password")
@patch("requests.Session.post")
@patch("canvas_cli.apps.auth.utils.get_api_client_credentials")
def test_get_or_request_api_token_raises_exception_if_error_token_response(
mock_client_credentials: MagicMock,
mock_post: MagicMock,
mock_get_password: MagicMock,
error_token_response: Any,
) -> None:
mock_client_credentials.return_value = "client_id=id&client_secret=secret"
mock_get_password.return_value = None
mock_post.return_value = error_token_response

with pytest.raises(Exception) as e:
get_or_request_api_token("http://localhost:8000")

assert "Unable to get a valid access token from the given host 'http://localhost:8000'" in repr(
e
)

mock_post.assert_called_once_with(
"http://localhost:8000/auth/token/",
headers={"Content-Type": "application/x-www-form-urlencoded"},
json=None,
data="grant_type=client_credentials&scope=system/Plugins.*&client_id=id&client_secret=secret",
)


@patch("keyring.get_password")
@patch("requests.Session.post")
@patch("canvas_cli.apps.auth.utils.get_api_client_credentials")
def test_get_or_request_api_token_raises_exception_if_expired_token(
mock_client_credentials: MagicMock,
mock_post: MagicMock,
mock_get_password: MagicMock,
expired_token_response: Any,
) -> None:
mock_client_credentials.return_value = "client_id=id&client_secret=secret"
mock_get_password.return_value = None
mock_post.return_value = expired_token_response

with pytest.raises(Exception) as e:
get_or_request_api_token("http://localhost:8000")

assert (
"A valid token could not be acquired from the given host 'http://localhost:8000'" in repr(e)
)

mock_post.assert_called_once_with(
"http://localhost:8000/auth/token/",
headers={"Content-Type": "application/x-www-form-urlencoded"},
json=None,
data="grant_type=client_credentials&scope=system/Plugins.*&client_id=id&client_secret=secret",
)
22 changes: 9 additions & 13 deletions canvas_cli/apps/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ def get_config() -> configparser.ConfigParser:
raise Exception(
f"""Please add your configuration file at '{CONFIG_PATH}' with the following format:
[my-canvas-instance]
[my-canvas-subdomain]
client_id=myclientid
client_secret=myclientsecret
[my-dev-canvas-instance]
[my-dev-canvas-subdomain]
client_id=devclientid
client_secret=devclientsecret
is_default=true
Expand Down Expand Up @@ -91,16 +91,17 @@ def get_default_host(host: str | None = None) -> str:

def request_api_token(host: str, api_client_credentials: str) -> dict:
"""Request an api token using the provided client_id and client_secret."""
grant_type = "grant_type=client_credentials"
scope = "scope=system/Plugins.*"

http = Http()
token_response = http.post(
f"{host}/auth/token/",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=f"grant_type=client_credentials&{api_client_credentials}",
data=f"{grant_type}&{scope}&{api_client_credentials}",
)
if token_response.status_code != requests.codes.ok:
raise Exception(
"Unable to get a valid access token. Please check your host, client_id, and client_secret"
)
raise Exception(f"Unable to get a valid access token from the given host '{host}'")
return token_response.json()


Expand Down Expand Up @@ -130,22 +131,17 @@ def get_or_request_api_token(host: str | None = None) -> str:

host_token_key = f"{host}|token"
token = get_password(host_token_key)

if token and is_token_valid(host_token_key):
return token

api_client_credentials = get_api_client_credentials(host)

if not (token_response := request_api_token(host, api_client_credentials)):
raise Exception(
"A token could not be acquired from the given host, client_id, and client_secret"
)
raise Exception(f"A token could not be acquired from the given host '{host}'")

token_expiration_date = datetime.now() + timedelta(seconds=token_response["expires_in"])
if not is_token_valid(host_token_key, token_expiration_date):
raise Exception(
"A valid token could not be acquired from the given host, client_id, and client_secret"
)
raise Exception(f"A valid token could not be acquired from the given host '{host}'")

new_token = token_response["access_token"]
set_password(host_token_key, new_token)
Expand Down

0 comments on commit bec6c7c

Please sign in to comment.