Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
nomad-lab
nomad-FAIR
Commits
1fb5ef2c
Commit
1fb5ef2c
authored
Feb 13, 2019
by
Markus Scheidgen
Browse files
Added create/update user to the api.
parent
48cf90b6
Changes
6
Hide whitespace changes
Inline
Side-by-side
.vscode/launch.json
View file @
1fb5ef2c
...
...
@@ -44,7 +44,7 @@
"cwd"
:
"${workspaceFolder}"
,
"program"
:
"${workspaceFolder}/.pyenv/bin/pytest"
,
"args"
:
[
"-sv"
,
"tests/test_api.py::Test
Uploads
::test_p
ost[tests/data/proc/empty.zip]
"
"-sv"
,
"tests/test_api.py::Test
Auth
::test_p
ut_user
"
]
},
{
...
...
nomad/api/auth.py
View file @
1fb5ef2c
...
...
@@ -135,6 +135,7 @@ user_model = api.model('User', {
'last_name'
:
fields
.
String
(
description
=
'The user
\'
s last name'
),
'email'
:
fields
.
String
(
description
=
'Guess what, the user
\'
s email'
),
'affiliation'
:
fields
.
String
(
description
=
'The user
\'
s affiliation'
),
'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.'
)
...
...
@@ -161,6 +162,54 @@ class UserResource(Resource):
401
,
message
=
'User not logged in, provide credentials via Basic HTTP authentication.'
)
@
api
.
doc
(
'create_user'
)
@
api
.
expect
(
user_model
)
@
api
.
marshal_with
(
user_model
,
skip_none
=
True
,
code
=
200
,
description
=
'User created'
)
@
login_really_required
def
put
(
self
):
"""
Creates a new user account. Currently only the admin user is allows. The
NOMAD-CoE repository GUI should be used to create user accounts for now.
Passwords have to be encrypted by the client with bcrypt and 2y indent.
"""
if
not
g
.
user
.
is_admin
:
abort
(
401
,
message
=
'Only the admin user can perform create user.'
)
data
=
request
.
get_json
()
if
data
is
None
:
data
=
{}
for
required_key
in
[
'last_name'
,
'first_name'
,
'password'
,
'email'
]:
if
required_key
not
in
data
:
abort
(
400
,
message
=
'The %s is missing'
%
required_key
)
user
=
coe_repo
.
User
.
create_user
(
email
=
data
[
'email'
],
password
=
data
.
get
(
'password'
,
None
),
crypted
=
True
,
first_name
=
data
[
'first_name'
],
last_name
=
data
[
'last_name'
],
affiliation
=
data
.
get
(
'affiliation'
,
None
))
return
user
,
200
@
api
.
doc
(
'update_user'
)
@
api
.
expect
(
user_model
)
@
api
.
marshal_with
(
user_model
,
skip_none
=
True
,
code
=
200
,
description
=
'User updated'
)
@
login_really_required
def
post
(
self
):
"""
Allows to edit the authenticated user and change his password. Password
have to be encrypted by the client with bcrypt and 2y indent.
"""
data
=
request
.
get_json
()
if
data
is
None
:
data
=
{}
if
'email'
in
data
:
abort
(
400
,
message
=
'Cannot change the users email.'
)
g
.
user
.
update
(
crypted
=
True
,
**
data
)
return
g
.
user
,
200
token_model
=
api
.
model
(
'Token'
,
{
'user'
:
fields
.
Nested
(
user_model
),
...
...
nomad/coe_repo/user.py
View file @
1fb5ef2c
...
...
@@ -13,9 +13,12 @@
# limitations under the License.
from
passlib.hash
import
bcrypt
from
sqlalchemy
import
Column
,
Integer
,
String
from
sqlalchemy
import
Column
,
Integer
,
String
,
ForeignKey
from
sqlalchemy.orm
import
relationship
import
datetime
import
jwt
import
random
import
string
from
nomad
import
infrastructure
,
config
,
utils
...
...
@@ -25,8 +28,9 @@ from .base import Base
class
Session
(
Base
):
# type: ignore
__tablename__
=
'sessions'
token
=
Column
(
String
,
primary_key
=
True
)
user_id
=
Column
(
String
)
token
=
Column
(
String
)
user_id
=
Column
(
String
,
ForeignKey
(
'users.user_id'
),
primary_key
=
True
)
user
=
relationship
(
'User'
)
class
LoginException
(
Exception
):
...
...
@@ -52,20 +56,51 @@ class User(Base): # type: ignore
affiliation
=
Column
(
String
)
password
=
Column
(
String
)
_token_chars
=
string
.
ascii_uppercase
+
string
.
ascii_lowercase
+
string
.
digits
def
__repr__
(
self
):
return
'<User(email="%s")>'
%
self
.
email
def
_hash_password
(
self
,
password
):
assert
False
,
'Login functions are done by the NOMAD-coe repository GUI'
# password_hash = bcrypt.encrypt(password, ident='2y')
# self.password = password_hash
@
staticmethod
def
create_user
(
email
:
str
,
password
:
str
,
crypted
:
bool
,
**
kwargs
):
repo_db
=
infrastructure
.
repository_db
repo_db
.
begin
()
try
:
user
=
User
(
email
=
email
,
**
kwargs
)
repo_db
.
add
(
user
)
user
.
set_password
(
password
,
crypted
)
# TODO this has to change, e.g. trade for JWTs
token
=
''
.
join
(
random
.
choices
(
User
.
_token_chars
,
k
=
64
))
repo_db
.
add
(
Session
(
token
=
token
,
user
=
user
))
repo_db
.
commit
()
return
user
except
Exception
as
e
:
repo_db
.
rollback
()
utils
.
get_logger
(
'__name__'
).
error
(
'could not create user'
,
email
=
email
,
exc_info
=
e
)
raise
e
def
update
(
self
,
crypted
:
bool
=
True
,
password
:
str
=
None
,
**
kwargs
):
repo_db
=
infrastructure
.
repository_db
repo_db
.
begin
()
try
:
if
password
is
not
None
:
self
.
set_password
(
password
,
crypted
=
crypted
)
for
key
in
kwargs
:
setattr
(
self
,
key
,
kwargs
.
get
(
key
))
repo_db
.
commit
()
except
Exception
as
e
:
repo_db
.
rollback
()
utils
.
get_logger
(
'__name__'
).
error
(
'could not edit user'
,
email
=
self
.
email
,
user_id
=
self
.
user_id
,
exc_info
=
e
)
raise
e
def
_verify_password
(
self
,
password
):
return
bcrypt
.
verify
(
password
,
self
.
password
)
def
_generate_auth_token
(
self
,
expiration
=
600
):
assert
False
,
'Login functions are done by the NOMAD-coe repository GUI'
@
staticmethod
def
from_user_id
(
user_id
)
->
'User'
:
return
infrastructure
.
repository_db
.
query
(
User
).
get
(
user_id
)
...
...
@@ -90,6 +125,20 @@ class User(Base): # type: ignore
config
.
services
.
api_secret
,
'HS256'
).
decode
(
'utf-8'
)
return
token
,
expires_at
def
set_password
(
self
,
password
:
str
,
crypted
:
bool
):
"""
Sets the users password. With ``crypted=True`` password is supposed to
be already bcrypted and 2y-indented.
"""
if
password
is
None
:
return
if
crypted
:
self
.
password
=
password
else
:
password_hash
=
bcrypt
.
encrypt
(
password
,
ident
=
'2y'
)
self
.
password
=
password_hash
@
property
def
token
(
self
):
return
self
.
get_auth_token
().
decode
(
'utf-8'
)
...
...
tests/conftest.py
View file @
1fb5ef2c
...
...
@@ -264,6 +264,8 @@ def postgres(postgres_infra):
""" Provides a clean coe repository db per function. Clears db before test. """
# do not wonder, this will not setback the id counters
postgres_infra
.
execute
(
'TRUNCATE uploads CASCADE;'
)
postgres_infra
.
execute
(
'DELETE FROM sessions WHERE user_id >= 4;'
)
postgres_infra
.
execute
(
'DELETE FROM users WHERE user_id >= 4;'
)
yield
postgres_infra
...
...
tests/test_api.py
View file @
1fb5ef2c
...
...
@@ -19,6 +19,7 @@ import base64
import
zipfile
import
io
import
inspect
from
passlib.hash
import
bcrypt
from
nomad
import
config
,
coe_repo
from
nomad.files
import
UploadFiles
,
PublicUploadFiles
...
...
@@ -112,7 +113,9 @@ class TestAuth:
def
test_get_user
(
self
,
client
,
test_user_auth
,
test_user
:
User
,
no_warn
):
rv
=
client
.
get
(
'/auth/user'
,
headers
=
test_user_auth
)
assert
rv
.
status_code
==
200
user
=
json
.
loads
(
rv
.
data
)
self
.
assert_user
(
client
,
json
.
loads
(
rv
.
data
))
def
assert_user
(
self
,
client
,
user
):
for
key
in
[
'first_name'
,
'last_name'
,
'email'
,
'token'
]:
assert
key
in
user
...
...
@@ -125,6 +128,49 @@ class TestAuth:
def
test_signature_token
(
self
,
test_user_signature_token
,
no_warn
):
assert
test_user_signature_token
is
not
None
def
test_put_user
(
self
,
client
,
postgres
,
admin_user_auth
):
rv
=
client
.
put
(
'/auth/user'
,
headers
=
admin_user_auth
,
content_type
=
'application/json'
,
data
=
json
.
dumps
(
dict
(
email
=
'test@email.com'
,
last_name
=
'Tester'
,
first_name
=
'Testi'
,
password
=
bcrypt
.
encrypt
(
'test_password'
,
ident
=
'2y'
))))
assert
rv
.
status_code
==
200
self
.
assert_user
(
client
,
json
.
loads
(
rv
.
data
))
def
test_put_user_admin_only
(
self
,
client
,
test_user_auth
):
rv
=
client
.
put
(
'/auth/user'
,
headers
=
test_user_auth
,
content_type
=
'application/json'
,
data
=
json
.
dumps
(
dict
(
email
=
'test@email.com'
,
last_name
=
'Tester'
,
first_name
=
'Testi'
,
password
=
bcrypt
.
encrypt
(
'test_password'
,
ident
=
'2y'
))))
assert
rv
.
status_code
==
401
def
test_put_user_required_field
(
self
,
client
,
admin_user_auth
):
rv
=
client
.
put
(
'/auth/user'
,
headers
=
admin_user_auth
,
content_type
=
'application/json'
,
data
=
json
.
dumps
(
dict
(
email
=
'test@email.com'
,
password
=
bcrypt
.
encrypt
(
'test_password'
,
ident
=
'2y'
))))
assert
rv
.
status_code
==
400
def
test_post_user
(
self
,
client
,
postgres
,
admin_user_auth
):
rv
=
client
.
put
(
'/auth/user'
,
headers
=
admin_user_auth
,
content_type
=
'application/json'
,
data
=
json
.
dumps
(
dict
(
email
=
'test@email.com'
,
last_name
=
'Tester'
,
first_name
=
'Testi'
,
password
=
bcrypt
.
encrypt
(
'test_password'
,
ident
=
'2y'
))))
assert
rv
.
status_code
==
200
user
=
json
.
loads
(
rv
.
data
)
rv
=
client
.
post
(
'/auth/user'
,
headers
=
{
'X-Token'
:
user
[
'token'
]},
content_type
=
'application/json'
,
data
=
json
.
dumps
(
dict
(
last_name
=
'Tester'
,
first_name
=
'Testi v.'
,
password
=
bcrypt
.
encrypt
(
'test_password_changed'
,
ident
=
'2y'
))))
assert
rv
.
status_code
==
200
self
.
assert_user
(
client
,
json
.
loads
(
rv
.
data
))
class
TestUploads
:
...
...
tests/test_coe_repo.py
View file @
1fb5ef2c
...
...
@@ -13,6 +13,7 @@
# limitations under the License.
import
pytest
from
passlib.hash
import
bcrypt
from
nomad.coe_repo
import
User
,
Calc
,
Upload
from
nomad
import
processing
,
parsing
,
datamodel
...
...
@@ -117,6 +118,20 @@ def test_add_upload_with_metadata(processed, example_user_metadata):
processed
.
upload_id
,
upload_with_metadata
)
@
pytest
.
mark
.
parametrize
(
'crypted'
,
[
True
,
False
])
def
test_create_user
(
postgres
,
crypted
):
password
=
bcrypt
.
encrypt
(
'test_password'
,
ident
=
'2y'
)
if
crypted
else
'test_password'
data
=
dict
(
email
=
'test@email.com'
,
last_name
=
'Teser'
,
first_name
=
'testi'
,
password
=
password
)
user
=
User
.
create_user
(
**
data
,
crypted
=
crypted
)
authenticated_user
=
User
.
verify_user_password
(
'test@email.com'
,
'test_password'
)
assert
authenticated_user
is
not
None
assert
user
.
user_id
==
authenticated_user
.
user_id
assert
user
.
get_auth_token
()
is
not
None
class
TestDataSets
:
@
pytest
.
fixture
(
scope
=
'function'
)
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment