Skip to content

Commit

Permalink
Add daily task to purge expired OIDC macaroons (#15463)
Browse files Browse the repository at this point in the history
* Add daily task to purge expired OIDC macaroons

* Add metric for number of OIDC-minted tokens deleted

* Delete OIDC macaroons that are at least 1 day old

* Add test case

---------

Co-authored-by: Dustin Ingram <di@users.noreply.github.com>
  • Loading branch information
facutuesca and di committed Feb 23, 2024
1 parent d5ce879 commit 675794b
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 2 deletions.
77 changes: 76 additions & 1 deletion tests/unit/oidc/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@

import pretend

from warehouse.oidc.tasks import compute_oidc_metrics
from warehouse.macaroons import caveats
from warehouse.macaroons.models import Macaroon
from warehouse.oidc.tasks import compute_oidc_metrics, delete_expired_oidc_macaroons

from ...common.db.oidc import GitHubPublisherFactory
from ...common.db.packaging import (
FileEventFactory,
FileFactory,
ProjectFactory,
ReleaseFactory,
UserFactory,
)


Expand Down Expand Up @@ -95,3 +98,75 @@ def test_compute_oidc_metrics(db_request, metrics):
"warehouse.oidc.publishers", 4, tags=["publisher:github_oidc_publishers"]
),
]


def test_delete_expired_oidc_macaroons(db_request, macaroon_service, metrics):
# We'll create 4 macaroons:
# - An OIDC macaroon with creation time of 1 day ago
# - An OIDC macaroon with creation time of 1 hour ago
# - An OIDC macaroon with creation time now
# - A non-OIDC macaroon with creation time of 1 day ago
# The task should only delete the first one

publisher = GitHubPublisherFactory.create()
claims = {"sha": "somesha", "ref": "someref"}
# Create an OIDC macaroon and set its creation time to 1 day ago
(_, old_oidc_macaroon) = macaroon_service.create_macaroon(
"fake location",
"fake description",
[
caveats.OIDCPublisher(oidc_publisher_id=str(publisher.id)),
],
oidc_publisher_id=publisher.id,
additional={"oidc": publisher.stored_claims(claims)},
)
old_oidc_macaroon.created -= datetime.timedelta(days=1)

# Create an OIDC macaroon and set its creation time to 1 hour ago
macaroon_service.create_macaroon(
"fake location",
"fake description",
[
caveats.OIDCPublisher(oidc_publisher_id=str(publisher.id)),
],
oidc_publisher_id=publisher.id,
additional={"oidc": publisher.stored_claims(claims)},
)
old_oidc_macaroon.created -= datetime.timedelta(hours=1)

# Create a normal OIDC macaroon
macaroon_service.create_macaroon(
"fake location",
"fake description",
[caveats.OIDCPublisher(oidc_publisher_id=str(publisher.id))],
oidc_publisher_id=publisher.id,
additional={"oidc": publisher.stored_claims(claims)},
)

# Create a non-OIDC macaroon and set its creation time to 1 day ago
user = UserFactory.create()
(_, non_oidc_macaroon) = macaroon_service.create_macaroon(
"fake location",
"fake description",
[caveats.RequestUser(user_id=str(user.id))],
user_id=user.id,
)
non_oidc_macaroon.created -= datetime.timedelta(days=1)

assert db_request.db.query(Macaroon).count() == 4

# The ID of the macaroon we expect to be deleted by the task
old_oidc_macaroon_id = old_oidc_macaroon.id

delete_expired_oidc_macaroons(db_request)
assert db_request.db.query(Macaroon).count() == 3
assert (
db_request.db.query(Macaroon)
.filter(Macaroon.id == old_oidc_macaroon_id)
.count()
== 0
)

assert metrics.gauge.calls == [
pretend.call("warehouse.oidc.expired_oidc_tokens_deleted", 1),
]
6 changes: 5 additions & 1 deletion warehouse/oidc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from warehouse.oidc.interfaces import IOIDCPublisherService
from warehouse.oidc.services import OIDCPublisherServiceFactory
from warehouse.oidc.tasks import compute_oidc_metrics
from warehouse.oidc.tasks import compute_oidc_metrics, delete_expired_oidc_macaroons
from warehouse.oidc.utils import (
ACTIVESTATE_OIDC_ISSUER_URL,
GITHUB_OIDC_ISSUER_URL,
Expand Down Expand Up @@ -78,3 +78,7 @@ def includeme(config):

# Compute OIDC metrics periodically
config.add_periodic_task(crontab(minute=0, hour="*"), compute_oidc_metrics)

# Daily purge expired OIDC-minted API tokens. These tokens are temporary in nature
# and expire after 15 minutes of creation.
config.add_periodic_task(crontab(minute=0, hour=6), delete_expired_oidc_macaroons)
28 changes: 28 additions & 0 deletions warehouse/oidc/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime, timedelta, timezone

from warehouse import tasks
from warehouse.macaroons.models import Macaroon
from warehouse.metrics import IMetricsService
from warehouse.oidc.models import OIDCPublisher
from warehouse.packaging.models import File, Project, Release
Expand Down Expand Up @@ -65,3 +68,28 @@ def compute_oidc_metrics(request):
.count(),
tags=[f"publisher:{discriminator}"],
)


@tasks.task(ignore_result=True, acks_late=True)
def delete_expired_oidc_macaroons(request):
"""
Purge all API tokens minted using OIDC Trusted Publishing with a creation time
more than 1 day ago. Since OIDC-minted macaroons expire 15 minutes after
creation, this task cleans up tokens that expired several hours ago and that
have accumulated since the last time this task was run.
"""
rows_deleted = (
request.db.query(Macaroon)
.filter(Macaroon.oidc_publisher_id.isnot(None))
.filter(
# The token has been created at more than 1 day ago
Macaroon.created + timedelta(days=1)
< datetime.now(tz=timezone.utc)
)
.delete(synchronize_session=False)
)
metrics = request.find_service(IMetricsService, context=None)
metrics.gauge(
"warehouse.oidc.expired_oidc_tokens_deleted",
rows_deleted,
)

0 comments on commit 675794b

Please sign in to comment.