-
Notifications
You must be signed in to change notification settings - Fork 12
feat: API key auth and identity for processing services #1194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6f80958
e3e7646
18545d5
7512193
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
mihow marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| """ | ||
| API key authentication for processing services. | ||
|
|
||
| Uses djangorestframework-api-key to provide key-based auth. Each ProcessingService | ||
| can have one or more API keys. When a request arrives with `Authorization: Api-Key <key>`, | ||
| the authentication class identifies the ProcessingService and sets request.auth to it. | ||
|
|
||
| Contains: | ||
| - ProcessingServiceAPIKeyAuthentication: DRF auth backend | ||
| - HasProcessingServiceAPIKey: DRF permission class | ||
|
|
||
| The ProcessingServiceAPIKey model lives in ami.ml.models.processing_service. | ||
| """ | ||
|
|
||
| import logging | ||
|
|
||
| from rest_framework import authentication, exceptions, permissions | ||
| from rest_framework_api_key.permissions import KeyParser | ||
|
|
||
| from ami.ml.models.processing_service import ProcessingServiceAPIKey | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class ProcessingServiceAPIKeyAuthentication(authentication.BaseAuthentication): | ||
| """ | ||
| DRF authentication class that identifies a ProcessingService from an API key. | ||
|
|
||
| Sets: | ||
| request.user = AnonymousUser (required by django-guardian/ObjectPermission) | ||
| request.auth = ProcessingService instance | ||
|
|
||
| This allows views to check `request.auth` to get the calling service, | ||
| and permission classes to verify project access. | ||
| """ | ||
|
|
||
| key_parser = KeyParser() | ||
|
|
||
| def authenticate(self, request): | ||
| key = self.key_parser.get(request) | ||
| if not key: | ||
| return None # No Api-Key header; fall through to next auth class | ||
|
|
||
| try: | ||
| api_key = ProcessingServiceAPIKey.objects.get_from_key(key) | ||
| except ProcessingServiceAPIKey.DoesNotExist: | ||
| raise exceptions.AuthenticationFailed("Invalid API key.") | ||
|
|
||
| if not api_key.is_valid: | ||
| raise exceptions.AuthenticationFailed("API key has been revoked or expired.") | ||
|
|
||
| from django.contrib.auth.models import AnonymousUser | ||
|
|
||
| return (AnonymousUser(), api_key.processing_service) | ||
|
|
||
| def authenticate_header(self, request): | ||
| return "Api-Key" | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this class and any methods already part of the DRF api_key package?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Claude says:
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Claude says: The library provides We use |
||
|
|
||
|
|
||
| class HasProcessingServiceAPIKey(permissions.BasePermission): | ||
| """ | ||
| Allow access for requests authenticated with a ProcessingService API key. | ||
|
|
||
| The auth backend places the ProcessingService on request.auth. | ||
| This permission verifies project membership. | ||
|
|
||
| Compose with ObjectPermission for endpoints used by both users and services: | ||
| permission_classes = [ObjectPermission | HasProcessingServiceAPIKey] | ||
| """ | ||
|
|
||
| def has_permission(self, request, view): | ||
| from ami.ml.models.processing_service import ProcessingService | ||
|
|
||
| if not isinstance(request.auth, ProcessingService): | ||
| return False | ||
|
|
||
| # For detail views (e.g. /jobs/{pk}/tasks/), defer project scoping | ||
| # to has_object_permission where we can derive it from the object. | ||
| if view.kwargs.get("pk"): | ||
| return True | ||
|
|
||
| get_active_project = getattr(view, "get_active_project", None) | ||
| if not callable(get_active_project): | ||
| return False | ||
|
|
||
| project = get_active_project() | ||
| if not project: | ||
| return False | ||
|
|
||
| return request.auth.projects.filter(pk=project.pk).exists() | ||
|
|
||
| def has_object_permission(self, request, view, obj): | ||
| from ami.ml.models.processing_service import ProcessingService | ||
|
|
||
| if not isinstance(request.auth, ProcessingService): | ||
| return False | ||
|
|
||
| ps = request.auth | ||
| project = obj.get_project() if hasattr(obj, "get_project") else None | ||
| if not project: | ||
| return False | ||
| return ps.projects.filter(pk=project.pk).exists() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # Generated by Django 4.2.10 on 2026-03-29 05:36 | ||
|
|
||
| from django.db import migrations, models | ||
| import django.db.models.deletion | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("ml", "0028_normalize_empty_endpoint_url_to_null"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="processingservice", | ||
| name="last_seen_client_info", | ||
| field=models.JSONField(blank=True, null=True), | ||
| ), | ||
| migrations.CreateModel( | ||
| name="ProcessingServiceAPIKey", | ||
| fields=[ | ||
| ( | ||
| "id", | ||
| models.CharField(editable=False, max_length=150, primary_key=True, serialize=False, unique=True), | ||
| ), | ||
| ("prefix", models.CharField(editable=False, max_length=8, unique=True)), | ||
| ("hashed_key", models.CharField(editable=False, max_length=150)), | ||
| ("created", models.DateTimeField(auto_now_add=True, db_index=True)), | ||
| ( | ||
| "name", | ||
| models.CharField( | ||
| default="", | ||
| help_text="A free-form name for the API key. Need not be unique. 50 characters max.", | ||
| max_length=50, | ||
| ), | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ), | ||
| ( | ||
| "revoked", | ||
| models.BooleanField( | ||
| blank=True, | ||
| default=False, | ||
| help_text="If the API key is revoked, clients cannot use it anymore. (This cannot be undone.)", | ||
| ), | ||
| ), | ||
| ( | ||
| "expiry_date", | ||
| models.DateTimeField( | ||
| blank=True, | ||
| help_text="Once API key expires, clients cannot use it anymore.", | ||
| null=True, | ||
| verbose_name="Expires", | ||
| ), | ||
| ), | ||
| ( | ||
| "processing_service", | ||
| models.ForeignKey( | ||
| on_delete=django.db.models.deletion.CASCADE, related_name="api_keys", to="ml.processingservice" | ||
| ), | ||
| ), | ||
| ], | ||
| options={ | ||
| "verbose_name": "Processing Service API Key", | ||
| "verbose_name_plural": "Processing Service API Keys", | ||
| "ordering": ("-created",), | ||
| "abstract": False, | ||
| }, | ||
| ), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,13 @@ | ||
| from ami.ml.models.algorithm import Algorithm, AlgorithmCategoryMap | ||
| from ami.ml.models.pipeline import Pipeline | ||
| from ami.ml.models.processing_service import ProcessingService | ||
| from ami.ml.models.processing_service import ProcessingService, ProcessingServiceAPIKey | ||
| from ami.ml.models.project_pipeline_config import ProjectPipelineConfig | ||
|
|
||
| __all__ = [ | ||
| "Algorithm", | ||
| "AlgorithmCategoryMap", | ||
| "Pipeline", | ||
| "ProcessingService", | ||
| "ProcessingServiceAPIKey", | ||
| "ProjectPipelineConfig", | ||
| ] |
Uh oh!
There was an error while loading. Please reload this page.