Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5c32cac
switch to role based ui header menus
cjllanwarne Oct 2, 2025
488abe4
status=>state
cjllanwarne Oct 2, 2025
1c19683
gate users page display behind permissions
cjllanwarne Oct 2, 2025
2cc4c7a
read/write permissions on roles page
cjllanwarne Oct 2, 2025
997c338
Update driver info page with readonly permissions
cjllanwarne Oct 2, 2025
eaee805
readonly pools UI
cjllanwarne Oct 2, 2025
91ba718
permissioning on job private pools and billing projects
cjllanwarne Oct 2, 2025
935000e
permissioning on ci pages
cjllanwarne Oct 2, 2025
3c605d3
system state management for admins not devs
cjllanwarne Oct 2, 2025
b825ffd
system state management for admins not devs
cjllanwarne Oct 2, 2025
b4deb89
monitoring is an admin action
cjllanwarne Oct 2, 2025
ed1122b
namespace management is an admin permission
cjllanwarne Oct 3, 2025
e13ae88
consolidate migrations
cjllanwarne Oct 3, 2025
be8cb72
WIP
cjllanwarne Oct 14, 2025
abbe606
WIP
cjllanwarne Oct 14, 2025
5855097
WIP
cjllanwarne Oct 30, 2025
ec0c001
WIP
cjllanwarne Nov 20, 2025
b0c0e4d
no more is_developer
cjllanwarne Nov 20, 2025
496bbbd
fix bootstrap, and correct driver user creation
cjllanwarne Nov 21, 2025
8e45974
avoid mutable default value
cjllanwarne Nov 21, 2025
fda94a4
fix ticks
cjllanwarne Nov 21, 2025
0877cac
unused userdat
cjllanwarne Dec 2, 2025
b0b05a5
merge refactor-system-permission changesets
cjllanwarne Dec 4, 2025
877ff66
oops
cjllanwarne Dec 4, 2025
f6aba00
fix migration script
cjllanwarne Dec 4, 2025
aa4b9dc
switch away from verify_dev
cjllanwarne Dec 4, 2025
a0d0806
fix tupling issue for a single value
cjllanwarne Dec 5, 2025
3e5dafd
fixup permission verifying endpoints again
cjllanwarne Dec 5, 2025
6142a06
support empty userdata in header
cjllanwarne Dec 5, 2025
fa4779d
fix table name
cjllanwarne Dec 5, 2025
c5b6307
fix users query
cjllanwarne Dec 5, 2025
4d2bf71
fix UI billing project lookup permissions
cjllanwarne Dec 5, 2025
781f11f
fix permission access check
cjllanwarne Dec 5, 2025
803415b
restore role options to create user panel
cjllanwarne Dec 5, 2025
54ddf58
fix system roles management
cjllanwarne Dec 10, 2025
8bd9e41
fix has_key error
cjllanwarne Dec 10, 2025
a7c390d
remove duplicate export definition
cjllanwarne Dec 10, 2025
6a8d267
fix developer role lookup to permission lookup in create_user
cjllanwarne Dec 10, 2025
e213cf4
fix variable parameterization issue; fix LEFT JOIN on roles during us…
cjllanwarne Dec 10, 2025
e5b9324
safety check accessing userdate permissions in web common
cjllanwarne Dec 10, 2025
747bea3
remove useless and incorrect role (not permission) check
cjllanwarne Dec 10, 2025
fe78fd1
fix how async generator is consumed
cjllanwarne Dec 12, 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
229 changes: 166 additions & 63 deletions auth/auth/auth.py

Large diffs are not rendered by default.

60 changes: 52 additions & 8 deletions auth/auth/driver/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from gear import Database, create_session
from gear.clients import get_identity_client
from gear.cloud_config import get_gcp_config, get_global_config
from gear.system_permissions import SystemPermission
from hailtop import aiotools, httpx
from hailtop import batch_client as bc
from hailtop.aiocloud.aioazure import AzureGraphClient
Expand Down Expand Up @@ -395,7 +396,22 @@ async def _create_user(app, user, skip_trial_bp, cleanup):
token = secret_alnum_string(3, case='numbers')
ident_token = f'{username}-{token}'

