Skip to content

Commit

Permalink
Merge pull request #3302 from HHS/update-30-minute-inactivity-logout-…
Browse files Browse the repository at this point in the history
…behavior

feat: Update 30 minute idle logout behavior
  • Loading branch information
jonnalley authored Jan 20, 2025
2 parents b6fd256 + 0b5a3ed commit 0cef725
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 6 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ MAPS. The purpose of OPS can be found on
## Dependencies

At a bare minimum, you need [Docker](https://www.docker.com) and
[Docker Compose](https://docs.docker.com/compose/install/) installed to run the application locally. If you want to do
development, you will also need to install [Python](https://www.python.org), [Node.js](https://nodejs.org), and
[Docker Compose](https://docs.docker.com/compose/install/) installed to run the application locally. [Podman](https://podman.io) has also been validated as a functional replacement for Docker.
If you want to do development, you will also need to install [Python](https://www.python.org), [Node.js](https://nodejs.org), and
[pre-commit](https://pre-commit.com/#installation).

## RSA Key Generation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""adding new OpsEvent type
Revision ID: 0536c9a5d32e
Revises: 52bf070f396e
Create Date: 2025-01-16 23:49:02.783289+00:00
"""
from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op
from alembic_postgresql_enum import TableReference

# revision identifiers, used by Alembic.
revision: str = '0536c9a5d32e'
down_revision: Union[str, None] = '52bf070f396e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.sync_enum_values(
enum_schema='ops',
enum_name='opseventtype',
new_values=['CREATE_BLI', 'UPDATE_BLI', 'DELETE_BLI', 'SEND_BLI_FOR_APPROVAL', 'CREATE_PROJECT', 'CREATE_NEW_AGREEMENT', 'UPDATE_AGREEMENT', 'DELETE_AGREEMENT', 'CREATE_NEW_CAN', 'UPDATE_CAN', 'DELETE_CAN', 'CREATE_CAN_FUNDING_RECEIVED', 'UPDATE_CAN_FUNDING_RECEIVED', 'DELETE_CAN_FUNDING_RECEIVED', 'CREATE_CAN_FUNDING_BUDGET', 'UPDATE_CAN_FUNDING_BUDGET', 'DELETE_CAN_FUNDING_BUDGET', 'CREATE_CAN_FUNDING_DETAILS', 'UPDATE_CAN_FUNDING_DETAILS', 'DELETE_CAN_FUNDING_DETAILS', 'ACKNOWLEDGE_NOTIFICATION', 'CREATE_BLI_PACKAGE', 'UPDATE_BLI_PACKAGE', 'CREATE_SERVICES_COMPONENT', 'UPDATE_SERVICES_COMPONENT', 'DELETE_SERVICES_COMPONENT', 'CREATE_PROCUREMENT_ACQUISITION_PLANNING', 'UPDATE_PROCUREMENT_ACQUISITION_PLANNING', 'DELETE_PROCUREMENT_ACQUISITION_PLANNING', 'CREATE_DOCUMENT', 'UPDATE_DOCUMENT', 'LOGIN_ATTEMPT', 'LOGOUT', 'IDLE_LOGOUT', 'GET_USER_DETAILS', 'CREATE_USER', 'UPDATE_USER', 'DEACTIVATE_USER'],
affected_columns=[TableReference(table_schema='ops', table_name='ops_event', column_name='event_type'), TableReference(table_schema='ops', table_name='ops_event_version', column_name='event_type')],
enum_values_to_rename=[],
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.sync_enum_values(
enum_schema='ops',
enum_name='opseventtype',
new_values=['CREATE_BLI', 'UPDATE_BLI', 'DELETE_BLI', 'SEND_BLI_FOR_APPROVAL', 'CREATE_PROJECT', 'CREATE_NEW_AGREEMENT', 'UPDATE_AGREEMENT', 'DELETE_AGREEMENT', 'CREATE_NEW_CAN', 'UPDATE_CAN', 'DELETE_CAN', 'CREATE_CAN_FUNDING_RECEIVED', 'UPDATE_CAN_FUNDING_RECEIVED', 'DELETE_CAN_FUNDING_RECEIVED', 'CREATE_CAN_FUNDING_BUDGET', 'UPDATE_CAN_FUNDING_BUDGET', 'DELETE_CAN_FUNDING_BUDGET', 'CREATE_CAN_FUNDING_DETAILS', 'UPDATE_CAN_FUNDING_DETAILS', 'DELETE_CAN_FUNDING_DETAILS', 'ACKNOWLEDGE_NOTIFICATION', 'CREATE_BLI_PACKAGE', 'UPDATE_BLI_PACKAGE', 'CREATE_SERVICES_COMPONENT', 'UPDATE_SERVICES_COMPONENT', 'DELETE_SERVICES_COMPONENT', 'CREATE_PROCUREMENT_ACQUISITION_PLANNING', 'UPDATE_PROCUREMENT_ACQUISITION_PLANNING', 'DELETE_PROCUREMENT_ACQUISITION_PLANNING', 'CREATE_DOCUMENT', 'UPDATE_DOCUMENT', 'LOGIN_ATTEMPT', 'LOGOUT', 'GET_USER_DETAILS', 'CREATE_USER', 'UPDATE_USER', 'DEACTIVATE_USER'],
affected_columns=[TableReference(table_schema='ops', table_name='ops_event', column_name='event_type'), TableReference(table_schema='ops', table_name='ops_event_version', column_name='event_type')],
enum_values_to_rename=[],
)
# ### end Alembic commands ###
3 changes: 2 additions & 1 deletion backend/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class OpsEventType(Enum):
CREATE_CAN_FUNDING_RECEIVED = auto()
UPDATE_CAN_FUNDING_RECEIVED = auto()
DELETE_CAN_FUNDING_RECEIVED = auto()

# CAN Funding Budget Related Events
CREATE_CAN_FUNDING_BUDGET = auto()
UPDATE_CAN_FUNDING_BUDGET = auto()
Expand Down Expand Up @@ -66,6 +66,7 @@ class OpsEventType(Enum):
# Auth Related Events
LOGIN_ATTEMPT = auto()
LOGOUT = auto()
IDLE_LOGOUT = auto()

# User Related Events
GET_USER_DETAILS = auto()
Expand Down
3 changes: 2 additions & 1 deletion backend/ops_api/ops/auth/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
get_bearer_token,
get_latest_user_session,
get_user_from_userinfo,
idle_logout,
)
from ops_api.ops.utils.errors import error_simulator
from ops_api.ops.utils.response import make_response_with_headers
Expand Down Expand Up @@ -130,7 +131,7 @@ def check_user_session_function(user: User):
raise InvalidUserSessionError(f"User with id={user.id} is using an invalid access token")
# Check if the last_accessed_at field of the latest user session is not more than a configurable threshold ago
if check_last_active_at(latest_user_session):
deactivate_all_user_sessions(user_sessions)
idle_logout(user, user_sessions)
raise InvalidUserSessionError(f"User with id={user.id} has not accessed the system for more than the threshold")
# Update the last_accessed_at field of the latest user session (if this isn't only touching /notification)
if "notification" not in request.endpoint:
Expand Down
20 changes: 18 additions & 2 deletions backend/ops_api/ops/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import requests
from authlib.jose import jwt as jose_jwt
from flask import Config, current_app, request
from flask_jwt_extended import create_access_token, create_refresh_token
from flask_jwt_extended import create_access_token, create_refresh_token, get_jwt_identity
from sqlalchemy import select
from sqlalchemy.orm import Session

from models import User, UserSession
from models import OpsEventType, User, UserSession
from ops_api.ops.auth.auth_types import UserInfoDict
from ops_api.ops.auth.exceptions import PrivateKeyError

Expand Down Expand Up @@ -197,3 +197,19 @@ def get_request_ip_address() -> str:
if request.headers.get("X-Forwarded-For")
else request.remote_addr
)


def idle_logout(user: User, user_sessions: list[UserSession]) -> dict[str, str]:
"""
Records an OpsEvent related to the user being logged out due to an idle session and deactivates all sessions including the current one.
Returns a dict of two strings containing a statement about what action was taken
"""
from ops_api.ops.utils.events import OpsEventHandler

with OpsEventHandler(OpsEventType.IDLE_LOGOUT) as la:
identity = get_jwt_identity()
la.metadata.update({"oidc_id": identity})

deactivate_all_user_sessions(user_sessions)

return {"message": f"User: {user.id} logged out for their session not being active within the configured threshold"}

0 comments on commit 0cef725

Please sign in to comment.