Skip to content

Commit

Permalink
[auth] Invalidate sessions hook (#14822)
Browse files Browse the repository at this point in the history
## Change Description

Adds a web endpoint to invalidate all user (non-robot) sessions. UI
element too:

<img width="1093" alt="image"
src="https://github.com/user-attachments/assets/9529bba3-7d55-447e-8ab3-dd19e4cfc070"
/>

## Security Assessment

Delete all except the correct answer:
- This change has a high security impact

### Impact Description

- New endpoint added. 
- Could cause DOS or lockout if accessible inappropriately. 

- Mitigated by:
  - Making this a developer-only functionality
- Using the same standard developer-only access pattern as we use
throughout the web service

(Reviewers: please confirm the security impact before approving)
  • Loading branch information
cjllanwarne authored Mar 6, 2025
1 parent 3733d6f commit 8f8c9f6
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 19 deletions.
26 changes: 26 additions & 0 deletions auth/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,12 @@ async def _delete_user(db: Database, username: str, id: Optional[str]):
raise UnknownUser(username)


async def _invalidate_all_sessions(db: Database):
await db.just_execute(
'DELETE s FROM sessions s JOIN users u ON s.user_id = u.id WHERE u.is_service_account = FALSE;'
)


@routes.post('/users/delete')
@web_security_headers
@auth.authenticated_developers_only()
Expand Down Expand Up @@ -758,6 +764,26 @@ async def maybe_pop_token(tx):
return json_response({'token': session['session_id'], 'username': session['username']})


@routes.post('/api/v1alpha/invalidate_all_sessions')
@api_security_headers
@auth.authenticated_developers_only()
async def rest_invalidate_all_sessions(request: web.Request, _) -> web.Response:
db = request.app[AppKeys.DB]
await _invalidate_all_sessions(db)
return web.Response(status=200)


@routes.post('/users/invalidate_all_sessions')
@web_security_headers
@auth.authenticated_developers_only()
async def invalidate_all_sessions(request: web.Request, _) -> NoReturn:
db = request.app[AppKeys.DB]
await _invalidate_all_sessions(db)
# Redirect to the user page. The session of the user calling this will have just been deleted, so this will
# allow the user to log back in.
raise web.HTTPFound(deploy_config.external_url('auth', '/user'))


@routes.post('/api/v1alpha/logout')
@api_security_headers
@auth.authenticated_users_only()
Expand Down
54 changes: 35 additions & 19 deletions auth/auth/templates/users.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,63 @@
{% endmacro %}

{% block content %}
<div class='w-full md:w-2/5 flex flex-col md:flex-row md:space-x-16 justify-between'>
<div>
<div class='w-full md:w-3/5 flex flex-col md:flex-row md:space-x-8 mb-8'>
<div class='w-full md:w-2/5'>
<h1 class='text-2xl font-light mb-4'>Create User</h1>
<form action="{{ base_path }}/users" method="POST">
<div class='flex-col space-y-2'>
<div class='flex justify-between space-x-4'>
<span>Username</span>
<input class='border rounded-sm p-1 text-sm' required name="username" />
<div class='flex items-center space-x-4'>
<span class='w-32'>Username</span>
<input class='border rounded-sm p-1 text-sm w-48' required name="username" />
</div>
<div class='flex justify-between space-x-4'>
<span>Login ID</span>
<input class='border rounded-sm p-1 text-sm' name="login_id" />
<div class='flex items-center space-x-4'>
<span class='w-32'>Login ID</span>
<input class='border rounded-sm p-1 text-sm w-48' name="login_id" />
</div>
<div class='space-y-1'>
<div><input type="checkbox" name="is_developer" value="1" /> Developer</div>
<div><input type="checkbox" name="is_service_account" value="1" /> Service Account</div>
</div>
<div><input type="checkbox" name="is_developer" value="1" /> Developer</div>
<div><input type="checkbox" name="is_service_account" value="1" /> Service Account</div>
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
<div class='flex justify-end'>
<div class='mt-4'>
{{ submit_button('Create') }}
</div>
</div>
</form>
</div>
<div>
<div class='w-full md:w-2/5'>
<h1 class='text-2xl font-light mb-4'>Delete User</h1>
<form action="{{ base_path }}/users/delete" method="POST">
<div class='flex-col space-y-2'>
<div class='flex justify-between space-x-4'>
<span>User ID</span>
<input class='border rounded-sm p-1 text-sm' required name="id" />
<div class='flex items-center space-x-4'>
<span class='w-32'>User ID</span>
<input class='border rounded-sm p-1 text-sm w-48' required name="id" />
</div>
<div class='flex justify-between space-x-4'>
<span>Username</span>
<input class='border rounded-sm p-1 text-sm' required name="username" />
<div class='flex items-center space-x-4'>
<span class='w-32'>Username</span>
<input class='border rounded-sm p-1 text-sm w-48' required name="username" />
</div>
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
<div class='flex justify-end'>
<div class='mt-4'>
{{ danger_button('Delete') }}
</div>
</div>
</form>
</div>
<div class='w-full md:w-1/5'>
<h1 class='text-2xl font-light mb-4'>Invalidate All User Sessions</h1>
<form action="{{ base_path }}/users/invalidate_all_sessions" method="POST">
<div class='flex-col space-y-4'>
<p class='text-sm text-amber-700'>
This will force all users to log back in to access the system. Any running jobs or batch submissions will continue to work normally.
</p>
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
<div>
{{ danger_button('Invalidate all user sessions') }}
</div>
</div>
</form>
</div>
</div>

<br />
Expand Down
3 changes: 3 additions & 0 deletions dev-docs/services/services-development-faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Eg for `batch`:
```bash
# First time only:
$ pip install -e web_common -e batch -e gear
# Tailwind 4 is not currently supported:
$ npm install tailwindcss@3


#Then:
$ SERVICE=batch make devserver
Expand Down

0 comments on commit 8f8c9f6

Please sign in to comment.