Skip to content

feat(keycloak authentication) #4162

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

Open
wants to merge 72 commits into
base: master
Choose a base branch
from
Open
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
49f1254
chore: remove google auth verification
ChrOertlin Jan 28, 2025
b8d6103
Merge branch 'master' into feat-keycloak
ChrOertlin Jan 28, 2025
5d348f7
feat: add python-keycloak dependency
ChrOertlin Jan 28, 2025
d6c2eeb
Add keycloak client
ahdamin Jan 28, 2025
2ae86a3
chore: test
ChrOertlin Jan 28, 2025
4f617a4
feat: implement auth service
ChrOertlin Feb 6, 2025
ac275da
feat: fix before request
ChrOertlin Feb 6, 2025
b2f3397
chore: add more user util
ChrOertlin Feb 6, 2025
b613bed
refactor: create user service for user operations
ChrOertlin Feb 6, 2025
43f4a12
feat: register user service in flask app
ChrOertlin Feb 6, 2025
fddb478
revert: introduction of authenticated user model
ChrOertlin Feb 6, 2025
9253efd
feat: redirect login button to new flow
ChrOertlin Feb 6, 2025
96cea98
chore: add redirect to appconfig
ChrOertlin Feb 6, 2025
3cffc98
fix: unused import
ChrOertlin Feb 6, 2025
6f8404b
feat: working prototype
ChrOertlin Feb 17, 2025
86b6466
chore: merge master
ChrOertlin Feb 17, 2025
90eaf4b
chore: test pass
ChrOertlin Feb 17, 2025
8f4d75a
chore: remove google from invoices
ChrOertlin Feb 17, 2025
202ca6d
chore: various improvements
ChrOertlin Feb 17, 2025
d702276
chore: linting
ChrOertlin Feb 17, 2025
5527ea0
feat: init from config
ChrOertlin Feb 18, 2025
d352cc6
Merge branch 'master' into feat-keycloak
ChrOertlin Feb 18, 2025
8de7bb5
chore: merge
ChrOertlin Feb 18, 2025
bbb6cbd
Merge branch 'master' into feat-keycloak
ChrOertlin Feb 19, 2025
cd0a2ba
Merge branch 'feat-keycloak' of https://github.com/Clinical-Genomics/…
ChrOertlin Feb 20, 2025
4d6995f
chore:changes
ChrOertlin Feb 20, 2025
589ea5d
feat: add user role check
ChrOertlin Feb 24, 2025
729cde3
feat: fix admin view
ChrOertlin Feb 24, 2025
0316ff4
chore: linitng
ChrOertlin Feb 24, 2025
5dbb483
logging
ChrOertlin Feb 25, 2025
924fd4c
chore: extract client from auth service
ChrOertlin Feb 25, 2025
1eee249
fix: auth init
ChrOertlin Feb 25, 2025
39f3f5d
Merge branch 'master' into feat-keycloak
ChrOertlin Feb 28, 2025
6b4233d
chore: simplify flow
ChrOertlin Mar 3, 2025
c1e6ef4
rm: token refresh
ChrOertlin Mar 3, 2025
9673d22
chore: cleanup flows
ChrOertlin Mar 3, 2025
afea95a
fix: access
ChrOertlin Mar 3, 2025
4f4520f
chore: improve error handling auth
ChrOertlin Mar 5, 2025
031e4ba
chore: error handling
ChrOertlin Mar 5, 2025
54a94fb
Merge branch 'master' into feat-keycloak
ChrOertlin Mar 5, 2025
ad0b484
chore: linting
ChrOertlin Mar 5, 2025
22ee0ae
Merge branch 'feat-keycloak' of https://github.com/Clinical-Genomics/…
ChrOertlin Mar 5, 2025
a7af4f8
fix: proper token access
ChrOertlin Mar 5, 2025
aeddad6
fix: cache user roles for access
ChrOertlin Mar 6, 2025
848f245
chore: linting
ChrOertlin Mar 6, 2025
49c3b82
fix: various fixes
ChrOertlin Mar 6, 2025
689d54a
fix
ChrOertlin Mar 6, 2025
d04a279
chore: register client and client config in cg config
ChrOertlin Mar 7, 2025
16763b5
chore: add proper instantiation
ChrOertlin Mar 10, 2025
e3c3258
add: general client tests
ChrOertlin Mar 10, 2025
f188269
chore: add tests
ChrOertlin Mar 10, 2025
3b539d8
chore: linting
ChrOertlin Mar 10, 2025
c671af8
Merge branch 'master' into feat-keycloak
ChrOertlin Mar 10, 2025
029b14f
Merge branch 'master' into feat-keycloak
islean May 7, 2025
2be972c
fix: potential XSS with markupsafe.Markup
ahdamin May 7, 2025
81f771f
fix: syntax and black formatting
ahdamin May 7, 2025
f94876c
fix: view_case_sample_link issue
ahdamin May 7, 2025
9d852e3
fix: potential XSS issue
ahdamin May 7, 2025
bab7189
fix: potential XSS issue
ahdamin May 7, 2025
3d5b93b
fix: potential XSS issue with Markup
ahdamin May 7, 2025
1945ad1
fix: potential XSS issue with Markup
ahdamin May 7, 2025
d157ead
fix: potential XSS issue with Markup
ahdamin May 7, 2025
8a40328
fix: potential XSS issue with Markup
ahdamin May 7, 2025
a0f633d
fix: potential XSS issue with Markup
ahdamin May 7, 2025
b7ae920
fix: potential XSS issue with Markup
ahdamin May 7, 2025
eec1acf
fix: potential XSS issue with Markup
ahdamin May 7, 2025
61594b6
fix: potential XSS issue with Markup
ahdamin May 7, 2025
5a0ed30
fix: potential XSS issue with Markup
ahdamin May 7, 2025
f2f9754
fix: potential XSS issue with Markup
ahdamin May 7, 2025
12604ca
fix: potential XSS issue with Markup
ahdamin May 7, 2025
a44cc1c
fix: potential XSS issue with Markup
ahdamin May 7, 2025
7277719
fix: potential XSS issue with Markup
ahdamin May 7, 2025
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
27 changes: 14 additions & 13 deletions cg/apps/tb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
import datetime
import logging
from typing import Any

