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

Refactor/security split 20220303 #480

Open
wants to merge 36 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f40e4a7
starting to address issue #478
cschu Oct 29, 2021
b58787d
version bump -> 1.1.0
cschu Oct 29, 2021
0ead5ef
ConnectionRefusedError are now caught when trying to send email
cschu Oct 29, 2021
4af403f
minor fix
cschu Oct 29, 2021
8b74fcf
pleasing flake8
cschu Oct 29, 2021
a8ffdd9
Merge branch 'develop' into feature/login_tracking_478
cschu Nov 5, 2021
d7af75e
implemented suggestions from code review
cschu Jan 13, 2022
56613b2
Merge branch 'feature/login_tracking_478' of https://github.com/ghga-…
cschu Jan 13, 2022
5d9c8e4
added get_db() to FixtureManager in order to be able to check databas…
cschu Jan 14, 2022
a1bc4da
added db-flush after dealing with invalid login attempt, renamed secu…
cschu Jan 14, 2022
d9a1327
added login_tracking integration test
cschu Jan 14, 2022
09cf5bb
Merge branch 'develop' into feature/login_tracking_478
cschu Feb 1, 2022
a57fe63
refactored appsettings:put -> settings.set_setting
cschu Feb 2, 2022
21e8474
revised revision message
cschu Feb 2, 2022
0c2533a
cosmetics
cschu Feb 2, 2022
0f0f636
fixed: date_value -> time_value
cschu Feb 3, 2022
17ba902
added set_application_setting() function to BaseIntegrationTest
cschu Feb 3, 2022
c7c454f
refactored test to use BaseIntegrationTest.set_application_setting()
cschu Feb 3, 2022
f4f20a9
removed get_db() function
cschu Feb 3, 2022
973eac4
pleasing flake8
cschu Feb 3, 2022
cf0a5ea
removed unneccessary list-comprehension
cschu Feb 3, 2022
9ec9b46
Refactor login attempts deletion
lkuchenb Feb 17, 2022
3da759f
Apply suggestions from code review
cschu Feb 17, 2022
ca0a62d
pleasing flake8
cschu Feb 17, 2022
218500a
pleasing mypy
cschu Feb 17, 2022
e2fb375
added failed login attempt tracking to expired apikey and group-misma…
cschu Feb 18, 2022
7bf43bf
refactor: revalidate_user now uses new transaction for login attempt …
cschu Feb 28, 2022
95833eb
added test for apikey-based login tracking
cschu Feb 28, 2022
5b560ff
Merge branch 'develop' into feature/login_tracking_478
cschu Feb 28, 2022
6597d01
minor: whitespace
cschu Feb 28, 2022
475753b
corrected down_revision
cschu Feb 28, 2022
0afd7aa
added missing comma
cschu Feb 28, 2022
aa5cdde
added missing comma
cschu Feb 28, 2022
84e54b1
remedied remaining issues from 7bf43bf
cschu Mar 3, 2022
8c30188
split password/token-related security functions into security.pwdz an…
cschu Mar 4, 2022
fb4a31b
pleasing flake8
cschu Mar 4, 2022
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
37 changes: 37 additions & 0 deletions datameta/alembic/versions/20211029_98911b881230.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""added failed_login_attempt tracking

