Replace plaintext token storage with SHA-256 hashes so leaked database contents cannot be used to authenticate. Plaintext is generated, shown once at creation time, and never persisted. - Add `hash_token()` helper and `MCPTokenManager.create_token()` that returns `(instance, plaintext)`. - Replace `token` field with indexed `token_hash`; look up bearers by hashing the incoming value. - Update dashboard, management command, and admin to surface plaintext only at creation. Disable admin "add" since it cannot reveal plaintext. - Migration drops the old `token` column and adds `token_hash`; pre-existing tokens are invalidated and must be reissued.
39 lines
1.2 KiB
Python
39 lines
1.2 KiB
Python
from django.contrib import admin
|
|
|
|
from .models import MCPToken
|
|
|
|
|
|
@admin.register(MCPToken)
|
|
class MCPTokenAdmin(admin.ModelAdmin):
|
|
list_display = [
|
|
"name",
|
|
"user",
|
|
"is_active",
|
|
"masked_token",
|
|
"expires_at",
|
|
"last_used_at",
|
|
"created_at",
|
|
]
|
|
list_filter = ["is_active"]
|
|
search_fields = ["name", "user__email", "user__username"]
|
|
readonly_fields = ["token_hash", "last_used_at", "created_at", "updated_at"]
|
|
fieldsets = (
|
|
(None, {"fields": ("user", "name", "is_active")}),
|
|
("Restrictions", {"fields": ("allowed_tools", "expires_at")}),
|
|
(
|
|
"Token (hashed at rest — plaintext is shown only once at creation)",
|
|
{"fields": ("token_hash",)},
|
|
),
|
|
("Audit", {"fields": ("last_used_at", "created_at", "updated_at")}),
|
|
)
|
|
|
|
@admin.display(description="Token")
|
|
def masked_token(self, obj):
|
|
return obj.get_masked_token()
|
|
|
|
def has_add_permission(self, request):
|
|
# Tokens must be created via the dashboard or management command
|
|
# so the plaintext can be surfaced to the user. Adding via admin
|
|
# would persist a hash with no plaintext ever shown.
|
|
return False
|