Skip to content

Commit

Permalink
Merge 272ae80 into 738b73a
Browse files Browse the repository at this point in the history
  • Loading branch information
crungehottman committed Apr 15, 2024
2 parents 738b73a + 272ae80 commit 70d35cf
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 0 deletions.
43 changes: 43 additions & 0 deletions exodus_gw/routers/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@

import logging
import os
import re
from datetime import datetime
from uuid import uuid4

Expand Down Expand Up @@ -208,6 +209,7 @@ def update_publish_items(
env: Environment = deps.env,
db: Session = deps.db,
settings: Settings = deps.settings,
call_context: auth.CallContext = deps.call_context,
) -> dict[None, None]:
"""Add publish items to an existing publish object.
Expand Down Expand Up @@ -276,6 +278,47 @@ def update_publish_items(

db_publish.resolve_links(ln_items=resolvable)

# Prevent unauthorized users from publishing to restricted paths within
# a particular CDN environment.
#
# Some users only need to publish to certain paths. Allowing those
# users to publish to other paths increases the risk of conflicts
# between clients, or of accidents with a large impact.
username = str(
call_context.client.serviceAccountId
or call_context.user.internalUsername
)
path_restrictions = (settings.publish_paths.get(env.name) or {}).get(
username
) or [".*"]
path_patterns = [re.compile(path) for path in path_restrictions]

for i in items:
# Determine whether the client is authorized to publish to this URI.
authorized = False
for pattern in path_patterns:
if re.match(pattern, i.web_uri):
authorized = True
break
if not authorized:
# The URI did not match one of the client's permitted patterns in publish_paths.
LOG.error(
"User '%s' is not authorized to publish to path '%s'",
username,
i.web_uri,
extra={
"publish_id": publish_id,
"event": "publish",
"success": False,
},
)

raise HTTPException(
403,
detail="User '%s' is not authorized to publish to path '%s'"
% (username, i.web_uri),
)

# Convert the list into dict and update each dict with a publish_id.
# Each item's 'dirty' and 'updated' are refreshed to ensure it's
# written to DynamoDB with the current update, even if it was already
Expand Down
13 changes: 13 additions & 0 deletions exodus_gw/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ class Settings(BaseSettings):
for validation. E.g., "exodus-migration-md5": "^[0-9a-f]{32}$"
"""

publish_paths: dict[str, dict[str, list[str]]] = {}
"""A set of user or service accounts which are only authorized to publish to a
particular set of path(s) in a given CDN environment and the regex(es) describing
the paths to which the user or service account is authorized to publish. The user or
service account will be prevented from publishing to any paths that do not match the
defined regular expression(s).
E.g., '{"pre": {"fake-user":
["^(/content)?/origin/files/sha256/[0-f]{2}/[0-f]{64}/[^/]{1,300}$"]}}'
Any user or service account not included in this configuration is considered to have
unrestricted publish access (i.e., can publish to any path).
"""

log_config: dict[str, Any] = {
"version": 1,
"incremental": True,
Expand Down
100 changes: 100 additions & 0 deletions tests/routers/test_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -1357,3 +1357,103 @@ def test_get_publish_not_found(auth_header, fake_publish):
assert r.json() == {
"detail": "No publish found for ID %s" % fake_publish.id
}


def test_update_user_authorized_publish_paths(db, auth_header, monkeypatch):
"""Ensure that a user can successfully publish content to any paths to
which they are authorized to publish."""

monkeypatch.setenv(
"EXODUS_GW_PUBLISH_PATHS",
json.dumps(
{
"test": {
"fake-user": [
"^(/content)?/origin/files/sha256/[0-f]{2}/[0-f]{64}/[^/]{1,300}$",
"^/fake-path/[0-f]{64}/[^/]{1,300}$",
]
}
}
),
)

publish_id = "11224567-e89b-12d3-a456-426614174000"

publish = Publish(id=publish_id, env="test", state="PENDING")

db.add(publish)
db.commit()

with TestClient(app) as client:
# Try to add some items to it
r = client.put(
"/test/publish/%s" % publish_id,
json=[
{
"web_uri": "/content/origin/files/sha256/00/0044062dca731c0d5c24148722537e181d752ca8cda0097005f9268a51658b0a/test.rpm",
"object_key": "1" * 64,
"content_type": "application/octet-stream",
},
{
"web_uri": "/fake-path/0097062dca731c0d5c24148722537e181d752ca8cda0097005f9268a51658b0a/other-test.rpm",
"object_key": "2" * 64,
"content_type": "application/octet-stream",
},
],
headers=auth_header(roles=["test-publisher"]),
)

# It should have succeeded
assert r.status_code == 200


def test_update_user_unauthorized_publish_paths(db, auth_header, monkeypatch):
"""When a user is only authorized to publish to certain paths in a given
CDN environment, ensure that the user is prevented from publishing to any
paths to which they are unauthorized to publish."""

monkeypatch.setenv(
"EXODUS_GW_PUBLISH_PATHS",
json.dumps(
{
"test": {
"fake-user": [
"^(/content)?/origin/files/sha256/[0-f]{2}/[0-f]{64}/[^/]{1,300}$",
"/fake-path/[0-f]{64}/[^/]{1,300}$",
]
}
}
),
)

publish_id = "11224567-e89b-12d3-a456-426614174000"

publish = Publish(id=publish_id, env="test", state="PENDING")

db.add(publish)
db.commit()

with TestClient(app) as client:
# Try to add some items to it
r = client.put(
"/test/publish/%s" % publish_id,
json=[
{
"web_uri": "/content/origin/files/sha256/00/0044062dca731c0d5c24148722537e181d752ca8cda0097005f9268a51658b0a/test.rpm",
"object_key": "1" * 64,
"content_type": "application/octet-stream",
},
{
"web_uri": "/content/origin/uri1",
"object_key": "2" * 64,
"content_type": "application/octet-stream",
},
],
headers=auth_header(roles=["test-publisher"]),
)

# It should have failed
assert r.status_code == 403
assert r.json() == {
"detail": "User 'fake-user' is not authorized to publish to path '/content/origin/uri1'"
}

0 comments on commit 70d35cf

Please sign in to comment.