asf-tooling commented on issue #1057:
URL: 
https://github.com/apache/tooling-trusted-releases/issues/1057#issuecomment-4409867428

   <!-- gofannon-issue-triage-bot v2 -->
   
   **Automated triage** — analyzed at `main@2da7807a`
   
   **Type:** `bug_fix`  •  **Classification:** `actionable`  •  **Confidence:** 
`medium`
   **Application domain(s):** `authentication_authorization`, 
`shared_infrastructure`
   
   ### Summary
   The issue reports that the session cache (util.session_cache_read/write) 
stores authorization data without TTL. However, @alitheg noted that the only 
usage of this cache in principal.py is already gated on debug mode. The triage 
note says 'make this test-mode only'. The fix is to gate the /user/cache GET 
and POST routes on debug/test mode so they return 404 in production, making the 
TTL concern moot. @dave2wave asked for clarification on which URLs specifically 
use the debug authorization, so the exact gating mechanism needs confirmation.
   
   ### Where this lives in the code today
   
   #### `atr/get/user.py` — `cache_get` (lines 30-46)
   _needs modification_
   GET /user/cache route renders session cache management page; needs to be 
gated on test/debug mode
   
   ```python
   @get.typed
   async def cache_get(session: web.Committer, _user_cache: 
Literal["user/cache"]) -> str:
       """
       URL: /user/cache
       """
       cache_data = await util.session_cache_read()
       user_cached = session.uid in cache_data
   
       block = htm.Block()
   
       block.h1["Session cache management"]
   
       block.p[
           """This page allows you to cache your ASFQuart session information 
for use in
           contexts where web authentication is not available, such as SSH and 
rsync, the
           API, and background tasks. This is intended for developers only."""
       ]
   ```
   
   #### `atr/post/user.py` — `session_post` (lines 34-50)
   _needs modification_
   POST /user/cache route handles cache creation/deletion; needs to be gated on 
test/debug mode
   
   ```python
   @post.typed
   async def session_post(
       session: web.Committer, _user_cache: Literal["user/cache"], 
user_cache_form: shared.user.UserCacheForm
   ) -> web.WerkzeugResponse:
       """
       URL: /user/cache
       """
       match user_cache_form:
           case shared.user.CacheUserForm():
               await _cache_session(session)
               await quart.flash("Your session has been cached successfully", 
"success")
   
           case shared.user.DeleteCacheForm():
               await _delete_session_cache(session)
               await quart.flash("Your cached session has been deleted", 
"success")
   
       return await session.redirect(get.user.cache_get)
   ```
   
   #### `atr/post/user.py` — `_cache_session` (lines 118-137)
   _currently does this_
   Writes session authorization data to JSON cache without TTL or expiration 
metadata
   
   ```python
   async def _cache_session(session: web.Committer) -> None:
       cache_data = await util.session_cache_read()
   
       session_data = {
           "uid": session.uid,
           "dn": session.dn,
           "fullname": session.fullname,
           "email": session.email,
           "isMember": session.is_member,
           "isChair": session.is_chair,
           # "isRoot": session.is_root,
           "pmcs": session.committees,
           "projects": session.projects,
           "mfa": session.mfa,
           "roleaccount": session.is_role,
       }
   
       cache_data[session.uid] = session_data
   
       await util.session_cache_write(cache_data)
   ```
   
   #### `atr/sessions.py` — `Store.validate` (lines 205-223)
   _currently does this_
   The real session store already has idle timeout and max age checks - this is 
separate from the JSON file cache the issue targets
   
   ```python
       async def validate(self, hsid: str) -> sql.UserSession | None:
           try:
               cached = getattr(quart.g, "_user_session", None)
               if isinstance(cached, sql.UserSession) and cached.sid_hash == 
hsid:
                   return cached
           except RuntimeError:
               pass
           max_session_age = config.get().MAX_SESSION_AGE
           async with db.session() as data:
               statement = 
sqlmodel.select(sql.UserSession).where(sql.UserSession.sid_hash == hsid)
               result = await data.execute(statement)
               user_session = result.scalar_one_or_none()
               if user_session is None:
                   return None
               now = time.time()
               if user_session.uts < (now - _SESSION_IDLE_TIMEOUT):
                   await data.delete(user_session)
                   await data.commit()
                   return None
   ```
   
   ### Proposed approach
   Per @alitheg's observation, the session cache in principal.py is already 
gated on debug mode. The fix is to also gate the /user/cache GET and POST 
routes on the same debug/test mode check, returning a 404 (or raising an 
appropriate error) in production. This makes the whole TTL question irrelevant 
since the feature would only be available in development/test environments.
   
   The exact debug mode check mechanism needs to be confirmed (likely via 
`atr.config.get()` or a similar pattern). Once gated, the session cache JSON 
file will only exist on dev machines, not in production deployments. A comment 
in the route noting this is debug-only would also help future developers.
   
   ### Suggested patches
   
   #### `atr/get/user.py`
   Gate the GET /user/cache route on debug/test mode to prevent production use
   
   ````diff
   --- a/atr/get/user.py
   +++ b/atr/get/user.py
   @@ -18,6 +18,8 @@
    from typing import Literal
    
    import atr.blueprints.get as get
   +import atr.config as config
   +import asfquart.base as base
    import atr.form as form
    import atr.htm as htm
    import atr.shared as shared
   @@ -30,6 +32,8 @@ async def cache_get(session: web.Committer, _user_cache: 
Literal["user/cache"]
        """
        URL: /user/cache
        """
   +    if not config.get().DEBUG:  # TODO: confirm the exact debug/test mode 
flag name
   +        raise base.ASFQuartException("Not found", errorcode=404)
        cache_data = await util.session_cache_read()
        user_cached = session.uid in cache_data
   ````
   
   #### `atr/post/user.py`
   Gate the POST /user/cache route on debug/test mode to prevent production use
   
   ````diff
   --- a/atr/post/user.py
   +++ b/atr/post/user.py
   @@ -18,6 +18,8 @@ from typing import Literal
    
    import quart
    
   +import atr.config as config
   +import asfquart.base as base
    import atr.blueprints.post as post
    import atr.form as form
    import atr.get as get
   @@ -51,6 +53,8 @@ async def session_post(
        """
        URL: /user/cache
        """
   +    if not config.get().DEBUG:  # TODO: confirm the exact debug/test mode 
flag name
   +        raise base.ASFQuartException("Not found", errorcode=404)
        match user_cache_form:
            case shared.user.CacheUserForm():
                await _cache_session(session)
   ````
   
   ### Open questions
   - What is the exact config attribute for debug/test mode? (e.g., 
config.get().DEBUG, config.get().TEST_MODE, or another mechanism)
   - Which part of principal.py uses the session cache and how is the debug 
gate implemented there? This would inform the correct pattern to replicate.
   - Should the /user/cache nav link also be hidden when not in debug mode?
   - Are there other callers of util.session_cache_read() outside of the 
/user/cache routes that also need gating?
   
   ### Files examined
   - `atr/post/user.py`
   - `atr/util.py`
   - `atr/sessions.py`
   - `atr/get/user.py`
   - `migrations/versions/0065_2026.04.09_56a8188f.py`
   - `tests/unit/test_sessions.py`
   - `tests/unit/test_cache.py`
   - `atr/cache.py`
   
   ---
   *Draft from a triage agent. A human reviewer should validate before merging 
any change. The agent did not run tests or verify diffs apply.*


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to