from google.auth.transport.requests import Request
from google.oauth2 import service_account

from cg.apps.tb.dto.create_job_request import CreateJobRequest
from cg.apps.tb.dto.summary_response import AnalysisSummary, SummariesResponse
from cg.apps.tb.models import AnalysesResponse, TrailblazerAnalysis
from cg.clients.authentication import keycloak_client
from cg.constants import Workflow
from cg.constants.constants import APIMethods, FileFormat, JobType, WorkflowManager
from cg.constants.priority import TrailblazerPriority
Expand All @@ -20,7 +17,8 @@
TrailblazerAPIHTTPError,
)
from cg.io.controller import APIRequest, ReadStream

from cg.services.authentication.models import TokenResponseModel
from cg.clients.authentication.keycloak_client import KeycloakClient

LOG = logging.getLogger(__name__)

Expand All @@ -43,19 +41,22 @@ class TrailblazerAPI:
AnalysisStatus.QC,
]

def __init__(self, config: dict):
self.service_account = config["trailblazer"]["service_account"]
self.service_account_auth_file = config["trailblazer"]["service_account_auth_file"]
def __init__(self, config: dict, keycloak_client: KeycloakClient):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this part needs to be tested on hasta-stage with the cli commands.

self.keycloak_client: KeycloakClient = keycloak_client
self.keycloak_backend_user = config["trailblazer"]["keycloak_backend_user"]
self.keycloak_backend_user_password = config["trailblazer"][
Comment on lines +45 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

We should create a confidential client with a client secret. This should allow us to circumvent using username+password
Screenshot 2025-05-13 at 14 15 28
rd

"keycloak_backend_user_password"
]
self.host = config["trailblazer"]["host"]

