Commit 2b8429c2 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Misc fixes and improvements.

parent 0e2e33e4
......@@ -59,6 +59,7 @@ class Upload {
uploadFile(file) {
const uploadFileWithProgress = async() => {
const authHeaders = await this.api.authHeaders()
let uploadRequest = await UploadRequest(
{
request: {
......@@ -66,7 +67,7 @@ class Upload {
method: 'PUT',
headers: {
'Content-Type': 'application/gzip',
...this.api.authHeaders
...authHeaders
}
},
files: [file],
......@@ -76,7 +77,7 @@ class Upload {
}
)
if (uploadRequest.error) {
handleApiError(uploadRequest.error)
handleApiError(uploadRequest.response ? uploadRequest.response.message : uploadRequest.error)
}
if (uploadRequest.aborted) {
throw Error('User abort')
......@@ -95,7 +96,7 @@ class Upload {
return new Promise(resolve => resolve(this))
} else {
if (this.upload_id) {
return this.api.swaggerPromise.then(client => client.apis.uploads.get_upload({
return this.api.swagger().then(client => client.apis.uploads.get_upload({
upload_id: this.upload_id,
page: page || 1,
per_page: perPage || 5,
......@@ -143,31 +144,48 @@ function handleApiError(e) {
}
class Api {
static async createSwaggerClient(accessToken) {
let data
if (accessToken) {
let auth = {
'OpenIDConnect Bearer Token': `Bearer ${accessToken}`
}
data = {authorizations: auth}
}
swagger() {
const self = this
return new Promise((resolve, reject) => {
self.keycloak.updateToken()
.success(() => {
self._swaggerClient
.then(swaggerClient => {
swaggerClient.authorizations = {
'OpenIDConnect Bearer Token': `Bearer ${self.keycloak.token}`
}
resolve(swaggerClient)
})
.catch(() => {
reject(new ApiError())
})
})
.error(() => {
reject(new ApiError())
})
})
}
try {
return await Swagger(`${apiBase}/swagger.json`, data)
} catch (e) {
throw new ApiError()
}
authHeaders() {
return new Promise((resolve, reject) => {
this.keycloak.updateToken()
.success(() => {
resolve({
'Authorization': `Bearer ${this.keycloak.token}`
})
})
.error(() => {
reject(new ApiError())
})
})
}
constructor(accessToken) {
constructor(keycloak) {
this.onStartLoading = () => null
this.onFinishLoading = () => null
this.authHeaders = {
'Authentication': `Bearer ${accessToken}`
}
this.swaggerPromise = Api.createSwaggerClient(accessToken).catch(handleApiError)
this._swaggerClient = Swagger(`${apiBase}/swagger.json`)
this.keycloak = keycloak
// keep a list of localUploads, these are uploads that are currently uploaded through
// the browser and that therefore not yet returned by the backend
......@@ -188,7 +206,7 @@ class Api {
async getUnpublishedUploads() {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.uploads.get_uploads({state: 'unpublished', page: 1, per_page: 1000}))
.catch(handleApiError)
.then(response => ({
......@@ -204,7 +222,7 @@ class Api {
async getPublishedUploads(page, perPage) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.uploads.get_uploads({state: 'published', page: page || 1, per_page: perPage || 10}))
.catch(handleApiError)
.then(response => ({
......@@ -220,7 +238,7 @@ class Api {
async archive(uploadId, calcId) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.archive.get_archive_calc({
upload_id: uploadId,
calc_id: calcId
......@@ -247,7 +265,7 @@ class Api {
async calcProcLog(uploadId, calcId) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.archive.get_archive_logs({
upload_id: uploadId,
calc_id: calcId
......@@ -259,7 +277,7 @@ class Api {
async getRawFileListFromCalc(uploadId, calcId) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.raw.get_file_list_from_calc({
upload_id: uploadId,
calc_id: calcId,
......@@ -272,7 +290,7 @@ class Api {
async repo(uploadId, calcId) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.repo.get_repo_calc({
upload_id: uploadId,
calc_id: calcId
......@@ -284,7 +302,7 @@ class Api {
async search(search) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.repo.search(search))
.catch(handleApiError)
.then(response => response.body)
......@@ -293,7 +311,7 @@ class Api {
async deleteUpload(uploadId) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.uploads.delete_upload({upload_id: uploadId}))
.catch(handleApiError)
.then(response => response.body)
......@@ -302,7 +320,7 @@ class Api {
async publishUpload(uploadId, withEmbargo) {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.uploads.exec_upload_operation({
upload_id: uploadId,
payload: {
......@@ -319,10 +337,10 @@ class Api {
async getSignatureToken() {
this.onStartLoading()
return this.swaggerPromise
.then(client => client.apis.auth.get_token())
return this.swagger()
.then(client => client.apis.auth.get_auth())
.catch(handleApiError)
.then(response => response.body)
.then(response => response.body.signature_token)
.finally(this.onFinishLoading)
}
......@@ -339,7 +357,7 @@ class Api {
this.onStartLoading()
try {
const loadMetaInfo = async(path) => {
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.archive.get_metainfo({metainfo_package_name: path}))
.catch(handleApiError)
.then(response => response.body)
......@@ -360,7 +378,7 @@ class Api {
async getInfo() {
if (!this._cachedInfo) {
this.onStartLoading()
this._cachedInfo = await this.swaggerPromise
this._cachedInfo = await this.swagger()
.then(client => {
return client.apis.info.get_info()
.then(response => response.body)
......@@ -373,7 +391,7 @@ class Api {
async getUploadCommand() {
this.onStartLoading()
return this.swaggerPromise
return this.swagger()
.then(client => client.apis.uploads.get_upload_command())
.catch(handleApiError)
.then(response => response.body)
......@@ -392,9 +410,18 @@ export class ApiProviderComponent extends React.Component {
keycloakInitialized: PropTypes.bool
}
constructor(props) {
super(props)
this.onToken = this.onToken.bind(this)
}
onToken(token) {
console.log(token)
}
update() {
const { keycloak } = this.props
this.setState({api: this.createApi(keycloak.token)})
this.setState({api: this.createApi(keycloak)})
if (keycloak.token) {
keycloak.loadUserInfo()
.success(user => {
......@@ -416,8 +443,8 @@ export class ApiProviderComponent extends React.Component {
}
}
createApi(accessToken) {
const api = new Api(accessToken)
createApi(keycloak) {
const api = new Api(keycloak)
api.onStartLoading = (name) => {
this.setState(state => ({loading: state.loading + 1}))
}
......
......@@ -49,7 +49,7 @@ class Download extends React.Component {
fullUrl = `${window.location.origin}${fullUrl}`
}
const downloadUrl = new URL(fullUrl)
downloadUrl.searchParams.append('token', result.token)
downloadUrl.searchParams.append('signature_token', result)
FileSaver.saveAs(downloadUrl.href, fileName)
this.setState({preparingDownload: false})
})
......
......@@ -41,7 +41,7 @@ import uuid
from nomad import config, processing, utils, infrastructure, datamodel
from .app import api, RFC3339DateTime
from .app import api
# Authentication scheme definitions, for swagger
......@@ -129,7 +129,7 @@ def authenticate(
if token is not None:
try:
decoded = jwt.decode(token, config.services.api_secret, algorithms=['HS256'])
user = datamodel.User.get(decoded['user'])
user = datamodel.User(user_id=decoded['user'], email=None)
if user is None:
abort(401, 'User for the given signature does not exist')
else:
......@@ -144,12 +144,10 @@ def authenticate(
elif 'token' in request.args:
abort(401, 'Queram param token not supported for this endpoint')
user_or_error = infrastructure.keycloak.authorize_flask(basic=basic)
if user_or_error is not None:
if isinstance(user_or_error, datamodel.User):
g.user = user_or_error
else:
abort(401, message=user_or_error)
else:
error = infrastructure.keycloak.authorize_flask(basic=basic)
if error is not None:
abort(401, message=error)
if required and g.user is None:
abort(401, message='Authentication is required for this endpoint')
......@@ -180,24 +178,7 @@ ns = api.namespace(
description='Authentication related endpoints.')
user_model = api.model('User', {
'user_id': fields.String(description='The users UUID.'),
'name': fields.String('The publically visible user name.'),
'first_name': fields.String(description='The user\'s first name'),
'last_name': fields.String(description='The user\'s last name'),
'email': fields.String(description='Guess what, the user\'s email'),
'affiliation': fields.Nested(model=api.model('Affiliation', {
'name': fields.String(description='The name of the affiliation', default='not given'),
'address': fields.String(description='The address of the affiliation', default='not given')})),
'password': fields.String(description='The bcrypt 2y-indented password for initial and changed password'),
'token': fields.String(
description='The access token that authenticates the user with the API. '
'User the HTTP header "X-Token" to provide it in API requests.'),
'created': RFC3339DateTime(description='The create date for the user.')
})
auth_model = api.model('Auth', {
'user': fields.Nested(user_model, skip_none=True, description='The authenticated user info'),
'access_token': fields.String(description='The OIDC access token'),
'upload_token': fields.String(description='A short token for human readable upload URLs'),
'signature_token': fields.String(description='A short term token to sign URLs')
......@@ -211,13 +192,12 @@ class AuthResource(Resource):
@authenticate(required=True, basic=True)
def get(self):
"""
Provides user and authentication information. This endpoint requires authentification.
Provides authentication information. This endpoint requires authentification.
Like all endpoints the OIDC access token based authentification. In additional,
basic HTTP authentification can be used. This allows to login and acquire an
access token.
The response contains information about the authentificated user; a
a short (10s) term JWT token that can be used to sign
The response contains a short (10s) term JWT token that can be used to sign
URLs with a ``signature_token`` query parameter, e.g. for file downloads on the
raw or archive api endpoints; a short ``upload_token`` that is used in
``curl`` command line based uploads; and the OIDC JWT access token.
......@@ -231,7 +211,6 @@ class AuthResource(Resource):
try:
return {
'user': infrastructure.keycloak.get_user(g.user.user_id),
'upload_token': generate_upload_token(g.user),
'signature_token': signature_token(),
'access_token': infrastructure.keycloak.access_token
......
......@@ -319,7 +319,7 @@ class UploadListResource(Resource):
upload.process_upload()
logger.info('initiated processing')
if bool(request.args.get('curl', False)):
if bool(request.args.get('token', False)):
raise DisableMarshalling(
'''
Thanks for uploading your data to nomad.
......
......@@ -27,39 +27,32 @@ class User:
# TODO legacy ids
"""
def __init__(
self, email, user_id=None, name=None, first_name='', last_name='', affiliation=None,
created: datetime.datetime = None, token=None, **kwargs):
self, user_id: str, email: str, name: str = None, first_name: str = None,
last_name: str = None, affiliation: str = None, affiliation_address: str = None,
created: datetime.datetime = None):
self.user_id = kwargs.get('id', kwargs.get('sub', user_id))
self.email = email
assert self.user_id is not None, 'Users must have a unique id'
assert email is not None, 'Users must have an email'
assert user_id is not None, 'Users must have a unique id'
self.first_name = kwargs.get('given_name', first_name)
self.last_name = kwargs.get('family_name', last_name)
name = kwargs.get('username', name)
created_timestamp = kwargs.get('createdTimestamp', None)
name = '' if name is None else name.strip()
self.first_name = '' if first_name is None else first_name.strip()
self.last_name = '' if last_name is None else last_name.strip()
self.user_id = user_id
self.email = email
if len(self.last_name) > 0 and len(self.first_name) > 0:
self.name = '%s, %s' % (self.last_name, self.first_name)
self.name = '%s %s' % (self.first_name, self.last_name)
elif len(name) != 0:
self.name = name
elif len(self.last_name) != 0:
self.name = self.last_name
elif len(self.first_name) != 0:
self.name = self.first_name
elif name is not None:
self.name = name
else:
self.name = 'unnamed user'
if created is not None:
self.created = None
elif created_timestamp is not None:
self.created = datetime.datetime.fromtimestamp(created_timestamp)
else:
self.created = None
# TODO affliation
self.created = created
self.affiliation = affiliation
self.affiliation_address = affiliation_address
@staticmethod
def get(*args, **kwargs) -> 'User':
......
......@@ -190,10 +190,10 @@ class Keycloak():
from nomad import datamodel
g.user = datamodel.User(
user_id=payload.get('sub', None),
name=payload.get('name', None),
email=payload.get('email', None),
name=payload.get('name', None),
first_name=payload.get('given_name', None),
family_name=payload.get('family_name', None))
last_name=payload.get('family_name', None))
return None
......@@ -232,13 +232,15 @@ class Keycloak():
logger.error('Could not retrieve user from keycloak', exc_info=e)
raise e
kwargs = {key: value[0] for key, value in keycloak_user.get('attributes', {}).items()}
return datamodel.User(
user_id=keycloak_user['id'],
email=keycloak_user['email'],
name=keycloak_user.get('username', None),
first_name=keycloak_user.get('firstName', None),
family_name=keycloak_user.get('lastName', None),
created=datetime.fromtimestamp(keycloak_user['createdTimestamp'] / 1000))
last_name=keycloak_user.get('lastName', None),
created=datetime.fromtimestamp(keycloak_user['createdTimestamp'] / 1000),
**kwargs)
@property
def _admin_client(self):
......
......@@ -198,7 +198,7 @@ class KeycloakMock:
if 'Authorization' in request.headers and request.headers['Authorization'].startswith('Bearer '):
user_id = request.headers['Authorization'].split(None, 1)[1].strip()
g.oidc_access_token = user_id
return User(**test_users[user_id])
g.user = User(**test_users[user_id])
def get_user(self, user_id=None, email=None):
if user_id is not None:
......
......@@ -26,7 +26,7 @@ import base64
from nomad.api.app import rfc3339DateTime
from nomad.api.auth import generate_upload_token
from nomad import search, parsing, files, config, utils
from nomad import search, parsing, files, config, utils, infrastructure
from nomad.files import UploadFiles, PublicUploadFiles
from nomad.processing import Upload, Calc, SUCCESS
from nomad.datamodel import UploadWithMetadata, CalcWithMetadata, User
......@@ -92,6 +92,16 @@ class TestKeycloak:
rv = client.get('/auth/', headers=auth_headers)
assert rv.status_code == 200
def test_get_user(self, keycloak):
user = infrastructure.keycloak.get_user(email='sheldon.cooper@nomad-coe.eu')
assert user.email is not None
assert user.name == 'Sheldon Cooper'
assert user.first_name == 'Sheldon'
assert user.last_name == 'Cooper'
assert user.created is not None
assert user.affiliation is not None
assert user.affiliation_address is not None
class TestAuth:
def test_auth_wo_credentials(self, client, no_warn):
......@@ -104,11 +114,7 @@ class TestAuth:
self.assert_auth(client, json.loads(rv.data))
def assert_auth(self, client, auth):
assert 'user' in auth
user = auth['user']
for key in ['first_name', 'last_name', 'email', 'name', 'user_id']:
assert key in user
assert 'user' not in auth
assert 'access_token' in auth
assert 'upload_token' in auth
assert 'signature_token' in auth
......@@ -241,12 +247,12 @@ class TestUploads:
rv = client.get('/uploads/123456789012123456789012', headers=test_user_auth)
assert rv.status_code == 404
def test_put_upload_token(self, client, non_empty_example_upload, test_user, no_warn):
def test_put_upload_token(self, client, non_empty_example_upload, test_user):
url = '/uploads/?token=%s&local_path=%s&name=test_upload' % (
generate_upload_token(test_user), non_empty_example_upload)
rv = client.put(url)
assert rv.status_code == 200
self.assert_upload(rv.data, name='test_upload')
assert 'Thanks for uploading' in rv.data.decode('utf-8')
@pytest.mark.parametrize('mode', ['multipart', 'stream', 'local_path'])
@pytest.mark.parametrize('name', [None, 'test_name'])
......@@ -692,7 +698,7 @@ class TestRepo():
(1, 'only_atoms', ['Br', 'K', 'Si']),
(1, 'only_atoms', ['Br', 'Si', 'K']),
(1, 'comment', 'specific'),
(1, 'authors', 'Hofstadter, Leonard'),
(1, 'authors', 'Leonard Hofstadter'),
(2, 'files', 'test/mainfile.txt'),
(2, 'paths', 'mainfile.txt'),
(2, 'paths', 'test'),
......@@ -819,7 +825,7 @@ class TestRepo():
(0, 'system', 'atom'),
(1, 'atoms', 'Br'),
(1, 'atoms', 'Fe'),
(1, 'authors', 'Hofstadter, Leonard'),
(1, 'authors', 'Leonard Hofstadter'),
(2, 'files', 'test/mainfile.txt'),
(0, 'quantities', 'dos')
])
......@@ -969,7 +975,7 @@ class TestRaw(UploadFilesBasedTests):
@pytest.mark.parametrize('query_params', [
{'atoms': 'Si'},
{'authors': 'Cooper, Sheldon'}
{'authors': 'Sheldon Cooper'}
])
def test_raw_files_from_query(self, client, processeds, test_user_auth, query_params):
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment