Skip to content

Commit 04303db

Browse files
authored
Track last access of API client (#906)
1 parent d8caa4a commit 04303db

File tree

6 files changed

+68
-3
lines changed

6 files changed

+68
-3
lines changed

.secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,9 @@
163163
"filename": "tests/unit/test_auth.py",
164164
"hashed_secret": "b32224ba01e706962030343e7d3d964b9db7034f",
165165
"is_verified": false,
166-
"line_number": 198
166+
"line_number": 218
167167
}
168168
]
169169
},
170-
"generated_at": "2024-04-05T18:55:16Z"
170+
"generated_at": "2024-05-29T11:38:33Z"
171171
}

ctms/crud.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,11 @@ def get_active_api_client_ids(db: Session) -> List[str]:
578578
return [row.client_id for row in rows]
579579

580580

581+
def update_api_client_last_access(db: Session, api_client: ApiClient):
582+
api_client.last_access = func.now()
583+
db.add(api_client)
584+
585+
581586
def get_contacts_from_newsletter(dbsession, newsletter_name):
582587
entries = (
583588
dbsession.query(Newsletter)

ctms/dependencies.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from ctms.auth import get_subject_from_token
99
from ctms.config import Settings
10-
from ctms.crud import get_api_client_by_id
10+
from ctms.crud import get_api_client_by_id, update_api_client_last_access
1111
from ctms.database import SessionLocal
1212
from ctms.metrics import oauth2_scheme
1313
from ctms.schemas import ApiClientSchema
@@ -67,6 +67,9 @@ def get_api_client(
6767
log_context["auth_fail"] = "No client record"
6868
raise credentials_exception
6969

70+
# Track last usage of API client.
71+
update_api_client_last_access(db, api_client)
72+
7073
return api_client
7174

7275

ctms/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Boolean,
55
Column,
66
Date,
7+
DateTime,
78
ForeignKey,
89
Index,
910
Integer,
@@ -183,6 +184,7 @@ class ApiClient(Base, TimestampMixin):
183184
email = Column(String(255), nullable=False)
184185
enabled = Column(Boolean, default=True)
185186
hashed_secret = Column(String, nullable=False)
187+
last_access = Column(DateTime(timezone=True))
186188

187189

188190
class MozillaFoundationContact(Base, TimestampMixin):
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Add column to track API use
2+
3+
Revision ID: f2041a868dca
4+
Revises: cea70cf9d626
5+
Create Date: 2024-05-27 18:11:06.463499
6+
7+
"""
8+
# pylint: disable=no-member invalid-name
9+
# no-member is triggered by alembic.op, which has dynamically added functions
10+
# invalid-name is triggered by migration file names with a date prefix
11+
# invalid-name is triggered by top-level alembic constants like revision instead of REVISION
12+
13+
import sqlalchemy as sa
14+
from alembic import op
15+
16+
# revision identifiers, used by Alembic.
17+
revision = "f2041a868dca" # pragma: allowlist secret
18+
down_revision = "cea70cf9d626" # pragma: allowlist secret
19+
branch_labels = None
20+
depends_on = None
21+
22+
23+
def upgrade():
24+
# ### commands auto generated by Alembic - please adjust! ###
25+
op.add_column(
26+
"api_client",
27+
sa.Column("last_access", sa.DateTime(timezone=True), nullable=True),
28+
)
29+
# ### end Alembic commands ###
30+
31+
32+
def downgrade():
33+
# ### commands auto generated by Alembic - please adjust! ###
34+
op.drop_column("api_client", "last_access")
35+
# ### end Alembic commands ###

tests/unit/test_auth.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,26 @@ def test_get_ctms_with_token(
188188
assert resp.status_code == 200
189189

190190

191+
def test_successful_login_tracks_last_access(
192+
dbsession, example_contact, anon_client, test_token_settings, client_id_and_secret
193+
):
194+
client_id = client_id_and_secret[0]
195+
api_client = get_api_client_by_id(dbsession, client_id)
196+
before = api_client.last_access
197+
198+
token = create_access_token(
199+
{"sub": f"api_client:{client_id}"}, **test_token_settings
200+
)
201+
anon_client.get(
202+
f"/ctms/{example_contact.email.email_id}",
203+
headers={"Authorization": f"Bearer {token}"},
204+
)
205+
206+
dbsession.flush()
207+
after = get_api_client_by_id(dbsession, client_id).last_access
208+
assert before != after
209+
210+
191211
def test_get_ctms_with_invalid_token_fails(
192212
example_contact, anon_client, test_token_settings, client_id_and_secret
193213
):

0 commit comments

Comments
 (0)