@property
def auth_header(self) -> dict:
credentials = service_account.IDTokenCredentials.from_service_account_file(
filename=self.service_account_auth_file,
target_audience="trailblazer",
token = TokenResponseModel(
**self.keycloak_client.get_token_by_user_password(
user_name=self.keycloak_backend_user, password=self.keycloak_backend_user_password
)
)
credentials.refresh(Request())
return {"Authorization": f"Bearer {credentials.token}"}
return {"Authorization": f"Bearer {token.access_token}"}

def query_trailblazer(
self, command: str, request_body: dict, method: str = APIMethods.POST
Expand Down
73 changes: 73 additions & 0 deletions cg/clients/authentication/keycloak_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from keycloak import KeycloakOpenID
from keycloak import KeycloakConnectionError


class KeycloakClient:
def __init__(self, server_url, client_id, realm_name, client_secret_key, redirect_uri):
self.server_url = server_url
self.client_id = client_id
self.realm_name = realm_name
self.client_secret_key = client_secret_key
self.redirect_uri = redirect_uri
self._client_instance: KeycloakOpenID | None = None

def get_client(self) -> KeycloakOpenID:
if self._client_instance is None:
try:
self._client_instance = KeycloakOpenID(
server_url=self.server_url,
client_id=self.client_id,
realm_name=self.realm_name,
client_secret_key=self.client_secret_key,
)
except KeycloakConnectionError as error:
raise KeycloakConnectionError(f"Failed to connect to Keycloak: {error}")
except Exception as error:
raise Exception(f"An error occurred while creating Keycloak client: {error}")
return self._client_instance

def get_auth_url(self, scope: str = "openid profile email") -> str:
"""Get the authorization URL for user login."""
client = self.get_client()
return client.auth_url(redirect_uri=self.redirect_uri, scope=scope)

def logout_user(self, refresh_token: str) -> None:
"""
Logout a user.
Args:
refresh_token: the refresh token stored in the session
"""
client: KeycloakOpenID = self.get_client()
client.logout(refresh_token)

def get_token_by_authorisation_code(self, code: str) -> dict:
"""
Get a token using the authorisation code.
Args:
code: code retrieved request
"""
client: KeycloakOpenID = self.get_client()
return client.token(
grant_type="authorization_code", code=code, redirect_uri=self.redirect_uri
)

def get_token_by_user_password(self, user_name: str, password: str) -> dict:
"""
Get a token using a username and password.
Args:
code: code retrieved request
"""
client: KeycloakOpenID = self.get_client()
return client.token(grant_type="password", username=user_name, password=pa)

def get_user_info(self, access_token: str) -> dict:
"""Get the user info for a provided access token.

Args:
access_token: access token given by keycloak

Returns:
dict: with the user information
"""
client: KeycloakOpenID = self.get_client()
return client.userinfo(access_token)
32 changes: 29 additions & 3 deletions cg/models/cg_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from cg.apps.scout.scoutapi import ScoutAPI
from cg.apps.tb import TrailblazerAPI
from cg.clients.arnold.api import ArnoldAPIClient
from cg.clients.authentication.keycloak_client import KeycloakClient
from cg.clients.chanjo2.client import Chanjo2APIClient
from cg.clients.janus.api import JanusAPIClient
from cg.constants.observations import LoqusdbInstance
Expand Down Expand Up @@ -125,8 +126,8 @@ class ClientConfig(BaseModel):


class TrailblazerConfig(BaseModel):
service_account: str
service_account_auth_file: str
keycloak_backend_user: str
keycloak_backend_user_password: str
Comment on lines +129 to +130
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove and use client_secret_key instead

host: str


Expand Down Expand Up @@ -387,6 +388,14 @@ class PostProcessingServices(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)


class KeyCloakConfig(BaseModel):
server_url: str
client_id: str
realm_name: str
client_secret_key: str
redirect_uri: str


class CGConfig(BaseModel):
data_input: DataInput | None = None
database: str
Expand Down Expand Up @@ -456,6 +465,8 @@ class CGConfig(BaseModel):
tar: CommonAppConfig | None = None
trailblazer: TrailblazerConfig = None
trailblazer_api_: TrailblazerAPI = None
keycloak: KeyCloakConfig = None
keycloak_client_: KeycloakClient | None = None

# Meta APIs that will use the apps from CGConfig
balsamic: BalsamicConfig | None = None
Expand Down Expand Up @@ -737,7 +748,7 @@ def trailblazer_api(self) -> TrailblazerAPI:
api = self.__dict__.get("trailblazer_api_")
if api is None:
LOG.debug("Instantiating trailblazer api")
api = TrailblazerAPI(config=self.dict())
api = TrailblazerAPI(config=self.dict(), keycloak_client=self.keycloak_client)
self.trailblazer_api_ = api
return api

Expand Down Expand Up @@ -792,3 +803,18 @@ def delivery_service_factory(self) -> DeliveryServiceFactory:
)
self.delivery_service_factory_ = factory
return factory

@property
def keycloak_client(self) -> KeycloakClient:
client = self.keycloak_client_
if client is None:
LOG.debug("Instantiating keycloak client")
client = KeycloakClient(
server_url=self.keycloak.server_url,
client_id=self.keycloak.client_id,
client_secret_key=self.keycloak.client_secret_key,
realm_name=self.keycloak.realm_name,
redirect_uri=self.keycloak.redirect_uri,
)
self.keycloak_client_ = client
return client
Loading
Loading