if user['is_developer'] == 1 or user['is_service_account'] == 1 or username == 'test':
system_permissions = []
if len(user['system_roles']) > 0:
# Fetch system permissions for the user's system roles
system_permissions = [
x['name']
async for x in db.select_and_fetchall(
'SELECT system_permissions.name FROM system_permissions JOIN system_role_permissions ON system_permissions.id = system_role_permissions.permission_id JOIN users_system_roles ON system_role_permissions.role_id = users_system_roles.role_id WHERE users_system_roles.user_id = %s',
(user['id'],),
)
]

if (
SystemPermission.ACCESS_DEVELOPER_ENVIRONMENTS.value in system_permissions
or user['is_service_account'] == 1
or username == 'test'
):
ident = username
else:
ident = ident_token
Expand Down Expand Up @@ -453,8 +469,13 @@ async def _create_user(app, user, skip_trial_bp, cleanup):
updates['hail_credentials_secret_name'] = hail_credentials_secret_name

namespace_name = user['namespace_name']
# auth services in test namespaces cannot/should not be creating and deleting namespaces
if namespace_name is None and user['is_developer'] == 1 and not is_test_deployment:
# Namespace creation step if it doesn't exist yet and the user is a developer
# NB auth services in test namespaces cannot/should not be creating and deleting namespaces
if (
namespace_name is None
and SystemPermission.ACCESS_DEVELOPER_ENVIRONMENTS.value in system_permissions
and not is_test_deployment
):
namespace_name = ident
namespace = K8sNamespaceResource(k8s_client)
cleanup.append(namespace.delete)
Expand Down Expand Up @@ -530,7 +551,9 @@ async def delete_user(app, user):
namespace_name = user['namespace_name']
# auth services in test namespaces cannot/should not be creating and deleting namespaces
if namespace_name is not None and namespace_name != DEFAULT_NAMESPACE and not is_test_deployment:
assert user['is_developer'] == 1
# Previously we asserted is_developer here. But it's easier to get a namespace without the right permission
# as roles are shuffled, and we would want to delete a floating developer namespace anyway...
# so let's just delete it.

# don't bother deleting database-server-config since we're
# deleting the namespace
Expand Down Expand Up @@ -574,18 +597,39 @@ async def resolve_identity_uid(app, hail_identity):
)


async def _users_in_state_with_roles(db: Database, state: str) -> List[dict]:
users = [
x
async for x in db.select_and_fetchall(
"""
SELECT users.*, GROUP_CONCAT(system_roles.name ORDER BY system_roles.name SEPARATOR ',') AS role_names
FROM users
JOIN users_system_roles ON users.id = users_system_roles.user_id
JOIN system_roles ON users_system_roles.role_id = system_roles.id
WHERE users.state = %s
GROUP BY users.id
""",
(state,),
)
]

for user in users:
user['system_roles'] = user['role_names'].split(',')
del user['role_names']

return users


async def update_users(app):
log.info('in update_users')

db = app['db']

creating_users = [x async for x in db.execute_and_fetchall('SELECT * FROM users WHERE state = %s;', 'creating')]

creating_users = await _users_in_state_with_roles(db, 'creating')
for user in creating_users:
await create_user(app, user)

deleting_users = [x async for x in db.execute_and_fetchall('SELECT * FROM users WHERE state = %s;', 'deleting')]

deleting_users = await _users_in_state_with_roles(db, 'deleting')
for user in deleting_users:
await delete_user(app, user)

Expand Down
5 changes: 5 additions & 0 deletions auth/auth/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def __init__(self, field_name, input, expected_type):
super().__init__(f"Expected '{field_name}' is of type {expected_type}. Found type {type(input)}", 'error')


class InvalidRole(AuthUserError):
def __init__(self, role):
super().__init__(f"Unknown system role '{role}'.", 'error')


