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

Release 4.1.0 #4098

Draft
wants to merge 42 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3cae6eb
Increase exporter readiness/liveness probe timeout
tillprochaska Nov 11, 2024
7855df3
Bump ingest-file version
tillprochaska Nov 11, 2024
c65249c
Fix UI message extraction
tillprochaska Nov 11, 2024
f86739d
Update translations
tillprochaska Nov 11, 2024
3cffdd2
Bump version: 4.0.1 → 4.1.0-rc1
tillprochaska Nov 11, 2024
6a28fcf
Do not handle 401/auth errors in router (#4000)
tillprochaska Nov 19, 2024
9b648bb
Bump version: 4.1.0-rc1 → 4.1.0-rc2
tillprochaska Nov 20, 2024
2cad26c
servicelayer 1.23.2
stchris Nov 23, 2024
a270e96
Bump version: 4.1.0-rc2 → 4.1.0-rc3
stchris Nov 23, 2024
796899c
Hash API keys (#3842)
tillprochaska Dec 10, 2024
ebde8d2
Bump version: 4.1.0-rc3 → 4.1.0-rc4
tillprochaska Dec 10, 2024
f45f9a3
bugfix: Run batch indexing on index workers only
stchris Dec 13, 2024
28d8cbc
Use servicelayer 1.23.3-rc1
stchris Dec 13, 2024
2ff613d
Bump version: 4.0.2 → 4.0.3-rc1
stchris Dec 13, 2024
72c53fe
servicelayer 1.23.3-rc2
stchris Dec 17, 2024
bce62c0
Bump version: 4.0.3-rc1 → 4.0.3-rc2
stchris Dec 17, 2024
234e17a
servicelayer 1.23.3-rc3
stchris Dec 18, 2024
1111d0d
Bump version: 4.0.3-rc2 → 4.0.3-rc3
stchris Dec 18, 2024
78f57ad
Bump servicelayer to 1.23.3-rc4
catileptic Dec 18, 2024
c2de577
Bump version: 4.0.3-rc3 → 4.0.3-rc4
catileptic Dec 18, 2024
61ecb4f
Bump servicelayer to 1.23.3-rc5
catileptic Dec 19, 2024
a59ad5d
Bump version: 4.0.3-rc4 → 4.0.3-rc5
stchris Dec 19, 2024
0ef3b84
servicelayer 1.23.3-rc6
stchris Jan 6, 2025
96084f3
Bump version: 4.0.3-rc5 → 4.0.3-rc6
stchris Jan 6, 2025
6e4828e
servicelayer 1.23.3-rc7 and sentry-sdk 2.19.2
stchris Jan 7, 2025
43a4169
Bump version: 4.0.3-rc6 → 4.0.3-rc7
stchris Jan 7, 2025
a3030a1
bugfix: prevent double sentry_sdk init
stchris Jan 7, 2025
9305ace
Bump version: 4.0.3-rc7 → 4.0.3-rc8
stchris Jan 7, 2025
dfe636f
feat: distinct sentry init for api app
stchris Jan 8, 2025
bdd8f26
Bump version: 4.0.3-rc8 → 4.0.3-rc9
stchris Jan 8, 2025
44e458a
Merge branch 'release/4.1.0' into release/4.0.3
stchris Jan 10, 2025
c820d5e
Merge pull request #4097 from alephdata/release/4.0.3
stchris Jan 10, 2025
a50a874
servicelayer 1.23.3
stchris Jan 10, 2025
e2bb30a
Bump version: 4.1.0-rc4 → 4.1.0-rc5
stchris Jan 10, 2025
8f23001
Add additional validation checks (#4104)
tillprochaska Jan 20, 2025
340d221
Do not delete API key to enable rollbacks to previous version.
tillprochaska Jan 20, 2025
d9bbd6f
Merge pull request #4105 from alephdata/allow-rollbacks-api-keys
stchris Jan 20, 2025
9a2ccfd
Bump version: 4.1.0-rc5 → 4.1.0-rc6
stchris Jan 20, 2025
dc0b5e7
bugfix: reenable email preview
stchris Jan 22, 2025
bb068f5
Bump version: 4.1.0-rc6 → 4.1.0-rc7
stchris Jan 22, 2025
6349abd
servicelayer 1.24.0-rc1
stchris Feb 4, 2025
a1e285f
Bump version: 4.1.0-rc7 → 4.1.0-rc8
stchris Feb 4, 2025
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
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.0.1
current_version = 4.1.0-rc8
tag_name = {new_version}
commit = True
tag = True
Expand Down
16 changes: 0 additions & 16 deletions aleph/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,12 @@
from werkzeug.local import LocalProxy
from werkzeug.middleware.profiler import ProfilerMiddleware

from aleph import __version__ as aleph_version
from aleph.settings import SETTINGS
from aleph.cache import Cache
from aleph.oauth import configure_oauth
from aleph.util import LoggingTransport
from aleph.metrics.flask import PrometheusExtension

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration


NONE = "'none'"
log = logging.getLogger(__name__)
Expand Down Expand Up @@ -67,18 +63,6 @@ def create_app(config=None):
config = {}

configure_logging(level=logging.DEBUG)

if SETTINGS.SENTRY_DSN:
sentry_sdk.init(
dsn=SETTINGS.SENTRY_DSN,
integrations=[
FlaskIntegration(),
],
traces_sample_rate=0,
release=aleph_version,
environment=SETTINGS.SENTRY_ENVIRONMENT,
send_default_pii=False,
)
app = Flask("aleph")
app.config.from_object(SETTINGS)
app.config.update(config)
Expand Down
32 changes: 27 additions & 5 deletions aleph/logic/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from aleph.model.common import make_token
from aleph.logic.mail import email_role
from aleph.logic.roles import update_role
from aleph.logic.util import ui_url
from aleph.logic.util import ui_url, hash_api_key

# Number of days after which API keys expire
API_KEY_EXPIRATION_DAYS = 90
Expand All @@ -29,15 +29,16 @@ def generate_user_api_key(role):
email_role(role, subject, html=html, plain=plain)

now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
role.api_key = make_token()
api_key = make_token()
role.api_key_digest = hash_api_key(api_key)
role.api_key_expires_at = now + datetime.timedelta(days=API_KEY_EXPIRATION_DAYS)
role.api_key_expiration_notification_sent = None

db.session.add(role)
db.session.commit()
update_role(role)

return role.api_key
return api_key


def send_api_key_expiration_notifications():
Expand Down Expand Up @@ -70,7 +71,7 @@ def _send_api_key_expiration_notification(
query = query.where(
and_(
and_(
Role.api_key != None, # noqa: E711
Role.api_key_digest != None, # noqa: E711
func.date(Role.api_key_expires_at) <= threshold,
),
or_(
Expand Down Expand Up @@ -104,10 +105,31 @@ def reset_api_key_expiration():
query = query.yield_per(500)
query = query.where(
and_(
Role.api_key != None, # noqa: E711
Role.api_key_digest != None, # noqa: E711
Role.api_key_expires_at == None, # noqa: E711
)
)

query.update({Role.api_key_expires_at: expires_at})
db.session.commit()


def hash_plaintext_api_keys():
query = Role.all_users()
query = query.yield_per(250)
query = query.where(
and_(
Role.api_key != None, # noqa: E711
Role.api_key_digest == None, # noqa: E711
)
)

results = db.session.execute(query).scalars()

for index, partition in enumerate(results.partitions()):
for role in partition:
role.api_key_digest = hash_api_key(role.api_key)
db.session.add(role)
log.info(f"Hashing API key: {role}")
log.info(f"Comitting partition {index}")
db.session.commit()
9 changes: 9 additions & 0 deletions aleph/logic/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import jwt
import hashlib
from normality import ascii_text
from urllib.parse import urlencode, urljoin
from datetime import datetime, timedelta
Expand Down Expand Up @@ -58,3 +59,11 @@ def archive_token(token):
token = jwt.decode(token, key=SETTINGS.SECRET_KEY, algorithms=DECODE, verify=True)
expire = datetime.utcfromtimestamp(token["exp"])
return token.get("c"), token.get("f"), token.get("m"), expire


def hash_api_key(api_key):
if api_key is None:
return None

digest = hashlib.sha256(api_key.encode("utf-8")).hexdigest()
return f"sha256${digest}"
11 changes: 10 additions & 1 deletion aleph/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
from aleph.queues import get_status, cancel_queue
from aleph.queues import get_active_dataset_status
from aleph.index.admin import delete_index
from aleph.logic.api_keys import reset_api_key_expiration as _reset_api_key_expiration
from aleph.logic.api_keys import (
reset_api_key_expiration as _reset_api_key_expiration,
hash_plaintext_api_keys as _hash_plaintext_api_keys,
)
from aleph.index.entities import iter_proxies
from aleph.index.util import AlephOperationalException
from aleph.logic.collections import create_collection, update_collection
Expand Down Expand Up @@ -573,3 +576,9 @@ def evilshit():
def reset_api_key_expiration():
"""Reset the expiration date of all legacy, non-expiring API keys."""
_reset_api_key_expiration()


@cli.command()
def hash_plaintext_api_keys():
"""Hash legacy plaintext API keys."""
_hash_plaintext_api_keys()
28 changes: 28 additions & 0 deletions aleph/migrate/versions/31e24765dee3_add_api_key_digest_column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Add api_key_digest column

Revision ID: 31e24765dee3
Revises: d46fc882ec6b
Create Date: 2024-07-04 11:07:19.915782

"""

# revision identifiers, used by Alembic.
revision = "31e24765dee3"
down_revision = "d46fc882ec6b"

from alembic import op
import sqlalchemy as sa


def upgrade():
op.add_column("role", sa.Column("api_key_digest", sa.Unicode()))
op.create_index(
index_name="ix_role_api_key_digest",
table_name="role",
columns=["api_key_digest"],
unique=True,
)


def downgrade():
op.drop_column("role", "api_key_digest")
21 changes: 16 additions & 5 deletions aleph/model/role.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import logging
from datetime import datetime, timezone
from normality import stringify
from sqlalchemy import or_, not_, func
from sqlalchemy import and_, or_, not_, func
from itsdangerous import URLSafeTimedSerializer
from werkzeug.security import generate_password_hash, check_password_hash

from aleph.core import db
from aleph.settings import SETTINGS
from aleph.model.common import SoftDeleteModel, IdModel, query_like
from aleph.util import anonymize_email
from aleph.logic.util import hash_api_key

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -52,6 +53,7 @@ class Role(db.Model, IdModel, SoftDeleteModel):
email = db.Column(db.Unicode, nullable=True)
type = db.Column(db.Enum(*TYPES, name="role_type"), nullable=False)
api_key = db.Column(db.Unicode, nullable=True)
api_key_digest = db.Column(db.Unicode, nullable=True)
api_key_expires_at = db.Column(db.DateTime, nullable=True)
api_key_expiration_notification_sent = db.Column(db.Integer, nullable=True)
is_admin = db.Column(db.Boolean, nullable=False, default=False)
Expand All @@ -72,7 +74,7 @@ def has_password(self):

@property
def has_api_key(self):
return self.api_key is not None
return self.api_key_digest is not None

@property
def is_public(self):
Expand Down Expand Up @@ -195,12 +197,20 @@ def by_email(cls, email):

@classmethod
def by_api_key(cls, api_key):
if api_key is None:
if api_key is None or not len(api_key.strip()):
return None

q = cls.all()
q = q.filter_by(api_key=api_key)
utcnow = datetime.now(timezone.utc)

digest = hash_api_key(api_key)
q = q.filter(
and_(
cls.api_key_digest != None, # noqa: E711
cls.api_key_digest == digest,
)
)

utcnow = datetime.now(timezone.utc)
# TODO: Exclude API keys without expiration date after deadline
# See https://github.com/alephdata/aleph/issues/3729
q = q.filter(
Expand All @@ -212,6 +222,7 @@ def by_api_key(cls, api_key):

q = q.filter(cls.type == cls.USER)
q = q.filter(cls.is_blocked == False) # noqa

return q.first()

@classmethod
Expand Down
43 changes: 36 additions & 7 deletions aleph/tests/test_api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,35 @@
from aleph.logic.api_keys import (
generate_user_api_key,
send_api_key_expiration_notifications,
hash_plaintext_api_keys,
)
from aleph.logic.util import hash_api_key
from aleph.tests.util import TestCase


class ApiKeysTestCase(TestCase):
def test_generate_user_api_key(self):
role = self.create_user()
assert role.api_key is None
assert role.api_key_digest is None
assert role.api_key_expires_at is None

with time_machine.travel("2024-01-01T00:00:00Z"):
generate_user_api_key(role)
db.session.refresh(role)
assert role.api_key is not None
assert role.api_key_digest is not None
assert role.api_key_expires_at.date() == datetime.date(2024, 3, 31)

old_key = role.api_key
old_digest = role.api_key_digest

with time_machine.travel("2024-02-01T00:00:00Z"):
generate_user_api_key(role)
db.session.refresh(role)
assert role.api_key != old_key
assert role.api_key_digest != old_digest
assert role.api_key_expires_at.date() == datetime.date(2024, 5, 1)

def test_generate_user_api_key_notification(self):
role = self.create_user(email="[email protected]")
assert role.api_key is None
assert role.api_key_digest is None

with mail.record_messages() as outbox:
assert len(outbox) == 0
Expand Down Expand Up @@ -65,7 +67,7 @@ def test_send_api_key_expiration_notifications(self):
assert len(outbox) == 1
assert outbox[0].subject == "[Aleph] API key generated"

assert role.api_key is not None
assert role.api_key_digest is not None
assert role.api_key_expires_at.date() == datetime.date(2024, 3, 31)

assert len(outbox) == 1
Expand Down Expand Up @@ -122,7 +124,7 @@ def test_send_api_key_expiration_notifications(self):

def test_send_api_key_expiration_notifications_no_key(self):
role = self.create_user(email="[email protected]")
assert role.api_key is None
assert role.api_key_digest is None

with mail.record_messages() as outbox:
assert len(outbox) == 0
Expand Down Expand Up @@ -193,3 +195,30 @@ def test_send_api_key_expiration_notifications_regenerate(self):

assert outbox[4].subject == "[Aleph] Your API key will expire in 7 days"
assert outbox[5].subject == "[Aleph] Your API key has expired"

def test_hash_plaintext_api_keys(self):
user_1 = self.create_user(foreign_id="user_1", email="[email protected]")
user_1.api_key = "1234567890"
user_1.api_key_digest = None

user_2 = self.create_user(foreign_id="user_2", email="[email protected]")
user_2.api_key = None
user_2.api_key_digest = None

db.session.add_all([user_1, user_2])
db.session.commit()

hash_plaintext_api_keys()

db.session.refresh(user_1)

# Do not delete the plaintext API key to allow for version rollbacks.
# `api_key` column will be removed in the next version at which point all
# plaintext keys will be deleted.
assert user_1.api_key == "1234567890"

assert user_1.api_key_digest == hash_api_key("1234567890")

db.session.refresh(user_2)
assert user_2.api_key is None
assert user_2.api_key_digest is None
13 changes: 7 additions & 6 deletions aleph/tests/test_role_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from aleph.model import Role
from aleph.tests.factories.models import RoleFactory
from aleph.logic.roles import create_user, create_group
from aleph.logic.util import hash_api_key

from aleph.tests.util import TestCase

Expand Down Expand Up @@ -83,7 +84,7 @@ def test_remove_role(self):

def test_role_by_api_key(self):
role_ = self.create_user()
role_.api_key = "1234567890"
role_.api_key_digest = hash_api_key("1234567890")
db.session.add(role_)
db.session.commit()

Expand All @@ -93,7 +94,7 @@ def test_role_by_api_key(self):

def test_role_by_api_key_empty(self):
role_ = self.create_user()
assert role_.api_key is None
assert role_.api_key_digest is None

role = Role.by_api_key(None)
assert role is None
Expand All @@ -103,26 +104,26 @@ def test_role_by_api_key_empty(self):

def test_role_by_api_key_expired(self):
role_ = self.create_user()
role_.api_key = "1234567890"
role_.api_key_digest = hash_api_key("1234567890")
role_.api_key_expires_at = datetime.datetime(2024, 3, 31, 0, 0, 0)
db.session.add(role_)
db.session.commit()

with time_machine.travel("2024-03-30T23:59:59Z"):
print(role_.api_key_expires_at)
role = Role.by_api_key(role_.api_key)
role = Role.by_api_key("1234567890")
assert role is not None
assert role.id == role_.id

with time_machine.travel("2024-03-31T00:00:00Z"):
role = Role.by_api_key(role_.api_key)
role = Role.by_api_key("1234567890")
assert role is None

def test_role_by_api_key_legacy_without_expiration(self):
# Ensure that legacy API keys that were created without an expiration
# date continue to work.
role_ = self.create_user()
role_.api_key = "1234567890"
role_.api_key_digest = hash_api_key("1234567890")
role_.api_key_expires_at = None
db.session.add(role_)
db.session.commit()
Expand Down
Loading
Loading