Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 20 additions & 14 deletions backend/account_v2/custom_auth_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from account_v2.authentication_plugin_registry import AuthenticationPluginRegistry
from account_v2.authentication_service import AuthenticationService
from account_v2.constants import Common
from backend.constants import RequestHeader, RequestMethod
from backend.constants import RequestHeader
from backend.internal_api_constants import INTERNAL_API_PREFIX

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -83,13 +83,6 @@ def _authenticate_with_platform_key(
except (ValueError, AttributeError):
return JsonResponse({"message": "Invalid API key format"}, status=401)

# Block DELETE before any DB lookup — never allowed via API key
if request.method == RequestMethod.DELETE:
return JsonResponse(
{"message": "DELETE operations are not allowed via API key"},
status=403,
)

try:
key = PlatformApiKey.objects.select_related(
"created_by", "api_user", "organization"
Expand All @@ -116,13 +109,26 @@ def _authenticate_with_platform_key(
status=401,
)

# Block write operations for read-only keys
if (
key.permission == ApiKeyPermission.READ
and request.method not in RequestMethod.SAFE_METHODS
):
if key.permission not in ApiKeyPermission.values:
logger.error(
"API key %s has unrecognized permission tier %r",
key.id,
key.permission,
)
return JsonResponse(
{"message": "API key has read-only permission"},
{"message": "API key has an unrecognized permission tier"},
status=403,
)

permission = ApiKeyPermission(key.permission)
if not permission.allows(request.method):
return JsonResponse(
{
"message": (
f"API key with permission '{permission.value}' "
f"does not allow {request.method}"
)
},
status=403,
)

Expand Down
23 changes: 23 additions & 0 deletions backend/platform_api/migrations/0002_add_full_access_permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("platform_api", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="platformapikey",
name="permission",
field=models.CharField(
choices=[
("read", "Read"),
("read_write", "Read/Write"),
("full_access", "Full Access"),
],
default="read_write",
max_length=20,
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("platform_api", "0002_add_full_access_permission"),
]

operations = [
migrations.AddConstraint(
model_name="platformapikey",
constraint=models.CheckConstraint(
check=models.Q(permission__in=["read", "read_write", "full_access"]),
name="platform_api_key_permission_valid",
),
),
]
16 changes: 15 additions & 1 deletion backend/platform_api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
class ApiKeyPermission(models.TextChoices):
READ = "read", "Read"
READ_WRITE = "read_write", "Read/Write"
FULL_ACCESS = "full_access", "Full Access"

def allows(self, method: str) -> bool:
"""Whether a key holding this tier may issue the given HTTP method."""
method = method.upper()
if self == ApiKeyPermission.FULL_ACCESS:
return method in {"GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"}
if self == ApiKeyPermission.READ_WRITE:
return method in {"GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"}
return method in {"GET", "HEAD", "OPTIONS"}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
Deepak-Kesavan marked this conversation as resolved.


class PlatformApiKey(DefaultOrganizationMixin, BaseModel):
Expand All @@ -20,7 +30,7 @@ class PlatformApiKey(DefaultOrganizationMixin, BaseModel):
key = models.UUIDField(default=uuid.uuid4, unique=True)
is_active = models.BooleanField(default=True)
permission = models.CharField(
max_length=16,
max_length=20,
Comment thread
jaseemjaskp marked this conversation as resolved.
choices=ApiKeyPermission.choices,
default=ApiKeyPermission.READ_WRITE,
)
Expand Down Expand Up @@ -50,6 +60,10 @@ class Meta:
fields=["name", "organization"],
name="unique_platform_api_key_name_per_org",
),
models.CheckConstraint(
check=models.Q(permission__in=ApiKeyPermission.values),
name="platform_api_key_permission_valid",
),
]

def __str__(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ const SAFE_TEXT_REGEX = /^[a-zA-Z0-9 \-_.,:()/]+$/;
const SAFE_TEXT_MESSAGE =
"Only alphanumeric characters, spaces, hyphens, underscores, periods, commas, colons, parentheses, and forward slashes are allowed.";

const PERMISSION_OPTIONS = [
{ value: "read_write", label: "Read/Write", color: "blue" },
{ value: "read", label: "Read", color: "default" },
{ value: "full_access", label: "Full Access", color: "green" },
];
const PERMISSION_CONFIG = Object.fromEntries(
PERMISSION_OPTIONS.map(({ value, label, color }) => [
value,
{ label, color },
]),
);

function PlatformApiKeys() {
const [keys, setKeys] = useState([]);
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -263,11 +275,13 @@ function PlatformApiKeys() {
dataIndex: "permission",
key: "permission",
width: "10%",
render: (text) => (
<Tag color={text === "read_write" ? "blue" : "default"}>
{text === "read_write" ? "Read/Write" : "Read"}
</Tag>
),
render: (text) => {
const { color, label } = PERMISSION_CONFIG[text] ?? {
color: "default",
label: `Unknown: ${text}`,
};
return <Tag color={color}>{label}</Tag>;
},
},
{
title: "Active",
Expand Down Expand Up @@ -424,10 +438,7 @@ function PlatformApiKeys() {
label="Permission"
initialValue="read_write"
>
<Select>
<Select.Option value="read_write">Read/Write</Select.Option>
<Select.Option value="read">Read</Select.Option>
</Select>
<Select options={PERMISSION_OPTIONS} />
</Form.Item>
</Form>
</Modal>
Expand Down Expand Up @@ -469,10 +480,7 @@ function PlatformApiKeys() {
/>
</Form.Item>
<Form.Item name="permission" label="Permission">
<Select>
<Select.Option value="read_write">Read/Write</Select.Option>
<Select.Option value="read">Read</Select.Option>
</Select>
<Select options={PERMISSION_OPTIONS} />
</Form.Item>
</Form>
</Modal>
Expand Down