class MultipleUserTypes(AuthUserError):
def __init__(self, username):
super().__init__(f"User '{username}' cannot be both a developer and a service account.", 'error')
Expand Down
66 changes: 29 additions & 37 deletions auth/auth/templates/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ components:
type: string
enum: [creating, active, deleting, deleted]
description: "Current state of the user account"
is_developer:
type: boolean
description: "Whether the user has developer privileges"
system_roles:
type: array
items:
type: string
description: "List of system roles assigned to the user"
is_service_account:
type: boolean
description: "Whether this is a service account"
Expand Down Expand Up @@ -80,15 +82,17 @@ components:
type: object
required:
- login_id
- is_developer
- system_roles
- is_service_account
properties:
login_id:
type: string
description: "Login identifier for the new user"
is_developer:
type: boolean
description: "Whether the user should have developer privileges"
system_roles:
type: array
items:
type: string
description: "List of system roles to assign to the new user"
is_service_account:
type: boolean
description: "Whether this is a service account"
Expand Down Expand Up @@ -280,36 +284,30 @@ paths:
'401':
description: Not authenticated

/api/v1alpha/verify_dev_credentials:
get:
summary: Verify developer credentials
description: Verify that the authenticated user has developer credentials
tags: [Authentication]
security:
- developerAuth: []
responses:
'200':
description: User has valid developer credentials
'401':
description: Unauthorized or not a developer

/api/v1alpha/verify_dev_or_sa_credentials:
/api/v1alpha/verify_system_permission:
get:
summary: Verify developer or service account credentials
description: Verify that the authenticated user has developer or service account credentials
summary: Verify system permission
description: Verify that the authenticated user has a specific system permission
tags: [Authentication]
security:
- developerAuth: []
parameters:
- name: permission
in: query
required: true
schema:
type: string
responses:
'200':
description: User has valid developer or service account credentials
description: User has the requested system permission
'401':
description: Unauthorized or not a developer/service account
description: Unauthorized

/api/v1alpha/check_system_permission:
get:
summary: Check system permission
description: Check if the authenticated user has a specific system permission
description: Verify that the authenticated user has a specific system permission. Otherwise returns a 401.
tags: [Authentication]
parameters:
- name: permission
Expand All @@ -321,18 +319,10 @@ paths:
responses:
'200':
description: Permission check completed successfully
content:
application/json:
schema:
type: object
properties:
has_permission:
type: boolean
description: Whether the user has the requested permission
'400':
description: Invalid permission parameter
'401':
description: Unauthorized
description: The caller does not have the specified system permission

/api/v1alpha/system_roles/me:
get:
Expand Down Expand Up @@ -580,9 +570,11 @@ paths:
state:
type: string
description: User state
is_developer:
type: integer
description: Whether user is a developer
system_roles:
type: array
items:
type: string
description: List of system roles assigned to the user
is_service_account:
type: integer
description: Whether user is a service account
Expand Down
4 changes: 4 additions & 0 deletions auth/auth/templates/roles.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ <h1 class='text-2xl font-light mb-4'>System role assignments</h1>
<ul class='list-none pl-0'>
{% for user in all_system_role_assignments[role]['users'] %}
<li class='flex items-center mb-1'>
{% if userdata['system_permissions']['assign_system_roles'] %}
<button
onclick="removeRole('{{ user }}', '{{ role }}')"
class='mr-2 w-6 flex-shrink-0 text-red-600 hover:text-red-800 transition-colors duration-200 flex items-center justify-center'
Expand All @@ -37,10 +38,12 @@ <h1 class='text-2xl font-light mb-4'>System role assignments</h1>
<path fill-rule='evenodd' d='M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z' clip-rule='evenodd'></path>
</svg>
</button>
{% endif %}
<span class='flex-grow text-left'>{{ user }}</span>
</li>
{% endfor %}
</ul>
{% if userdata['system_permissions']['assign_system_roles'] %}
<hr class='my-4 border-slate-200'>
<div class='flex items-center mt-4'>
<span class='mr-2 flex items-center justify-center'>
Expand All @@ -64,6 +67,7 @@ <h1 class='text-2xl font-light mb-4'>System role assignments</h1>
Add
</button>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
Expand Down
28 changes: 18 additions & 10 deletions auth/auth/templates/users.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

