Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion web/server/codechecker_server/database/config_db_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ class OAuthToken(Base):
__tablename__ = 'oauth_tokens'

id = Column(Integer, autoincrement=True, primary_key=True)
provider = Column(String, nullable=False)
access_token = Column(String, nullable=False)
expires_at = Column(DateTime)
refresh_token = Column(String, nullable=False)
Expand All @@ -226,8 +227,9 @@ class OAuthToken(Base):
nullable=False,
index=True)

def __init__(self, access_token, expires_at, refresh_token,
def __init__(self, provider, access_token, expires_at, refresh_token,
auth_session_id):
self.provider = provider
self.access_token = access_token
self.expires_at = expires_at
self.refresh_token = refresh_token
Expand Down
20 changes: 11 additions & 9 deletions web/server/codechecker_server/database/db_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
File, FileContent, \
Report, ReportAnalysisInfo, ReportPathDataFile, RunHistoryAnalysisInfo, \
RunLock
from .config_db_model import Session as SessionRecord
from .config_db_model import OAuthToken, Session as SessionRecord

LOG = get_logger('server')
RUN_LOCK_TIMEOUT_IN_DATABASE = 30 * 60 # 30 minutes.
Expand Down Expand Up @@ -436,15 +436,17 @@ def delete_expired_auth_sessions(config_db_sessionmaker: sessionmaker,
cutoff_date = (datetime.now() - timedelta(
seconds=session_lifetime))

# OAuth backed sessions can still be renewed silently, so
# they are left to the refresh aware cleanup instead.
has_oauth_token = session.query(OAuthToken.auth_session_id) \
.filter(OAuthToken.auth_session_id == SessionRecord.id)

query = session.query(SessionRecord) \
.filter(SessionRecord.last_access < cutoff_date) \
.filter(~has_oauth_token.exists())
if user_name:
session.query(SessionRecord) \
.filter(SessionRecord.user_name == user_name) \
.filter(SessionRecord.last_access < cutoff_date) \
.delete(synchronize_session=False)
else:
session.query(SessionRecord) \
.filter(SessionRecord.last_access < cutoff_date) \
.delete(synchronize_session=False)
query = query.filter(SessionRecord.user_name == user_name)
query.delete(synchronize_session=False)

session.commit()
LOG.debug("Finished cleanup of expired auth sessions.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Keep oauth session alive

Revision ID: 6df2e06f8cf6
Revises: 635389f535cd
Create Date: 2026-09-02 23:04:07.756806
"""

from logging import getLogger

from alembic import op
import sqlalchemy as sa

# Revision identifiers, used by Alembic.
revision = '6df2e06f8cf6'
down_revision = '635389f535cd'
branch_labels = None
depends_on = None


def upgrade():
LOG = getLogger("migration/config")
# Nullable: existing rows predate this column and have no provider.
op.add_column(
'oauth_tokens',
sa.Column('provider', sa.String(), nullable=True))


def downgrade():
LOG = getLogger("migration/config")
op.drop_column('oauth_tokens', 'provider')
120 changes: 109 additions & 11 deletions web/server/codechecker_server/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import hashlib
from typing import Optional

from authlib.integrations.base_client import OAuthError
from authlib.integrations.requests_client import OAuth2Session
from sqlalchemy.orm import sessionmaker

from codechecker_common.compatibility.multiprocessing import cpu_count
Expand Down Expand Up @@ -951,6 +953,12 @@ def create_session_oauth(self, provider: str,
not providers.get(provider).get('enabled'):
return False

# Perform cleanup of session memory, if neccessary.
self.__logins_since_prune += 1
if self.__logins_since_prune >= \
self.__auth_config['logins_until_cleanup']:
self.__cleanup_sessions()

# Generate a new token and create a local session.
codechecker_session_token = generate_session_token()

Expand Down Expand Up @@ -991,6 +999,7 @@ def create_session_oauth(self, provider: str,
.first()

oauth_token_session = OAuthToken(
provider=provider,
access_token=access_token,
expires_at=token_expires_at,
refresh_token=refresh_token,
Expand Down Expand Up @@ -1110,24 +1119,112 @@ def get_session(self, token) -> Optional[_Session]:
return None

for sess in self.__sessions:
if sess.is_alive and sess.token == token:
# If the session is alive but the should be re-validated.
if sess.is_refresh_time_expire:
sess.revalidate()
return sess
if sess.token == token:
if sess.is_alive:
# If the session is alive but the should be re-validated.
if sess.is_refresh_time_expire:
sess.revalidate()
return sess

if self.__try_extend_oauth_session(sess):
return sess
break

# Try to get a local session from the database.
local_session = self.__get_local_session_from_db(token)
if local_session and local_session.is_alive:
self.__sessions.append(local_session)
if local_session.is_refresh_time_expire:
local_session.revalidate()
return local_session
if local_session:
if local_session.is_alive:
self.__sessions.append(local_session)
if local_session.is_refresh_time_expire:
local_session.revalidate()
return local_session

if self.__try_extend_oauth_session(local_session):
self.__sessions.append(local_session)
return local_session

self.invalidate(token)

return None

def __try_extend_oauth_session(self, session: _Session) -> bool:
"""
Extends an OAuth session whose lifetime has lapsed: locally
while the access token is valid, then via a refresh token grant.
Updates last_access both in the DB and in the session.
"""
if not self.__is_method_enabled('oauth'):
return False

token = session.token
transaction = None
try:
transaction = self.__config_db_sessionmaker()
row = transaction.query(OAuthToken, SessionRecord) \
.join(SessionRecord,
OAuthToken.auth_session_id == SessionRecord.id) \
.filter(SessionRecord.token == token) \
.limit(1).one_or_none()

if not row or not row[0].provider:
return False

oauth_token, session_record = row
now = datetime.now()

if oauth_token.expires_at and now < oauth_token.expires_at:
session_record.last_access = now
transaction.commit()
session.last_access = now
LOG.info("Extended session %s... locally, the %s access "
"token is valid until %s.",
token[:8], oauth_token.provider,
oauth_token.expires_at)
return True

oauth_config = self.get_oauth_config(oauth_token.provider)
if not oauth_config or not oauth_config.get('enabled'):
return False

LOG.info("Access token of session %s... expired at %s, "
"requesting a new one (provider: %s).",
token[:8], oauth_token.expires_at,
oauth_token.provider)

oauth2_session = OAuth2Session(
oauth_config['client_id'],
oauth_config['client_secret'],
scope=oauth_config['scope'])

new_token = oauth2_session.refresh_token(
oauth_config['token_url'],
refresh_token=oauth_token.refresh_token)

oauth_token.access_token = new_token['access_token']
oauth_token.refresh_token = new_token.get(
'refresh_token', oauth_token.refresh_token)
oauth_token.expires_at = \
datetime.fromtimestamp(new_token['expires_at'])
session_record.last_access = now

transaction.commit()
session.last_access = now
LOG.info("Refreshed session %s..., the new access token "
"expires at %s.", token[:8], oauth_token.expires_at)
return True
except OAuthError as e:
LOG.info("Refresh token of session %s... was rejected by %s "
"(%s), the user has to log in again.",
token[:8], oauth_token.provider, e.error)
return False
except Exception as e:
LOG.error("OAuth session extension failed for %s...: %s",
token[:8], str(e))
return False
finally:
if transaction:
transaction.close()

def invalidate_local_session(self, token):
"""
Remove a user's previous session from the local in memory store.
Expand Down Expand Up @@ -1173,5 +1270,6 @@ def __cleanup_sessions(self):
self.invalidate_local_session(s.token)

for s in self.__sessions[:]:
if not s.is_alive:
if not s.is_alive and not self.__try_extend_oauth_session(
s.token):
self.invalidate(s.token)
Loading
Loading