Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding oauthn #690

Merged
merged 72 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
e810563
added oauth
ZohebShaikh Nov 11, 2024
983ea61
Test refactoring
DiamondJoseph Nov 11, 2024
5ded18a
Linting
DiamondJoseph Nov 11, 2024
e6b396c
Move RMQ launch into docker-compose
DiamondJoseph Nov 11, 2024
0d20a46
More changes
DiamondJoseph Nov 11, 2024
365b59e
Refactor tests to not require mocking jwt_decode
DiamondJoseph Nov 11, 2024
f23a828
Refactor dependencies
DiamondJoseph Nov 11, 2024
a7e0515
Consistency in names
DiamondJoseph Nov 11, 2024
c99aa5c
Test changes for mocks
DiamondJoseph Nov 11, 2024
5f5b355
Revert unrelated changes
DiamondJoseph Nov 11, 2024
82f1ddd
Remove vestigial decoupling
DiamondJoseph Nov 11, 2024
a6185a5
Revert unrelated changes
DiamondJoseph Nov 11, 2024
0b270d4
Re-use fixtures
DiamondJoseph Nov 11, 2024
3b06ea0
Refresh dev-requirements
DiamondJoseph Nov 11, 2024
2c8e614
Check if access_token is jwt first
DiamondJoseph Nov 11, 2024
69af289
Revert accidental change
DiamondJoseph Nov 11, 2024
13ca9b5
lint
DiamondJoseph Nov 11, 2024
1f2b340
Last test
DiamondJoseph Nov 11, 2024
def1ccc
Linting
DiamondJoseph Nov 11, 2024
355b055
whitespace changes
DiamondJoseph Nov 11, 2024
bf6000a
Rename fixtures and config
DiamondJoseph Nov 12, 2024
56a5485
Remove useless default for CLIClientConfig
DiamondJoseph Nov 12, 2024
3f9555e
token_path less verbose
DiamondJoseph Nov 12, 2024
aef1911
Revert unrelated main changes
DiamondJoseph Nov 12, 2024
f875afe
Revert server handling changes
DiamondJoseph Nov 12, 2024
84c9fe0
Remove unused endpoint
DiamondJoseph Nov 12, 2024
3e0f247
Revert unrelated change
DiamondJoseph Nov 12, 2024
34457b9
Ensure logout flow and add required(?) headers
DiamondJoseph Nov 12, 2024
6241a23
Handle exceptions
DiamondJoseph Nov 12, 2024
a43ea73
Remove system test config until container provided
DiamondJoseph Nov 12, 2024
7da35dc
Refactored requests to use form-encoded data instead of JSON payloads…
ZohebShaikh Nov 13, 2024
492fe19
added tests for invalid token
ZohebShaikh Nov 13, 2024
1243807
Merge branch 'main' into adding-oauthn
ZohebShaikh Nov 14, 2024
b31c275
Merge remote-tracking branch 'origin/main' into adding-oauthn
ZohebShaikh Nov 18, 2024
d7cb5e8
Merge branch 'main' into adding-oauthn
DiamondJoseph Nov 18, 2024
a3894a7
Prevent issues with using client_audiences as span attributes
DiamondJoseph Nov 18, 2024
f200e93
added middleware for oauthn
ZohebShaikh Nov 19, 2024
b69d394
using sorted list to compare plans
ZohebShaikh Nov 21, 2024
0638a9c
updated scopes for requests made for login&logout
ZohebShaikh Nov 21, 2024
0a93d16
added sort for devices
ZohebShaikh Nov 21, 2024
2a92349
Merge remote-tracking branch 'origin/main' into adding-oauthn
ZohebShaikh Nov 21, 2024
ba3ff6e
added bound for result and post merge changes
ZohebShaikh Nov 21, 2024
e649c2d
added match for response
ZohebShaikh Nov 21, 2024
d717829
added test for server checking of access token
ZohebShaikh Nov 21, 2024
9d116b1
added test for 401 unauthenticated response
ZohebShaikh Nov 21, 2024
2e365e0
added tests for main server auth
ZohebShaikh Nov 21, 2024
66eee00
made changes to the conftest
ZohebShaikh Nov 21, 2024
c015582
added design changes
ZohebShaikh Nov 25, 2024
721fa22
saving access token
ZohebShaikh Nov 26, 2024
cd69900
updated flow
ZohebShaikh Nov 26, 2024
638a0cc
simplified getting valid access token
ZohebShaikh Nov 26, 2024
f961e51
updated working tests
ZohebShaikh Nov 27, 2024
4904ee2
added unit tests for authentication
ZohebShaikh Nov 27, 2024
8166c4d
removed pytest skip for tests
ZohebShaikh Nov 27, 2024
ea471d8
added tests for oauth
ZohebShaikh Nov 27, 2024
27c12ce
simplified tests
ZohebShaikh Nov 27, 2024
d7e32b2
added tests for coverage
ZohebShaikh Nov 28, 2024
fa30faa
Merge branch 'main' into adding-oauthn
ZohebShaikh Nov 28, 2024
4178ccd
added detail for 401
ZohebShaikh Nov 28, 2024
c3899b3
updated system test comment
ZohebShaikh Nov 28, 2024
889730b
updated test
ZohebShaikh Nov 28, 2024
841adcb
added code review changes
ZohebShaikh Nov 28, 2024
9037ce2
added browser to open the login page
ZohebShaikh Nov 28, 2024
45bf6c2
changed name to blueapi
ZohebShaikh Nov 28, 2024
07c9686
deleted expand user check
ZohebShaikh Nov 29, 2024
bb52bdb
added code review changes
ZohebShaikh Nov 29, 2024
6fc6b73
updated test
ZohebShaikh Nov 29, 2024
e7e9620
Merge remote-tracking branch 'origin/main' into adding-oauthn
ZohebShaikh Dec 2, 2024
48c3c34
added minor changes and increased coverage
ZohebShaikh Dec 2, 2024
7bd63af
added open_new_tab literal
ZohebShaikh Dec 3, 2024
72cbcf5
changed from Blueapi to blueapi
ZohebShaikh Dec 3, 2024
a614684
updated system tests
ZohebShaikh Dec 3, 2024
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
5 changes: 5 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ boltons==24.1.0
cachetools==5.5.0
caproto==1.1.1
certifi==2024.8.30
cffi==1.17.1
cfgv==3.4.0
charset-normalizer==3.4.0
click==8.1.7
Expand All @@ -35,6 +36,7 @@ confluent-kafka==2.6.0
contourpy==1.3.1
copier==9.4.1
coverage==7.6.4
cryptography==43.0.3
cycler==0.12.1
dask==2024.11.2
databroker==1.2.5
Expand Down Expand Up @@ -95,6 +97,7 @@ jinja2-ansible-filters==1.3.2
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
jupyterlab_widgets==3.0.13
jwcrypto==1.5.6
kiwisolver==1.4.7
ldap3==2.9.1
locket==1.0.0
Expand Down Expand Up @@ -170,6 +173,7 @@ pure_eval==0.2.3
pvxslibs==1.3.2
py==1.11.0
pyasn1==0.6.1
pycparser==2.22
pycryptodome==3.21.0
pydantic==2.9.2
pydantic-extra-types==2.10.0
Expand All @@ -179,6 +183,7 @@ pydantic_numpy==5.0.2
pydata-sphinx-theme==0.16.0
pyepics==3.5.7
Pygments==2.18.0
PyJWT==2.9.0
pymongo==4.10.1
pyOlog==4.5.0
pyparsing==3.2.0
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies = [
"opentelemetry-distro>=0.48b0",
"opentelemetry-instrumentation-fastapi>=0.48b0",
"observability-utils>=0.1.4",
"pyjwt[crypto]",
]
dynamic = ["version"]
license.file = "LICENSE"
Expand Down Expand Up @@ -66,6 +67,7 @@ dev = [
"types-requests",
"types-urllib3",
"mock",
"jwcrypto",
]

