Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rc 1.8.12 #262

Merged
merged 6 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 9 additions & 3 deletions api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env python3

__copyright__ = 'Copyright (c) 2021-2022, Utrecht University'
__copyright__ = 'Copyright (c) 2021-2023, Utrecht University'
__license__ = 'GPLv3, see LICENSE'

import base64
import json
import sys
import zlib
from timeit import default_timer as timer
from typing import Any, Dict, Optional

Expand Down Expand Up @@ -52,7 +54,7 @@ def break_strings(N: int, m: int) -> int:
return (N - 1) // m + 1

def nrep_string_expr(s: str, m: int = 64) -> str:
return ' ++\n'.join('"{}"'.format(escape_quotes(s[i * m:i * m + m])) for i in range(break_strings(len(s), m) + 1))
return '++\n'.join('"{}"'.format(escape_quotes(s[i * m:i * m + m])) for i in range(break_strings(len(s), m) + 1))

if app.config.get('LOG_API_CALL_DURATION', False):
begintime = timer()
Expand All @@ -61,7 +63,11 @@ def nrep_string_expr(s: str, m: int = 64) -> str:
data = {}

params = json.dumps(data)
arg_str_expr = nrep_string_expr(params)

# Compress params and encode as base64 to reduce size (max rule length in iRODS is 20KB)
compressed_params = zlib.compress(params.encode())
base64_encoded_params = base64.b64encode(compressed_params)
arg_str_expr = nrep_string_expr(base64_encoded_params.decode('utf-8'))