{% block content %}
<div class='w-full md:w-3/5 flex flex-col md:flex-row md:space-x-8 mb-8'>
{% if userdata['system_permissions']['create_users'] %}
<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">
Expand All @@ -25,8 +26,16 @@ <h1 class='text-2xl font-light mb-4'>Create User</h1>
<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>
{% for role in system_roles %}
<div>
<input type="checkbox" name="system_roles[]" value="{{ role }}" id="system_roles_{{ role }}" />
<label for="system_roles_{{ role }}">{{ role }}</label>
</div>
{% endfor %}
<div>
<input type="checkbox" name="is_service_account" value="1" id="is_service_account" />
<label for="is_service_account">Service Account</label>
</div>
</div>
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
<div class='mt-4'>
Expand All @@ -35,6 +44,8 @@ <h1 class='text-2xl font-light mb-4'>Create User</h1>
</div>
</form>
</div>
{% endif %}
{% if userdata['system_permissions']['update_users'] %}
<div class='w-full md:w-2/5'>
<h1 class='text-2xl font-light mb-4'>Reactivate User</h1>
<form action="{{ base_path }}/users/activate" method="POST" onsubmit="validateReactivateForm()">
Expand All @@ -54,6 +65,8 @@ <h1 class='text-2xl font-light mb-4'>Reactivate User</h1>
</div>
</form>
</div>
{% endif %}
{% if userdata['system_permissions']['delete_users'] %}
<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">
Expand All @@ -73,6 +86,8 @@ <h1 class='text-2xl font-light mb-4'>Delete User</h1>
</div>
</form>
</div>
{% endif %}
{% if userdata['system_permissions']['update_users'] %}
<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">
Expand All @@ -87,6 +102,7 @@ <h1 class='text-2xl font-light mb-4'>Invalidate All User Sessions</h1>
</div>
</form>
</div>
{% endif %}
</div>

<br />
Expand All @@ -96,10 +112,6 @@ <h1 class='text-2xl font-light mb-4'>Users</h1>
<input type="checkbox" id="show-active" class="form-checkbox" onchange="filterTable()">
<span class="ml-2">Only Active</span>
</label>
<label class="inline-flex items-center">
<input type="checkbox" id="show-developers" class="form-checkbox" onchange="filterTable()">
<span class="ml-2">Only Developers</span>
</label>
</div>
<table class="table-auto w-full" id="users-table">
<thead>
Expand All @@ -110,7 +122,6 @@ <h1 class='text-2xl font-light mb-4'>Users</h1>
<th class='h-12 bg-slate-200 font-light text-left'>Hail Identity</th>
<th class='h-12 bg-slate-200 font-light text-left'>State</th>
<th class='h-12 bg-slate-200 font-light text-left'>Last Active</th>
<th class='h-12 bg-slate-200 font-light'>Developer</th>
<th class='h-12 bg-slate-200 font-light rounded-tr pr-2'>Robot</th>
</tr>
</thead>
Expand All @@ -123,7 +134,6 @@ <h1 class='text-2xl font-light mb-4'>Users</h1>
<td>{{ user['hail_identity'] }}</td>
<td class='text-center' data-state="{{ user['state'] }}">{{ user['state'] }}</td>
<td class='text-center' data-state="{{ user['last_activated'] }}">{{ user['last_activated'] }}</td>
<td class='text-center' data-developer="{{ user['is_developer'] }}">{{ check_or_cross(user['is_developer']) }}</td>
<td class='text-center'>{{ check_or_cross(user['is_service_account']) }}</td>
</tr>
{% endfor %}
Expand All @@ -139,11 +149,9 @@ <h1 class='text-2xl font-light mb-4'>Users</h1>

rows.forEach(row => {
const state = row.querySelector('[data-state]').dataset.state.trim();
const isDeveloper = row.querySelector('[data-developer]').dataset.developer.trim();

let show = true;
if (showActive && state !== 'active') show = false;
if (showDevelopers && isDeveloper !== '1') show = false;

row.style.display = show ? '' : 'none';
});
Expand Down
Loading