[project.scripts]
Expand Down
43 changes: 41 additions & 2 deletions src/blueapi/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
from blueapi.client.client import BlueapiClient
from blueapi.client.event_bus import AnyEvent, BlueskyStreamingError, EventBusClient
from blueapi.client.rest import BlueskyRemoteControlError
from blueapi.config import ApplicationConfig, ConfigLoader
from blueapi.config import (
ApplicationConfig,
ConfigLoader,
)
from blueapi.core import OTLP_EXPORT_ENABLED, DataEvent
from blueapi.service.authentication import SessionCacheManager, SessionManager
from blueapi.worker import ProgressEvent, Task, WorkerEvent

from .scratch import setup_scratch
Expand Down Expand Up @@ -134,7 +138,12 @@ def wrapper(*args, **kwargs):
try:
func(*args, **kwargs)
except ConnectionError:
print("Failed to establish connection to FastAPI server.")
print("Failed to establish connection to blueapi server.")
except BlueskyRemoteControlError as e:
if str(e) == "<Response [401]>":
print("Access denied. Please check your login status and try again.")
else:
raise e

return wrapper

Expand Down Expand Up @@ -343,3 +352,33 @@ def scratch(obj: dict) -> None:
setup_scratch(config.scratch)
else:
raise KeyError("No scratch config supplied")