# Set parameters as variable instead of parameter input to circumvent iRODS string limits.
rule_body = ''' *x={}
Expand Down
84 changes: 52 additions & 32 deletions search/static/search/js/revision.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,79 @@
var revisionTargetColl = '',
folderSelectBrowser = null,
dlgCurrentFolder = '',
currentSearchArg = '',
currentSearchStringRev = null,
currentSearchTypeRev = null,
mainTable = null;

const browsePageItems = 10;

$( document ).ready(function() {
// Click on file browser -> open revision details
startBrowsing(browsePageItems);
if ($('#file-browser').length && $('#search-filter').val().length > 0) {
currentSearchStringRev = $('#search-filter').val()
currentSearchTypeRev = $('#search_concept').attr('data-type')

$('#file-browser tbody').on('click', 'tr', function () {
if (currentSearchTypeRev === 'revision') {
browseRevisions()
}
}

// Click on file browser -> open revision details
$(document).on("click","#file-browser tbody tr",function() {
clickFileForRevisionDetails($(this), mainTable);
});

$('#search-panel li').on('click', function () {
currentSearchStringRev = $('#search-filter').val()
currentSearchTypeRev = $('#search_concept').attr('data-type')
browseRevisions()
})

$('.search-btn').on('click', function() {
currentSearchStringRev = $('#search-filter').val()
currentSearchTypeRev = $('#search_concept').attr('data-type')
browseRevisions();
});

$("#search-filter").bind('keypress', function(e) {
if (e.keyCode==13) {
currentSearchStringRev = $('#search-filter').val()
currentSearchTypeRev = $('#search_concept').attr('data-type')
browseRevisions();
}
});
});

/// MAIN TABLE containing revisioned files
function startBrowsing(items) {
var mainTable = $('#file-browser').DataTable({
"bFilter": false,
"bInfo": false,
"bLengthChange": true,
"language": {
"emptyTable": "No accessible files/folders present",
"lengthMenu": "_MENU_"
function browseRevisions()
{ if (currentSearchTypeRev === 'revision') {
// Destroy current Datatable.
const datatable = $('#file-browser').DataTable()
datatable.destroy()

$('#file-browser').DataTable({
bFilter: false,
bInfo: false,
bLengthChange: true,
language: {
emptyTable: 'Your search did not match any documents',
lengthMenu: '_MENU_'
},
"dom": '<"top">frt<"bottom"lp><"clear">',
'columns': [{render: tableRenderer.name, orderable: false, data: 'main_original_dataname'},
{render: tableRenderer.count, orderable: false, data: 'revision_count'}
dom: '<"top">rt<"bottom"lp><"clear">',
columns: [
{ render: tableRenderer.name, orderable: false, data: 'main_original_dataname' },
{ render: tableRenderer.count, orderable: false, data: 'revision_count' }
],
"ajax": getRevisionListContents,
"processing": true,
"serverSide": true,
"iDeferLoading": 0,
"ordering": false,
"pageLength": items
});
browseRevisions();
}

function browseRevisions()
{ currentSearchArg = $("#search-filter").val();

let fileBrowser = $('#file-browser').DataTable();
fileBrowser.ajax.reload();
ajax: getRevisionListContents,
processing: true,
serverSide: true,
ordering: false,
pageLength: 10
})
$('#file-browser').on('length.dt', function (e, settings, len) {
Yoda.storage.session.set('pageLength', len)
})
}
}

// Fetches directory contents to populate the listing table.
Expand Down Expand Up @@ -88,7 +108,7 @@ let getRevisionListContents = (() => {
let get = async (args) => {
// Check if we can use the cache.
if (cache.length
&& currentSearchArg === cacheSearchArg /// DIT MOET SEARCH ARGUMENT WORDEN!!!
&& currentSearchStringRev === cacheSearchArg /// DIT MOET SEARCH ARGUMENT WORDEN!!!
//&& args.order[0].dir === cacheSortOrder
//&& args.order[0].column === cacheSortCol
&& args.start >= cacheStart
Expand All @@ -100,7 +120,7 @@ let getRevisionListContents = (() => {
let j = ++i;

let result = await Yoda.call('revisions_search_on_filename',
{'searchString': currentSearchArg, /// TOEVOEGEN SEARCH ARGUMENT
{'searchString': currentSearchStringRev, /// TOEVOEGEN SEARCH ARGUMENT
'offset': args.start,
'limit': batchSize});

Expand All @@ -111,7 +131,7 @@ let getRevisionListContents = (() => {
total = result.total;
cacheStart = args.start;
cache = result.items;
cacheSearchArg = currentSearchArg;
cacheSearchArg = currentSearchStringRev;
//cacheSortCol = args.order[0].column;
//cacheSortOrder = args.order[0].dir;

Expand Down
57 changes: 48 additions & 9 deletions user/user.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/env python3

__copyright__ = 'Copyright (c) 2021-2022, Utrecht University'
__copyright__ = 'Copyright (c) 2021-2023, Utrecht University'
__license__ = 'GPLv3, see LICENSE'

import json
import secrets
from typing import List

import jwt
Expand Down Expand Up @@ -205,6 +206,9 @@ def userinfo_request(token: str) -> requests.Response:

return response

class StateMismatchError(Exception):
pass

class UserinfoSubMismatchError(Exception):
pass

Expand All @@ -216,6 +220,13 @@ class UserinfoEmailMismatchError(Exception):
exception_occurred = "OPENID_ERROR" # To identify exception in finally-clause

try:
email = g.login_username.lower()

# Ensure that the request is not a forgery and that the user sending
# this connect request is the expected user.
if request.args['state'] != session.get('state'):
raise StateMismatchError

token_response = token_request()
js = token_response.json()
access_token = js['access_token']
Expand Down Expand Up @@ -245,8 +256,6 @@ class UserinfoEmailMismatchError(Exception):

# Check if login email matches with user info email.
email_identifier = app.config.get('OIDC_EMAIL_FIELD')
email = g.login_username.lower()

userinfo_email = userinfo_payload[email_identifier]
if not isinstance(userinfo_email, list):
userinfo_email = [userinfo_email]
Expand Down Expand Up @@ -307,6 +316,10 @@ class UserinfoEmailMismatchError(Exception):
True
)

except StateMismatchError:
# Invalid state parameter.
log_error("Invalid state parameter")

except UserinfoSubMismatchError:
# Possible Token substitution attack.
log_error(
Expand All @@ -320,13 +333,20 @@ class UserinfoEmailMismatchError(Exception):
f'Mismatch between email and user info email: {email} is not in {userinfo_email}',
True
)
exception_occurred = "USERINFO_EMAIL_MISMATCH_ERROR"

except Exception:
log_error(f"Unexpected exception during callback for username {email}", True)

finally:
if exception_occurred == "CAT_INVALID_USER_ERROR" or exception_occurred == "CAT_INVALID_AUTHENTICATION":
flash('Username/password was incorrect', 'danger')
flash('Username / password was incorrect', 'danger')
elif exception_occurred == "USERINFO_EMAIL_MISMATCH_ERROR":
flash(
'Unable to sign in. Please verify that your username has been entered correctly. '
'If your username has been entered correctly and this issue persists, please contact the system administrator',
'danger'
)
elif exception_occurred == "OPENID_ERROR":
flash(
'An error occurred during the OpenID Connect protocol. '
Expand Down Expand Up @@ -357,6 +377,11 @@ def should_redirect_to_oidc(username: str) -> bool:
def oidc_authorize_url(username: str) -> str:
authorize_url: str = app.config.get('OIDC_AUTH_URI')

# Generate a random string for the state parameter.
# https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
session['state'] = secrets.token_urlsafe(32)
authorize_url += '&state=' + session['state']

if app.config.get('OIDC_LOGIN_HINT') and username:
authorize_url += '&login_hint=' + username

Expand Down Expand Up @@ -414,11 +439,25 @@ def prepare_user() -> None:
g.user = user_id
g.irods = irods

# Check for notifications.
endpoints = ["static", "call", "upload_get", "upload_post"]
if request.endpoint is not None and not request.endpoint.endswith(tuple(endpoints)):
response = api.call('notifications_load', data={})
g.notifications = len(response['data'])
try:
# Check for notifications.
endpoints = ["static", "call", "upload_get", "upload_post"]
if request.endpoint is not None and not request.endpoint.endswith(tuple(endpoints)):
response = api.call('notifications_load', data={})
g.notifications = len(response['data'])
except PAM_AUTH_PASSWORD_FAILED:
# Password is not valid any more (probably OIDC access token).
connman.clean(session.sid)
session.clear()

session['login_username'] = login_username

# If the username matches the domain set for OIDC
if should_redirect_to_oidc(login_username):
return redirect(oidc_authorize_url(login_username))
# Else (i.e. it is an external user, local user, or OIDC is disabled)
else:
return redirect(url_for('user_bp.login'))
else:
redirect('user_bp.login')

Expand Down
Loading