Revision ID: 98911b881230
Revises: 7fdc829db18d
Create Date: 2021-10-29 15:01:29.844727

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '98911b881230'
down_revision = '6a9f0bec38e6'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'loginattempts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('timestamp', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_loginattempts_user_id_users')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_loginattempts')),
sa.UniqueConstraint('uuid', name=op.f('uq_loginattempts_uuid'))
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('loginattempts')
# ### end Alembic commands ###
6 changes: 3 additions & 3 deletions datameta/api/apikeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from typing import Optional, List
from .. import models
from .. import security
from ..security import authz
from ..security import authz, tokenz
from pyramid.httpexceptions import HTTPOk, HTTPUnauthorized, HTTPForbidden
from ..resource import resource_by_id, get_identifier
from ..errors import get_validation_error
Expand Down Expand Up @@ -95,8 +95,8 @@ def generate_api_key(request: Request, user: models.User, label: str, expires: O
"""
Generate API Token and store unsalted hash in db.
"""
token = security.generate_token()
token_hash = security.hash_token(token)
token = tokenz.generate_token()
token_hash = tokenz.hash_token(token)

db = request.dbsession
apikey = models.ApiKey(
Expand Down
43 changes: 5 additions & 38 deletions datameta/api/appsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
from .. import resource, security, errors
from ..security import authz
from ..resource import resource_by_id
from ..settings import get_setting_value_type
import datetime
from ..settings import get_setting_value_type, set_setting, SettingUpdateError


@dataclass
Expand Down Expand Up @@ -89,43 +88,11 @@ def put(request: Request):
raise HTTPForbidden()

target_setting = resource_by_id(db, ApplicationSetting, settings_id)

temp, value_type = get_setting_value_type(target_setting)
value = request.openapi_validated.body["value"]

if value_type == 'int':
try:
value_int = int(value)
except ValueError:
raise errors.get_validation_error(["You have to provide an integer."])

target_setting.int_value = value_int

elif value_type == 'string':
target_setting.str_value = value

elif value_type == 'float':
try:
value_float = float(value)
except ValueError:
raise errors.get_validation_error(["You have to provide a float."])

target_setting.float_value = value_float

elif value_type == 'date':
try:
datetime.datetime.strptime(value, "%Y-%m-%d")
except ValueError:
raise errors.get_validation_error(["The date value has to specified in the form '%Y-%m-%d'."])

target_setting.date_value = value

elif value_type == 'time':
try:
datetime.datetime.strptime(value, "%H:%M:%S")
except ValueError:
raise errors.get_validation_error(["The time value has to specified in the form '%H:%M:%S'."])

target_setting.date_value = value
try:
set_setting(db, target_setting.key, value)
except SettingUpdateError as e:
raise errors.get_validation_error([str(e)])

return HTTPNoContent()
3 changes: 2 additions & 1 deletion datameta/api/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pyramid.response import FileResponse
from datetime import datetime
from .. import security, models, storage
from ..security import tokenz
from ..resource import get_identifier
from .files import access_file_by_user
from sqlalchemy import and_
Expand Down Expand Up @@ -75,7 +76,7 @@ def download_by_token(request: Request) -> HTTPOk:
Usage: /download/{download_token}
"""
token = request.matchdict['token']
hashed_token = security.hash_token(token)
hashed_token = tokenz.hash_token(token)

# get download token from db
db = request.dbsession
Expand Down
10 changes: 5 additions & 5 deletions datameta/api/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from pyramid.view import view_config

from .. import security, errors
from ..security import authz
from ..security import authz, tokenz, pwdz
from ..models import User
from datetime import datetime

Expand All @@ -41,7 +41,7 @@ def put(request):
token = None
if request_id == '0':
# Try to find the token
token = security.get_password_reset_token(db, request_credential)
token = tokenz.get_password_reset_token(db, request_credential)
if token is None:
raise HTTPNotFound() # 404 Token not found

Expand All @@ -64,16 +64,16 @@ def put(request):
raise HTTPForbidden() # 403 User ID not found, hidden from the user intentionally

# Only changing the user's own password is allowed
if not authz.update_user_password(auth_user, target_user) or not security.check_password_by_hash(request_credential, auth_user.pwhash):
if not authz.update_user_password(auth_user, target_user) or not pwdz.check_password_by_hash(request_credential, auth_user.pwhash):
raise HTTPForbidden() # 403 Not authorized to change this user's password

# Verify the password quality
error = security.verify_password(db, auth_user.id, request_newPassword)
error = pwdz.verify_password(db, auth_user.id, request_newPassword)
if error:
raise errors.get_validation_error([error])

# Set the new password
auth_user.pwhash = security.register_password(db, auth_user.id, request_newPassword)
auth_user.pwhash = pwdz.register_password(db, auth_user.id, request_newPassword)

# Delete the password token if any
if token:
Expand Down
3 changes: 2 additions & 1 deletion datameta/api/ui/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ...models import User, Group, RegRequest
from ...settings import get_setting
from ... import email, security, siteid, errors
from ...security import tokenz
from ...resource import resource_by_id, get_identifier
from sqlalchemy.exc import IntegrityError

Expand Down Expand Up @@ -98,7 +99,7 @@ def v_admin_put_request(request):
db.delete(reg_req)

# Obtain a new token
_, clear_token = security.get_new_password_reset_token(db, new_user)
_, clear_token = tokenz.get_new_password_reset_token(db, new_user)

# Generate the token url
token_url = request.route_url('setpass', token = clear_token)
Expand Down
5 changes: 3 additions & 2 deletions datameta/api/ui/forgot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

from pyramid.view import view_config

from ... import security, email
from ... import email
from ...security import tokenz
from ...settings import get_setting

import re
Expand Down Expand Up @@ -51,7 +52,7 @@ def v_forgot_api(request):
return { 'success' : False, 'error' : 'MALFORMED_EMAIL' }

try:
db_token_obj, clear_token = security.get_new_password_reset_token_from_email(db, req_email)
db_token_obj, clear_token = tokenz.get_new_password_reset_token_from_email(db, req_email)
except KeyError:
# User not found
log.debug(f"DURING RECOVERY TOKEN REQUEST: USER COULD NOT BE RESOLVED FROM EMAIL: {req_email}")
Expand Down
5 changes: 3 additions & 2 deletions datameta/api/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
import webob
import logging
from datetime import datetime
from .. import resource, models, storage, security
from .. import resource, models, storage
from ..security import tokenz

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,7 +54,7 @@ def post(request) -> HTTPNoContent:
db_file = resource.resource_by_id(db, models.File, req_file_id)

# Validate access token match
db_file = db_file if db_file and req_token and db_file.upload_expires > datetime.now() and db_file.access_token == security.hash_token(req_token) else None
db_file = db_file if db_file and req_token and db_file.upload_expires > datetime.now() and db_file.access_token == tokenz.hash_token(req_token) else None

if db_file is None: # Includes token mismatch
raise HTTPNotFound(json=None)
Expand Down
4 changes: 3 additions & 1 deletion datameta/defaults/appsettings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ template_welcome_token:

Best regards,
The Support Team
},

subject_reject:
str_value : Your registration request was rejected
Expand Down Expand Up @@ -85,6 +84,9 @@ logo_html:
user_agreement:
str_value: ""

security_max_failed_login_attempts:
int_value: 5

security_password_minimum_length:
int_value: 10

Expand Down
3 changes: 2 additions & 1 deletion datameta/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
DownloadToken,
Service,
ServiceExecution,
UsedPassword
LoginAttempt,
UsedPassword,
)

# run configure_mappers after defining all of the models to ensure
Expand Down
11 changes: 11 additions & 0 deletions datameta/models/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ class User(Base):
services = relationship('Service', secondary=user_service_table, back_populates='users')
service_executions = relationship('ServiceExecution', back_populates='user')
used_passwords = relationship('UsedPassword', back_populates='user')
login_attempts = relationship("LoginAttempt", back_populates='user', cascade="all, delete-orphan")


class LoginAttempt(Base):
__tablename__ = 'loginattempts'
id = Column(Integer, primary_key=True)
uuid = Column(UUID(as_uuid=True), unique=True, default=uuid.uuid4, nullable=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
timestamp = Column(DateTime, nullable=False)
# Relationships
user = relationship('User', back_populates='login_attempts')


class ApiKey(Base):
Expand Down
3 changes: 2 additions & 1 deletion datameta/scripts/initialize_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from pyramid.paster import bootstrap, setup_logging
from sqlalchemy.exc import OperationalError

from ..security import register_password, hash_token
from ..security.pwdz import register_password
from ..security.tokenz import hash_token
from ..models import User, Group, MetaDatum, DateTimeMode, ApiKey


Expand Down
Loading