@main.command(name="login")
@check_connection
@click.pass_obj
def login(obj: dict) -> None:
config: ApplicationConfig = obj["config"]
try:
auth: SessionManager = SessionManager.from_cache(config.auth_token_path)
access_token = auth.get_valid_access_token()
assert access_token
print("Logged in")
except Exception:
client = BlueapiClient.from_config(config)
oidc_config = client.get_oidc_config()
auth = SessionManager(
oidc_config, cache_manager=SessionCacheManager(config.auth_token_path)
)
auth.start_device_flow()


@main.command(name="logout")
@click.pass_obj
def logout(obj: dict) -> None:
config: ApplicationConfig = obj["config"]
try:
auth: SessionManager = SessionManager.from_cache(config.auth_token_path)
auth.logout()
except FileNotFoundError:
print("Logged out")
20 changes: 19 additions & 1 deletion src/blueapi/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

from blueapi.config import ApplicationConfig
from blueapi.core.bluesky_types import DataEvent
from blueapi.service.authentication import SessionManager
from blueapi.service.model import (
DeviceModel,
DeviceResponse,
EnvironmentResponse,
OIDCConfig,
PlanModel,
PlanResponse,
TaskResponse,
Expand Down Expand Up @@ -45,7 +47,12 @@ def __init__(

@classmethod
def from_config(cls, config: ApplicationConfig) -> "BlueapiClient":
rest = BlueapiRestClient(config.api)
session_manager: SessionManager | None = None
try:
session_manager = SessionManager.from_cache(config.auth_token_path)
except Exception:
... # Swallow exceptions
rest = BlueapiRestClient(config.api, session_manager=session_manager)
if config.stomp is None:
return cls(rest)
client = StompClient.for_broker(
Expand Down Expand Up @@ -416,3 +423,14 @@ def _wait_for_reload(
f"Failed to reload the environment within {timeout} "
"seconds, a server restart is recommended"
)

@start_as_current_span(TRACER)
def get_oidc_config(self) -> OIDCConfig:
"""
Get oidc config from the server

Returns:
OIDCConfig: Details of the oidc Config
"""

return self._rest.get_oidc_config()
25 changes: 22 additions & 3 deletions src/blueapi/client/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
from pydantic import TypeAdapter

from blueapi.config import RestConfig
from blueapi.service.authentication import JWTAuth, SessionManager
from blueapi.service.model import (
DeviceModel,
DeviceResponse,
EnvironmentResponse,
OIDCConfig,
PlanModel,
PlanResponse,
TaskResponse,
Expand Down Expand Up @@ -45,8 +47,13 @@ def _exception(response: requests.Response) -> Exception | None:
class BlueapiRestClient:
_config: RestConfig

def __init__(self, config: RestConfig | None = None) -> None:
def __init__(
self,
config: RestConfig | None = None,
session_manager: SessionManager | None = None,
) -> None:
self._config = config or RestConfig()
self._session_manager = session_manager

def get_plans(self) -> PlanResponse:
return self._request_and_deserialize("/plans", PlanResponse)
Expand Down Expand Up @@ -125,6 +132,9 @@ def delete_environment(self) -> EnvironmentResponse:
"/environment", EnvironmentResponse, method="DELETE"
)

def get_oidc_config(self) -> OIDCConfig:
return self._request_and_deserialize("/config/oidc", OIDCConfig)

@start_as_current_span(TRACER, "method", "data", "suffix")
def _request_and_deserialize(
self,
Expand All @@ -137,10 +147,19 @@ def _request_and_deserialize(
url = self._url(suffix)
# Get the trace context to propagate to the REST API
carr = get_context_propagator()

if data:
response = requests.request(method, url, json=data, headers=carr)
response = requests.request(
method,
url,
json=data,
headers=carr,
auth=JWTAuth(self._session_manager),
)
else:
response = requests.request(method, url, headers=carr)
response = requests.request(
method, url, headers=carr, auth=JWTAuth(self._session_manager)
)
exception = get_exception(response)
if exception is not None:
raise exception
Expand Down
60 changes: 58 additions & 2 deletions src/blueapi/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
from collections.abc import Mapping
from enum import Enum
from functools import cached_property
from pathlib import Path
from typing import Any, Generic, Literal, TypeVar
from typing import Any, Generic, Literal, TypeVar, cast

import requests
import yaml
from bluesky_stomp.models import BasicAuthentication
from pydantic import BaseModel, Field, TypeAdapter, ValidationError
from pydantic import (
BaseModel,
Field,
TypeAdapter,
ValidationError,
)

from blueapi.utils import BlueapiBaseModel, InvalidConfigError

Expand Down Expand Up @@ -77,6 +84,53 @@ class ScratchConfig(BlueapiBaseModel):
repositories: list[ScratchRepository] = Field(default_factory=list)


class OIDCConfig(BlueapiBaseModel):
well_known_url: str = Field(
description="URL to fetch OIDC config from the provider"
)
client_id: str = Field(description="Client ID")
client_audience: str = Field(description="Client Audience(s)", default="blueapi")

@cached_property
def _config_from_oidc_url(self) -> dict[str, Any]:
response: requests.Response = requests.get(self.well_known_url)
response.raise_for_status()
return response.json()

@cached_property
def device_authorization_endpoint(self) -> str:
return cast(
str, self._config_from_oidc_url.get("device_authorization_endpoint")
)

@cached_property
def token_endpoint(self) -> str:
return cast(str, self._config_from_oidc_url.get("token_endpoint"))

@cached_property
def issuer(self) -> str:
return cast(str, self._config_from_oidc_url.get("issuer"))

@cached_property
def authorization_endpoint(self) -> str:
return cast(str, self._config_from_oidc_url.get("authorization_endpoint"))

@cached_property
def jwks_uri(self) -> str:
return cast(str, self._config_from_oidc_url.get("jwks_uri"))

@cached_property
def end_session_endpoint(self) -> str:
return cast(str, self._config_from_oidc_url.get("end_session_endpoint"))

@cached_property
def id_token_signing_alg_values_supported(self) -> list[str]:
return cast(
list[str],
self._config_from_oidc_url.get("id_token_signing_alg_values_supported"),
)


class ApplicationConfig(BlueapiBaseModel):
"""
Config for the worker application as a whole. Root of
Expand All @@ -88,6 +142,8 @@ class ApplicationConfig(BlueapiBaseModel):
logging: LoggingConfig = Field(default_factory=LoggingConfig)
api: RestConfig = Field(default_factory=RestConfig)
scratch: ScratchConfig | None = None
oidc: OIDCConfig | None = None
auth_token_path: Path | None = None

def __eq__(self, other: object) -> bool:
if isinstance(other, ApplicationConfig):
Expand Down
3 changes: 2 additions & 1 deletion src/blueapi/service/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .authentication import SessionManager
from .model import DeviceModel, PlanModel

__all__ = ["PlanModel", "DeviceModel"]
__all__ = ["PlanModel", "DeviceModel", "SessionManager"]
Loading
Loading