Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
239c0b8
Update pip lockfiles (#15206)
chrisvittal Dec 9, 2025
5d54486
Remove unnecessary accessConfigs parameter from VM config API payload…
grohli Dec 10, 2025
e6acdb2
[query] mill multi-file builds (#15207)
ehigham Dec 15, 2025
4abeb1f
[auth] Remove low-risk reflected xss finding (#15213)
cjllanwarne Dec 16, 2025
0718551
[auth] Add 'inactive' status to the list of account errors (#15210)
chrisvittal Dec 16, 2025
42504e3
[query] supplant `build-info.properties` with cgen (#15211)
ehigham Jan 5, 2026
3458ff8
[terraform] Dynamic cloudnat ports and all regions (#15212)
cjllanwarne Jan 7, 2026
5dcb3c9
[hailctl] Direct to the batch ui for dev deploy builds (#15214)
cjllanwarne Jan 12, 2026
cb54af4
[query] Replace Deprecated `AnyRefMap` with `HashMap` (#15221)
ehigham Jan 13, 2026
3033aa4
[terraform] Make nat maxes higher, powers of 2 (#15228)
cjllanwarne Jan 15, 2026
12d2541
[compiler] misc 2.13 deprecation fixes (#15226)
patrick-schultz Jan 16, 2026
1e1dbf6
[query] replace `readRepeatedly` with built-in `readNBytes` (#15231)
ehigham Jan 20, 2026
e68a2a0
[docs] Document refreshing the trivy scanner gsa key (#15235)
cjllanwarne Jan 22, 2026
fb3343b
[query] remove tunion (#15232)
ehigham Jan 23, 2026
a8a3cd2
[Services] Bump python deps (#15218)
cjllanwarne Jan 29, 2026
3eb0827
[query] Remove ENumpyBinaryNDArray (#15219)
chrisvittal Jan 29, 2026
0a67c1d
[query] Fix BlockMatrix persist (#15233)
chrisvittal Jan 29, 2026
3fae026
[query] remove `RichRegex` (#15236)
ehigham Jan 29, 2026
9e1e01d
[query] fix LiftMeOut blockargs (#15245)
ehigham Jan 30, 2026
ca9193d
[query] sort block args matching (#15247)
ehigham Jan 30, 2026
3ed78fd
Match error message regardless of single/double quote marks (#15237)
jmarshall Jan 30, 2026
b6c5643
[query] printer/parser rules for StreamBufferedAggregate (#15241)
ehigham Jan 30, 2026
2cbc8c2
[query] AggFilter blockargs (#15250)
ehigham Jan 30, 2026
3c34d5c
[query] AggGroupBy blockargs (#15249)
ehigham Jan 30, 2026
2e2bd7a
Feb 2nd step en route to 0.2.138 release
dancoates Feb 1, 2026
4f87ee5
[compiler] enforce use of `override` keyword using wartremover (#15238)
patrick-schultz Feb 3, 2026
9b5e325
[batch] ensure worker VMs are ipv4 only (#15256)
cjllanwarne Feb 3, 2026
9432b61
[compiler] delete EmitRegion (#15248)
patrick-schultz Feb 4, 2026
a702d83
[query] delete unused function registry from python client (#15257)
ehigham Feb 5, 2026
9ec6553
[batch] Refactor js to remove unsafe-inline CSP (#15191)
kush-chandra Feb 5, 2026
6efcae8
[ukbb-rg] Remove ukbb-rg from the main hail project. (#15261)
chrisvittal Feb 5, 2026
6f7b5d1
[query] replace ApplySeeded with RNGSplitStatic + Apply (#15254)
ehigham Feb 6, 2026
42ed3ef
Merged to upstream as of Feb 9th 2026
hrrsheen Feb 8, 2026
6f09557
Adding STRAIGHT_JOIN to SELECT in search query sql (#15234)
grohli Feb 10, 2026
122cc7c
[ci|release] absolve ci of testing and releasing on azure (#15258)
ehigham Feb 10, 2026
4ad0f86
[ci] fix copy_third_party_images (#15269)
ehigham Feb 10, 2026
3c34320
[query] Fix all-ref case in haplotypeFreqEM (#15270)
chrisvittal Feb 11, 2026
126c765
[query] use one finalizer in PartitionNativeIntervalReader (#15274)
ehigham Feb 12, 2026
84d58f5
[batch] Use dockerhub proxy to reduce total requests to dockerhub (#1…
cjllanwarne Feb 12, 2026
76fdf2d
Update base ubuntu image to noble 20260113 (#15240)
cjllanwarne Feb 13, 2026
b302efd
[build] Fix mirror_hailgenetics_images task (#15278)
cjllanwarne Feb 13, 2026
8eecfbb
[query] extract `utils` into new module (#15223)
ehigham Feb 13, 2026
909a7a8
Merging from upstream as of Feb 16, 2026.
milo-hyben Feb 15, 2026
0bfc4b5
[batch] Fix test_submit during deploy (#15281)
cjllanwarne Feb 17, 2026
b316ad7
[terraform] Reconcile terraform with current state (#15280)
cjllanwarne Feb 17, 2026
697aa49
[batch] Refresh incomplete batch status pages (#15283)
cjllanwarne Feb 18, 2026
99ae33b
[query] Sym is unused (#15282)
ehigham Feb 18, 2026
1c4c0a0
[query] Cache Alpha-Equivalent Compiled Functions (#15262)
ehigham Feb 18, 2026
cc9751a
[query] LiftMeOut is uninterpretable (#15263)
ehigham Feb 18, 2026
9fc57e6
[query] delete legacy IRandomness (#15271)
ehigham Feb 18, 2026
0461108
[services] Bump pip dependencies (#15268)
cjllanwarne Feb 19, 2026
4a93e20
[services] refresh requirements, fixing check_pip_requirements (#15293)
cjllanwarne Feb 20, 2026
3058d17
[batch] Fixes a bad use of id in a log message (#15284)
cjllanwarne Feb 20, 2026
5abe18f
[auth] More helpful inactive account message (#15286)
cjllanwarne Feb 20, 2026
d5af360
Merged to upstream as of 23 Feb 2026
jmarshall Feb 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions auth/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
recursive-include auth/templates *
recursive-include auth/static *
45 changes: 37 additions & 8 deletions auth/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from urllib.parse import urlparse

import aiohttp_session
import jinja2
import kubernetes_asyncio.client
import kubernetes_asyncio.client.rest
import kubernetes_asyncio.config
Expand Down Expand Up @@ -67,6 +68,8 @@

CLOUD = get_global_config()['cloud']
DEFAULT_NAMESPACE = os.environ['HAIL_DEFAULT_NAMESPACE']
INACTIVE_USER_TIMEOUT_DAYS = int(os.environ.get('INACTIVE_USER_TIMEOUT_DAYS', '60'))
AUTH_ROOT = os.path.dirname(os.path.abspath(__file__))

is_test_deployment = DEFAULT_NAMESPACE != 'default'

Expand Down Expand Up @@ -219,9 +222,7 @@ def validate_next_page_url(next_page):
actual_next_page_domain = urlparse(next_page).netloc

if actual_next_page_domain not in valid_next_domains:
raise web.HTTPBadRequest(
text=f'Invalid next page: \'{next_page}\'. Domain \'{actual_next_page_domain}\' not in {valid_next_domains}'
)
raise web.HTTPBadRequest(text='Invalid next page.')


@routes.get('/healthcheck')
Expand All @@ -243,6 +244,22 @@ async def openapi(request):
return await render_template('auth', request, None, 'openapi.yaml', page_context)


@routes.get('/auth/static/js/{filename}')
@web_security_headers
async def fetch_js_file(request):
filename = request.match_info['filename']
if filename.endswith('.js'):
page_context = {'base_path': deploy_config.base_path('auth')}
try:
response = await render_template('auth', request, None, f'js/{filename}', page_context)
response.content_type = 'application/javascript'
return response
except jinja2.exceptions.TemplateNotFound as error:
raise web.HTTPNotFound() from error
else:
raise web.HTTPNotFound()


@routes.get('')
@routes.get('/')
@web_security_headers
Expand Down Expand Up @@ -271,7 +288,12 @@ async def creating_account(request: web.Request, userdata: Optional[UserData]) -
set_message(session, f'Account does not exist for login id {login_id}.', 'error')
raise web.HTTPFound(deploy_config.external_url('auth', ''))

page_context = {'username': user['username'], 'state': user['state'], 'login_id': user['login_id']}
page_context = {
'username': user['username'],
'state': user['state'],
'login_id': user['login_id'],
'inactive_timeout_days': INACTIVE_USER_TIMEOUT_DAYS,
}

if user['state'] in ('deleting', 'deleted'):
return await render_template('auth', request, userdata, 'account-error.html', page_context)
Expand Down Expand Up @@ -422,9 +444,16 @@ async def callback(request) -> web.Response:

raise web.HTTPFound(creating_url)

if user['state'] in ('deleting', 'deleted'):
page_context = {'username': user['username'], 'state': user['state'], 'login_id': user['login_id']}
return await render_template('auth', request, user, 'account-error.html', page_context)
if user['state'] in ('deleting', 'deleted', 'inactive'):
page_context = {
'username': user['username'],
'state': user['state'],
'login_id': user['login_id'],
'inactive_timeout_days': INACTIVE_USER_TIMEOUT_DAYS,
}
return await render_template(
'auth', request, user, 'account-error.html', page_context, status_code=web.HTTPUnauthorized.status_code
)

if user['state'] == 'creating':
if caller == 'signup':
Expand Down Expand Up @@ -1239,7 +1268,7 @@ def run():
]
)

setup_aiohttp_jinja2(app, 'auth')
setup_aiohttp_jinja2(app, 'auth', jinja2.FileSystemLoader(f'{AUTH_ROOT}/static/'))
setup_aiohttp_session(app)

setup_common_static_routes(routes)
Expand Down
5 changes: 5 additions & 0 deletions auth/auth/static/js/account-creating.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let protocol = location.protocol.replace("http", "ws")
let sock = new WebSocket(protocol + location.host + "{{ base_path }}/creating/wait");
sock.onmessage = function (_event) {
window.location.reload()
}
73 changes: 73 additions & 0 deletions auth/auth/static/js/roles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const csrfToken = document.head.querySelector('meta[name="csrf"]')?.getAttribute('value') || '';

document.getElementsByName('remove-role-button').forEach(button => {
button.addEventListener('click', (_e) => { removeRole(button.dataset.user, button.dataset.role); })
});

document.getElementsByName('add-role-button').forEach(button => {
button.addEventListener('click', (_e) => { addRole(button.dataset['username-input-id'], button.dataset.role); })
});

async function removeRole(username, roleName) {
if (!confirm(`Are you sure you want to remove the '${roleName}' role from user '${username}'?`)) {
return;
}

try {
const response = await fetch("{{ base_path }}/api/v1alpha/system_roles/${username}", {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
role_removal: roleName
})
});

if (response.ok) {
// Refresh the page to show updated data
window.location.reload();
} else {
const errorData = await response.json().catch(() => ({}));
alert(`Failed to remove role: ${errorData.message || response.statusText}`);
}
} catch (error) {
console.error('Error removing role:', error);
alert('Failed to remove role. Please try again.');
}
}

async function addRole(usernameInputId, roleName) {
const username = document.getElementById(usernameInputId).value.trim();

if (!username) {
alert('Please enter a username');
return;
}

try {
const response = await fetch("{{ base_path }}/api/v1alpha/system_roles/${username}", {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
role_addition: roleName
})
});

if (response.ok) {
// Clear the input and refresh the page
document.getElementById(usernameInputId).value = '';
window.location.reload();
} else {
const errorData = await response.json().catch(() => ({}));
alert(`Failed to add role: ${errorData.message || response.statusText}`);
}
} catch (error) {
console.error('Error adding role:', error);
alert('Failed to add role. Please try again.');
}
}
23 changes: 23 additions & 0 deletions auth/auth/static/js/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const tosCheckbox = document.getElementById('tosCheckbox');
tosCheckbox.addEventListener('load', (_e) => { updateButtons(); })
tosCheckbox.addEventListener('change', (_e) => { updateButtons(); })

function updateButtons() {
const checkbox = document.getElementById('tosCheckbox');
const signupButton = document.getElementById('signupButton');
const loginButton = document.getElementById('loginButton');

signupButton.disabled = !checkbox.checked;
loginButton.disabled = !checkbox.checked;

// Update button styles based on state
[signupButton, loginButton].forEach(button => {
if (button.disabled) {
button.classList.add('opacity-50', 'cursor-not-allowed');
button.classList.remove('hover:bg-blue-700');
} else {
button.classList.remove('opacity-50', 'cursor-not-allowed');
button.classList.add('hover:bg-blue-700');
}
});
}
35 changes: 35 additions & 0 deletions auth/auth/static/js/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const showActiveCheckbox = document.getElementById('show-active');
showActiveCheckbox.addEventListener('change', (_e) => { filterTable(); })
const showDevelopersCheckbox = document.getElementById('show-developers');
showDevelopersCheckbox.addEventListener('change', (_e) => { filterTable(); })

const reactivateUserForm = document.getElementById('reactivate-user-form');
reactivateUserForm.addEventListener('submit', (_e) => { validateReactivateForm(); })

function filterTable() {
const showActive = document.getElementById('show-active').checked;
const showDevelopers = document.getElementById('show-developers').checked;

const rows = document.querySelectorAll('#users-table tbody tr');

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';
});
}
function validateReactivateForm() {
const userId = document.getElementById('inactive-id').value.trim();
const username = document.getElementById('inactive-username').value.trim();

if (!userId && !username) {
alert('Please provide either a User ID or a Username.');
return false;
}
return true;
}
8 changes: 2 additions & 6 deletions auth/auth/templates/account-creating.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
<div class="spinner"></div>
<b>Creating account for {{ username }}</b>
</div>
<script>
let protocol = location.protocol.replace("http", "ws")
let sock = new WebSocket(protocol + location.host + "{{ base_path }}/creating/wait");
sock.onmessage = function (event) {
window.location.reload()
}

<script defer src='{{ auth_base_url }}/auth/static/js/account-creating.js'>
</script>
{% endblock %}
27 changes: 23 additions & 4 deletions auth/auth/templates/account-error.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@
{% block title %}Account Error{% endblock %}
{% block content %}
<div class="vcentered">
<div><b>Account is in a bad state!</b></div>
{% if state == 'inactive' %}
<div><b>Your account is inactive</b></div>
<br>
<div>
Please contact <a href="mailto:hail-team@broadinstitute.org">hail-team@broadinstitute.org</a>
Accounts that haven't been logged into for {{ inactive_timeout_days }} days are automatically marked as inactive.
<br>
{% if support_email %}
Contact <a href="mailto:{{ support_email }}?subject=Reactivate%20account%3A%20{{ username | urlencode }}&body=Hello%2C%0A%0APlease%20reactivate%20my%20Hail%20account.%0A%0AUsername%3A%20{{ username | urlencode }}%0ALogin%20ID%3A%20{{ login_id | urlencode }}%0A%0AThank%20you" class="text-sky-600 underline hover:text-sky-800 font-medium">{{ support_email }}</a> to reactivate.
{% else %}
Contact contact your support team to reactivate.
{% endif %}
</div>
{% else %}
<div><b>Account is in a bad state!</b></div>
<br>
<div>
{% if support_email %}
Please contact <a href="mailto:{{ support_email }}" class="text-sky-600 underline hover:text-sky-800 font-medium">{{ support_email }}</a> to get help.
{% else %}
Please contact your support team to get help.
{% endif %}
<br>
<div class="small">Username: <b>{{ username }}</b></div>
<div class="small">Login ID: <b>{{ login_id }}</b></div>
<div class="small">State: <b>{{ state }}</b></div>
</div>
{% endif %}
<br>
<div class="small">Username: <b>{{ username }}</b></div>
<div class="small">Login ID: <b>{{ login_id }}</b></div>
</div>
{% endblock %}
Loading