mirror of
https://github.com/ansible/awx.git
synced 2026-01-08 14:32:07 -03:30
* Requirements POC docs from Claude Code eval * Removed unnecessary reference. * Excluded custom DRF configurations per @AlanCoding * Implement review changes from @chrismeyersfsu --------- Co-authored-by: Peter Braun <pbraun@redhat.com>
1305 lines
43 KiB
ReStructuredText
1305 lines
43 KiB
ReStructuredText
=====================================================
|
|
API Development Requirements
|
|
=====================================================
|
|
|
|
**AWX REST API Best Practices**
|
|
|
|
:Version: 1.0
|
|
:Date: September 2025
|
|
:Based on: AWX Enterprise Django REST Framework Analysis
|
|
:Generated by: Claude Code AI
|
|
|
|
----
|
|
|
|
.. contents:: Table of Contents
|
|
:depth: 3
|
|
:local:
|
|
|
|
----
|
|
|
|
.. important::
|
|
This document defines mandatory API development standards for enterprise-grade Django REST Framework applications based on AWX production patterns.
|
|
|
|
----
|
|
|
|
1. API Architecture and Configuration
|
|
=====================================
|
|
|
|
1.1 Django REST Framework Configuration
|
|
---------------------------------------
|
|
|
|
**REQUIRED**: Comprehensive DRF setup with enterprise-grade defaults:
|
|
|
|
.. code-block:: python
|
|
|
|
# settings/defaults.py
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_PAGINATION_CLASS': 'awx.api.pagination.Pagination',
|
|
'PAGE_SIZE': 25,
|
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
|
'awx.api.authentication.JWTAuthentication',
|
|
'awx.api.authentication.SessionAuthentication',
|
|
'awx.api.authentication.LoggedBasicAuthentication',
|
|
),
|
|
'DEFAULT_PERMISSION_CLASSES': (
|
|
'awx.api.permissions.ModelAccessPermission',
|
|
),
|
|
'DEFAULT_PARSER_CLASSES': (
|
|
'awx.api.parsers.JSONParser',
|
|
),
|
|
'DEFAULT_RENDERER_CLASSES': (
|
|
'awx.api.renderers.DefaultJSONRenderer',
|
|
'awx.api.renderers.BrowsableAPIRenderer',
|
|
),
|
|
'DEFAULT_METADATA_CLASS': 'awx.api.metadata.Metadata',
|
|
'EXCEPTION_HANDLER': 'awx.api.views.api_exception_handler',
|
|
'VIEW_DESCRIPTION_FUNCTION': 'awx.api.generics.get_view_description',
|
|
'NON_FIELD_ERRORS_KEY': '__all__',
|
|
'DEFAULT_VERSION': 'v2',
|
|
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema',
|
|
}
|
|
|
|
**Configuration Requirements**:
|
|
|
|
- **Centralized Settings**: API configuration must be centralized and environment-aware
|
|
- **Version Control**: Default API version must be explicitly configured
|
|
- **Error Handling**: Custom exception handler for consistent error responses
|
|
|
|
1.2 Modular API Structure
|
|
-------------------------
|
|
|
|
**REQUIRED**: Organize API with clear separation of concerns:
|
|
|
|
.. code-block:: text
|
|
|
|
api/
|
|
├── __init__.py
|
|
├── authentication.py # Custom authentication classes
|
|
├── permissions.py # Permission and access control
|
|
├── serializers.py # Base serializer patterns
|
|
├── generics.py # Custom view classes
|
|
├── pagination.py # Pagination strategies
|
|
├── renderers.py # Response formatters
|
|
├── fields.py # Custom field types
|
|
├── exceptions.py # Custom exception classes
|
|
├── metadata.py # Field metadata for clients
|
|
├── urls/ # Resource-specific URL modules
|
|
│ ├── __init__.py
|
|
│ ├── organization.py
|
|
│ ├── user.py
|
|
│ ├── project.py
|
|
│ └── job_template.py
|
|
└── views/ # Resource-specific view modules
|
|
├── __init__.py
|
|
├── organization.py
|
|
└── user.py
|
|
|
|
**Structural Requirements**:
|
|
|
|
- **Resource-Based Organization**: Each API resource must have its own module
|
|
- **URL Separation**: URL patterns organized by resource type
|
|
- **View Inheritance**: Consistent view hierarchy with shared base classes
|
|
- **Component Isolation**: Authentication, permissions, and serialization in separate modules
|
|
|
|
----
|
|
|
|
2. REST API Design Standards
|
|
============================
|
|
|
|
2.1 Core REST Principles
|
|
-------------------------
|
|
|
|
**MANDATORY**: All APIs must adhere to these fundamental principles:
|
|
|
|
.. list-table:: REST Design Requirements
|
|
:header-rows: 1
|
|
:widths: 30 70
|
|
|
|
* - Principle
|
|
- Implementation Requirement
|
|
* - **Paginate Everything**
|
|
- Any endpoint returning collections MUST be paginated
|
|
* - **Performance Target**
|
|
- Expected response time ≤ 250ms (1/4 second)
|
|
* - **RBAC Filtering**
|
|
- All collections MUST be filtered by user access permissions
|
|
* - **API Discoverability**
|
|
- All endpoints MUST be traversable from API root '/'
|
|
* - **RESTful Verbs**
|
|
- HTTP methods MUST follow REST conventions
|
|
* - **Constant Time Queries**
|
|
- Database queries MUST NOT vary with result set size
|
|
|
|
2.2 URL Pattern Standards
|
|
--------------------------
|
|
|
|
**REQUIRED**: Consistent RESTful URL patterns:
|
|
|
|
.. code-block:: python
|
|
|
|
# Standard resource patterns
|
|
urlpatterns = [
|
|
# Collection endpoints
|
|
re_path(r'^$', ResourceList.as_view(), name='resource_list'),
|
|
|
|
# Instance endpoints
|
|
re_path(r'^(?P<pk>[0-9]+)/$', ResourceDetail.as_view(), name='resource_detail'),
|
|
|
|
# Nested resource relationships
|
|
re_path(r'^(?P<pk>[0-9]+)/children/$', ResourceChildrenList.as_view(),
|
|
name='resource_children_list'),
|
|
|
|
# Action endpoints
|
|
re_path(r'^(?P<pk>[0-9]+)/launch/$', ResourceLaunch.as_view(),
|
|
name='resource_launch'),
|
|
re_path(r'^(?P<pk>[0-9]+)/copy/$', ResourceCopy.as_view(),
|
|
name='resource_copy'),
|
|
re_path(r'^(?P<pk>[0-9]+)/callback/$', ResourceCallback.as_view(),
|
|
name='resource_callback'),
|
|
]
|
|
|
|
**URL Requirements**:
|
|
|
|
- **Numeric Primary Keys**: Use ``(?P<pk>[0-9]+)`` pattern for resource IDs
|
|
- **Hierarchical Relationships**: Support nested resource access patterns
|
|
- **Action Endpoints**: Additional endpoints for resource-specific actions
|
|
- **Consistent Naming**: Follow ``resource_action`` naming convention
|
|
|
|
2.3 HTTP Method Standards
|
|
-------------------------
|
|
|
|
**REQUIRED**: Proper HTTP method usage:
|
|
|
|
.. list-table:: HTTP Method Requirements
|
|
:header-rows: 1
|
|
:widths: 15 25 60
|
|
|
|
* - Method
|
|
- Usage
|
|
- Requirements
|
|
* - **GET**
|
|
- Retrieve resources
|
|
- Idempotent, no side effects, cacheable
|
|
* - **POST**
|
|
- Create resources
|
|
- Returns 201 Created with Location header
|
|
* - **PUT**
|
|
- Update/Replace resources
|
|
- Idempotent, full resource replacement
|
|
* - **PATCH**
|
|
- Partial updates
|
|
- Non-idempotent, partial resource modification
|
|
* - **DELETE**
|
|
- Remove resources
|
|
- Idempotent, returns 204 No Content
|
|
* - **OPTIONS**
|
|
- Metadata discovery
|
|
- Returns available methods and field metadata
|
|
|
|
----
|
|
|
|
3. Authentication and Authorization
|
|
===================================
|
|
|
|
3.1 Multi-Layer Authentication Stack
|
|
------------------------------------
|
|
|
|
**REQUIRED**: Implement comprehensive authentication system:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/authentication.py
|
|
class LoggedBasicAuthentication(authentication.BasicAuthentication):
|
|
"""Basic authentication with comprehensive logging"""
|
|
|
|
def authenticate(self, request):
|
|
if not settings.AUTH_BASIC_ENABLED:
|
|
return
|
|
|
|
ret = super().authenticate(request)
|
|
if ret:
|
|
username = ret[0].username if ret[0] else '<none>'
|
|
logger.info(
|
|
f"User {username} performed {request.method} "
|
|
f"to {request.path} through the API using Basic Auth"
|
|
)
|
|
return ret
|
|
|
|
class SessionAuthentication(authentication.SessionAuthentication):
|
|
"""Enhanced session authentication with CSRF handling"""
|
|
|
|
def authenticate(self, request):
|
|
# Custom session validation logic
|
|
return super().authenticate(request)
|
|
|
|
**Authentication Requirements**:
|
|
|
|
1. **JWT Authentication**: Primary token-based authentication
|
|
2. **Session Authentication**: Browser-based session support
|
|
3. **Basic Authentication**: With comprehensive request logging
|
|
4. **Authentication Logging**: All authentication events must be logged
|
|
5. **Fallback Support**: Multiple authentication methods for different clients
|
|
|
|
3.2 Role-Based Access Control (RBAC)
|
|
------------------------------------
|
|
|
|
**REQUIRED**: Implement comprehensive permission system:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/permissions.py
|
|
class ModelAccessPermission(permissions.BasePermission):
|
|
"""Advanced model-based access control"""
|
|
|
|
def has_permission(self, request, view):
|
|
# Check view-level permissions
|
|
if not hasattr(view, 'model'):
|
|
return False
|
|
|
|
# Parent resource permission checking
|
|
if hasattr(view, 'parent_model'):
|
|
parent_obj = view.get_parent_object()
|
|
if not check_user_access(
|
|
request.user,
|
|
view.parent_model,
|
|
'read',
|
|
parent_obj
|
|
):
|
|
return False
|
|
|
|
return True
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
# Object-level permission checking
|
|
permission_map = {
|
|
'GET': 'read',
|
|
'POST': 'add',
|
|
'PUT': 'change',
|
|
'PATCH': 'change',
|
|
'DELETE': 'delete',
|
|
}
|
|
|
|
required_permission = permission_map.get(request.method, 'read')
|
|
return check_user_access(request.user, view.model, required_permission, obj)
|
|
|
|
**RBAC Requirements**:
|
|
|
|
- **Method-Specific Permissions**: Different permissions for GET, POST, PUT, DELETE
|
|
- **Object-Level Control**: Fine-grained access control on individual resources
|
|
- **Hierarchical Permissions**: Parent-child relationship permission inheritance
|
|
- **Data-Aware Permissions**: Access control based on request data content
|
|
- **Audit Trail**: All permission checks must be logged
|
|
|
|
3.3 API Token Management
|
|
------------------------
|
|
|
|
**REQUIRED**: Secure token handling patterns:
|
|
|
|
.. code-block:: python
|
|
|
|
# Token-based authentication requirements
|
|
JWT_SETTINGS = {
|
|
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
|
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
|
'ROTATE_REFRESH_TOKENS': True,
|
|
'BLACKLIST_AFTER_ROTATION': True,
|
|
'ALGORITHM': 'RS256',
|
|
'SIGNING_KEY': settings.JWT_PRIVATE_KEY,
|
|
'VERIFYING_KEY': settings.JWT_PUBLIC_KEY,
|
|
}
|
|
|
|
**Token Requirements**:
|
|
|
|
- **Short-Lived Access Tokens**: Maximum 1-hour expiration
|
|
- **Refresh Token Rotation**: Automatic rotation on refresh
|
|
- **Token Blacklisting**: Revoked tokens must be blacklisted
|
|
- **Asymmetric Encryption**: Use RS256 with key pairs
|
|
- **Secure Storage**: Private keys must be externally managed
|
|
|
|
----
|
|
|
|
4. Serialization and Data Validation
|
|
=====================================
|
|
|
|
4.1 Base Serializer Architecture
|
|
--------------------------------
|
|
|
|
**REQUIRED**: Implement consistent serializer patterns:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/serializers.py
|
|
class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetaclass):
|
|
"""Base serializer with common enterprise patterns"""
|
|
|
|
class Meta:
|
|
fields = (
|
|
'id', 'type', 'url', 'related', 'summary_fields',
|
|
'created', 'modified', 'name', 'description'
|
|
)
|
|
summary_fields = ()
|
|
summarizable_fields = ()
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._setup_summary_fields()
|
|
self._setup_related_fields()
|
|
|
|
def _setup_summary_fields(self):
|
|
"""Configure optimized nested data representation"""
|
|
pass
|
|
|
|
def _setup_related_fields(self):
|
|
"""Configure related resource URL fields"""
|
|
pass
|
|
|
|
**Serializer Requirements**:
|
|
|
|
- **Base Class Inheritance**: All serializers must inherit from base serializer
|
|
- **Common Fields**: Standardized fields across all resources
|
|
- **Summary Fields**: Optimized nested data representation
|
|
- **Related URLs**: Structured related resource references
|
|
- **Metaclass Usage**: Custom metaclass for field inheritance
|
|
|
|
4.2 Custom Field Types
|
|
----------------------
|
|
|
|
**REQUIRED**: Domain-specific field validation:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/fields.py
|
|
class BooleanNullField(NullFieldMixin, serializers.BooleanField):
|
|
"""Boolean field that allows null and empty string as False"""
|
|
|
|
def to_internal_value(self, data):
|
|
if data is None or data == '':
|
|
return False
|
|
return super().to_internal_value(data)
|
|
|
|
class CharNullField(NullFieldMixin, serializers.CharField):
|
|
"""Char field that allows null input and coerces to empty string"""
|
|
|
|
def to_internal_value(self, data):
|
|
if data is None:
|
|
return ''
|
|
return super().to_internal_value(data)
|
|
|
|
class JSONField(serializers.Field):
|
|
"""Custom JSON field with validation"""
|
|
|
|
def to_representation(self, value):
|
|
return json.loads(value) if isinstance(value, str) else value
|
|
|
|
def to_internal_value(self, data):
|
|
try:
|
|
return json.dumps(data)
|
|
except (TypeError, ValueError):
|
|
raise serializers.ValidationError("Invalid JSON data")
|
|
|
|
**Field Requirements**:
|
|
|
|
- **Null Handling**: Consistent null value processing across field types
|
|
- **Type Coercion**: Automatic data type conversion where appropriate
|
|
- **Validation Mixins**: Reusable validation logic through mixins
|
|
- **Custom Serialization**: Domain-specific serialization logic
|
|
|
|
4.3 Data Validation Patterns
|
|
----------------------------
|
|
|
|
**REQUIRED**: Comprehensive validation strategies:
|
|
|
|
.. code-block:: python
|
|
|
|
class ResourceSerializer(BaseSerializer):
|
|
"""Example resource serializer with validation"""
|
|
|
|
class Meta(BaseSerializer.Meta):
|
|
model = Resource
|
|
fields = BaseSerializer.Meta.fields + ('status', 'type', 'configuration')
|
|
|
|
def validate_configuration(self, value):
|
|
"""Field-level validation"""
|
|
if not isinstance(value, dict):
|
|
raise serializers.ValidationError("Configuration must be a JSON object")
|
|
return value
|
|
|
|
def validate(self, data):
|
|
"""Object-level validation"""
|
|
if data.get('status') == 'active' and not data.get('configuration'):
|
|
raise serializers.ValidationError(
|
|
"Active resources must have configuration"
|
|
)
|
|
return data
|
|
|
|
def create(self, validated_data):
|
|
"""Custom creation logic with audit trail"""
|
|
instance = super().create(validated_data)
|
|
self._log_creation(instance)
|
|
return instance
|
|
|
|
**Validation Requirements**:
|
|
|
|
- **Field-Level Validation**: Validate individual fields with custom logic
|
|
- **Object-Level Validation**: Cross-field validation rules
|
|
- **Business Logic Validation**: Domain-specific validation rules
|
|
- **Audit Logging**: All validation failures must be logged
|
|
- **Error Formatting**: Consistent error message structure
|
|
|
|
----
|
|
|
|
5. Performance and Optimization
|
|
===============================
|
|
|
|
5.1 Query Optimization Standards
|
|
--------------------------------
|
|
|
|
**MANDATORY**: Database query performance requirements:
|
|
|
|
.. warning::
|
|
**Critical Performance Rule**: The number of database queries MUST be constant time and MUST NOT vary with the size of the result set.
|
|
|
|
.. code-block:: python
|
|
|
|
# api/generics.py
|
|
def optimize_queryset(queryset, view, remove_django_content_type=False):
|
|
"""Query optimization for API views"""
|
|
|
|
# Select related for foreign keys
|
|
if hasattr(view, 'select_related_fields'):
|
|
queryset = queryset.select_related(*view.select_related_fields)
|
|
|
|
# Prefetch related for many-to-many and reverse foreign keys
|
|
if hasattr(view, 'prefetch_related_fields'):
|
|
queryset = queryset.prefetch_related(*view.prefetch_related_fields)
|
|
|
|
# Only select necessary fields
|
|
if hasattr(view, 'only_fields'):
|
|
queryset = queryset.only(*view.only_fields)
|
|
|
|
return queryset
|
|
|
|
**Query Optimization Requirements**:
|
|
|
|
- **Constant Time Queries**: Query count independent of result set size
|
|
- **Select Related**: Use ``select_related()`` for foreign key relationships
|
|
- **Prefetch Related**: Use ``prefetch_related()`` for many-to-many relationships
|
|
- **Field Selection**: Use ``only()`` to limit selected fields
|
|
- **No Serializer Queries**: Database queries prohibited in serializers
|
|
|
|
5.2 Pagination Strategy
|
|
-----------------------
|
|
|
|
**REQUIRED**: Advanced pagination with performance optimization:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/pagination.py
|
|
class Pagination(pagination.PageNumberPagination):
|
|
"""Enterprise pagination with performance features"""
|
|
|
|
page_size_query_param = 'page_size'
|
|
max_page_size = 200
|
|
count_disabled = False
|
|
|
|
def paginate_queryset(self, queryset, request, **kwargs):
|
|
# Optional count disabling for large datasets
|
|
self.count_disabled = 'count_disabled' in request.query_params
|
|
|
|
if self.count_disabled:
|
|
# Skip expensive COUNT query for large datasets
|
|
return self._paginate_without_count(queryset, request)
|
|
|
|
return super().paginate_queryset(queryset, request, **kwargs)
|
|
|
|
def get_paginated_response(self, data):
|
|
response_data = {
|
|
'count': self.page.paginator.count if not self.count_disabled else None,
|
|
'next': self.get_next_link(),
|
|
'previous': self.get_previous_link(),
|
|
'results': data
|
|
}
|
|
return Response(response_data)
|
|
|
|
**Pagination Requirements**:
|
|
|
|
- **Performance Optimization**: Optional count disabling for large datasets
|
|
- **Configurable Page Sizes**: Maximum page size limits with user control
|
|
- **Consistent Format**: Standardized pagination response format
|
|
- **Memory Efficiency**: Efficient memory usage for large result sets
|
|
|
|
5.3 Caching Strategies
|
|
----------------------
|
|
|
|
**REQUIRED**: Multi-level caching implementation:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/middleware.py
|
|
class SettingsCacheMiddleware(MiddlewareMixin):
|
|
"""Clear settings cache on each request"""
|
|
|
|
def process_request(self, request):
|
|
from django.conf import settings
|
|
if hasattr(settings, '_awx_conf_memoizedcache'):
|
|
settings._awx_conf_memoizedcache.clear()
|
|
|
|
class ResponseCacheMiddleware(MiddlewareMixin):
|
|
"""Cache API responses based on content type"""
|
|
|
|
def process_response(self, request, response):
|
|
# Cache responses for GET requests only
|
|
if request.method == 'GET' and response.status_code == 200:
|
|
cache_key = self._generate_cache_key(request)
|
|
cache.set(cache_key, response.content, timeout=300)
|
|
return response
|
|
|
|
**Caching Requirements**:
|
|
|
|
- **Settings Caching**: Intelligent configuration caching with invalidation
|
|
- **Response Caching**: Cache GET responses with appropriate TTL
|
|
- **Cache Invalidation**: Automatic cache clearing on data changes
|
|
- **Selective Caching**: Cache only appropriate endpoints and data
|
|
|
|
----
|
|
|
|
6. Error Handling and Status Codes
|
|
===================================
|
|
|
|
6.1 Custom Exception Hierarchy
|
|
------------------------------
|
|
|
|
**REQUIRED**: Comprehensive exception handling system:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/exceptions.py
|
|
class APIException(Exception):
|
|
"""Base API exception with structured error format"""
|
|
status_code = 400
|
|
detail = "An error occurred"
|
|
|
|
def __init__(self, detail=None, code=None):
|
|
if detail is not None:
|
|
self.detail = detail
|
|
if code is not None:
|
|
self.status_code = code
|
|
|
|
class ActiveJobConflict(ValidationError):
|
|
"""Resource conflict with active jobs"""
|
|
status_code = 409
|
|
|
|
def __init__(self, active_jobs):
|
|
super().__init__()
|
|
self.detail = {
|
|
"error": "Resource is being used by running jobs",
|
|
"active_jobs": active_jobs,
|
|
"conflict_type": "active_job_dependency"
|
|
}
|
|
|
|
class PermissionDenied(APIException):
|
|
"""Enhanced permission denied with context"""
|
|
status_code = 403
|
|
|
|
def __init__(self, detail=None, required_permission=None, resource=None):
|
|
super().__init__(detail)
|
|
self.detail = {
|
|
"error": detail or "Permission denied",
|
|
"required_permission": required_permission,
|
|
"resource": resource
|
|
}
|
|
|
|
**Exception Requirements**:
|
|
|
|
- **Structured Error Format**: Consistent error response structure
|
|
- **HTTP Status Codes**: Proper status code mapping for different error types
|
|
- **Context Information**: Rich error context for debugging and user feedback
|
|
- **Exception Hierarchy**: Logical exception inheritance structure
|
|
|
|
6.2 Global Exception Handler
|
|
-----------------------------
|
|
|
|
**REQUIRED**: Centralized exception processing:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/views/__init__.py
|
|
def api_exception_handler(exc, context):
|
|
"""Enhanced exception handler with logging and standardization"""
|
|
|
|
# Handle database integrity errors
|
|
if isinstance(exc, IntegrityError):
|
|
exc = ParseError("Database constraint violation: " + str(exc))
|
|
|
|
# Handle Django field errors
|
|
if isinstance(exc, FieldError):
|
|
exc = ParseError("Invalid field specification: " + str(exc))
|
|
|
|
# Handle permission errors with context
|
|
if isinstance(exc, PermissionDenied):
|
|
logger.warning(
|
|
f"Permission denied for user {context['request'].user} "
|
|
f"accessing {context['request'].path}"
|
|
)
|
|
|
|
# Use default DRF exception handler
|
|
response = exception_handler(exc, context)
|
|
|
|
# Add custom error metadata
|
|
if response is not None:
|
|
response.data['timestamp'] = timezone.now().isoformat()
|
|
response.data['path'] = context['request'].path
|
|
response.data['method'] = context['request'].method
|
|
|
|
return response
|
|
|
|
**Exception Handler Requirements**:
|
|
|
|
- **Comprehensive Logging**: All exceptions must be logged with context
|
|
- **Error Transformation**: Convert database/system errors to API errors
|
|
- **Metadata Addition**: Include request context in error responses
|
|
- **Security Considerations**: Sanitize error messages for production
|
|
|
|
6.3 Status Code Standards
|
|
-------------------------
|
|
|
|
**REQUIRED**: Proper HTTP status code usage:
|
|
|
|
.. list-table:: Status Code Requirements
|
|
:header-rows: 1
|
|
:widths: 15 25 60
|
|
|
|
* - Code
|
|
- Usage
|
|
- Implementation Requirements
|
|
* - **200 OK**
|
|
- Successful GET, PUT, PATCH
|
|
- Include response timing headers
|
|
* - **201 Created**
|
|
- Successful POST
|
|
- Include Location header with new resource URL
|
|
* - **204 No Content**
|
|
- Successful DELETE
|
|
- No response body required
|
|
* - **400 Bad Request**
|
|
- Validation errors
|
|
- Include detailed field-level error information
|
|
* - **401 Unauthorized**
|
|
- Authentication required
|
|
- Include WWW-Authenticate header
|
|
* - **403 Forbidden**
|
|
- Permission denied
|
|
- Include required permission information
|
|
* - **404 Not Found**
|
|
- Resource not found
|
|
- Include suggested alternatives if available
|
|
* - **409 Conflict**
|
|
- Resource conflicts
|
|
- Include conflict resolution information
|
|
* - **422 Unprocessable Entity**
|
|
- Business logic errors
|
|
- Include business rule violation details
|
|
* - **500 Internal Server Error**
|
|
- System errors
|
|
- Log full error details, return sanitized message
|
|
|
|
----
|
|
|
|
7. API Documentation and Discovery
|
|
==================================
|
|
|
|
7.1 OpenAPI/Swagger Integration
|
|
-------------------------------
|
|
|
|
**REQUIRED**: Comprehensive API documentation system:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/swagger.py
|
|
class CustomSwaggerAutoSchema(SwaggerAutoSchema):
|
|
"""Enhanced Swagger schema generation"""
|
|
|
|
def get_tags(self, operation_keys=None):
|
|
"""Generate logical API tags"""
|
|
tags = []
|
|
|
|
if hasattr(self.view, 'swagger_topic'):
|
|
tags.append(str(self.view.swagger_topic).title())
|
|
elif hasattr(self.view, 'serializer_class'):
|
|
serializer = self.view.serializer_class
|
|
if hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'model'):
|
|
model_name = serializer.Meta.model._meta.verbose_name_plural
|
|
tags.append(str(model_name).title())
|
|
|
|
return tags
|
|
|
|
def get_operation_id(self, operation_keys):
|
|
"""Generate unique operation IDs"""
|
|
return '_'.join(operation_keys)
|
|
|
|
def get_description(self):
|
|
"""Enhanced operation descriptions"""
|
|
description = super().get_description()
|
|
|
|
# Add permission requirements
|
|
if hasattr(self.view, 'required_permissions'):
|
|
description += f"\n\nRequired permissions: {self.view.required_permissions}"
|
|
|
|
# Add rate limiting information
|
|
if hasattr(self.view, 'throttle_classes'):
|
|
description += f"\n\nRate limiting: Applied"
|
|
|
|
return description
|
|
|
|
**Documentation Requirements**:
|
|
|
|
- **Auto-Generated Schemas**: Complete OpenAPI 3.0 schema generation
|
|
- **Logical Grouping**: API endpoints grouped by resource/functionality
|
|
- **Operation Metadata**: Detailed operation descriptions with examples
|
|
- **Permission Documentation**: Clear permission requirements for each endpoint
|
|
- **Response Examples**: Example responses for all status codes
|
|
|
|
7.2 Rich Field Metadata
|
|
-----------------------
|
|
|
|
**REQUIRED**: Comprehensive field information for clients:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/metadata.py
|
|
class Metadata(metadata.SimpleMetadata):
|
|
"""Enhanced metadata with comprehensive field information"""
|
|
|
|
def get_field_info(self, field):
|
|
"""Generate detailed field metadata"""
|
|
field_info = super().get_field_info(field)
|
|
|
|
# Add validation information
|
|
field_info['validators'] = self._get_validator_info(field)
|
|
|
|
# Add filtering capabilities
|
|
field_info['filterable'] = getattr(field, 'filterable', False)
|
|
field_info['searchable'] = getattr(field, 'searchable', False)
|
|
|
|
# Add help text and examples
|
|
field_info['help_text'] = getattr(field, 'help_text', None)
|
|
field_info['example'] = getattr(field, 'example', None)
|
|
|
|
# Add relationship information
|
|
if hasattr(field, 'queryset'):
|
|
field_info['related_model'] = field.queryset.model.__name__
|
|
field_info['related_url'] = self._get_related_url(field)
|
|
|
|
return field_info
|
|
|
|
def _get_validator_info(self, field):
|
|
"""Extract validator information"""
|
|
validators = []
|
|
for validator in field.validators:
|
|
validators.append({
|
|
'type': validator.__class__.__name__,
|
|
'message': getattr(validator, 'message', None)
|
|
})
|
|
return validators
|
|
|
|
**Metadata Requirements**:
|
|
|
|
- **Validation Rules**: Complete field validation information
|
|
- **Filtering Support**: Indicate which fields support filtering/searching
|
|
- **Relationship Data**: Information about related resources
|
|
- **Help Text**: User-friendly field descriptions and examples
|
|
- **Client Tooling**: Metadata suitable for auto-generating client SDKs
|
|
|
|
7.3 API Discoverability
|
|
-----------------------
|
|
|
|
**REQUIRED**: Complete API traversal from root endpoint:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/views/root.py
|
|
class ApiRootView(APIView):
|
|
"""API root with complete endpoint discovery"""
|
|
|
|
def get(self, request, format=None):
|
|
"""Return all available API endpoints"""
|
|
endpoints = {
|
|
'ping': reverse('api:ping', request=request),
|
|
'config': reverse('api:config', request=request),
|
|
'me': reverse('api:me', request=request),
|
|
'dashboard': reverse('api:dashboard', request=request),
|
|
'organizations': reverse('api:organization_list', request=request),
|
|
'users': reverse('api:user_list', request=request),
|
|
'teams': reverse('api:team_list', request=request),
|
|
'projects': reverse('api:project_list', request=request),
|
|
'inventories': reverse('api:inventory_list', request=request),
|
|
'job_templates': reverse('api:job_template_list', request=request),
|
|
'jobs': reverse('api:job_list', request=request),
|
|
}
|
|
|
|
# Add versioning information
|
|
endpoints['current_version'] = request.version
|
|
endpoints['available_versions'] = ['v1', 'v2']
|
|
|
|
# Add authentication information
|
|
endpoints['auth_methods'] = [
|
|
'jwt', 'session', 'basic'
|
|
]
|
|
|
|
return Response(endpoints)
|
|
|
|
**Discoverability Requirements**:
|
|
|
|
- **Complete Traversal**: All endpoints accessible from API root
|
|
- **Version Information**: Available API versions clearly indicated
|
|
- **Authentication Methods**: Supported authentication mechanisms listed
|
|
- **Relationship Links**: Related resource URLs in all responses
|
|
|
|
----
|
|
|
|
8. API Versioning Strategy
|
|
===========================
|
|
|
|
8.1 URL Path Versioning
|
|
-----------------------
|
|
|
|
**REQUIRED**: Clean URL-based versioning system:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/versioning.py
|
|
class URLPathVersioning(BaseVersioning):
|
|
"""URL path-based API versioning"""
|
|
|
|
version_param = 'version'
|
|
default_version = 'v2'
|
|
allowed_versions = ['v1', 'v2']
|
|
|
|
def determine_version(self, request, *args, **kwargs):
|
|
version = kwargs.get(self.version_param, self.default_version)
|
|
if version not in self.allowed_versions:
|
|
raise exceptions.NotFound(f"API version '{version}' not found")
|
|
return version
|
|
|
|
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
|
if request.version is not None:
|
|
kwargs = {} if kwargs is None else kwargs
|
|
kwargs[self.version_param] = request.version
|
|
return super().reverse(viewname, args, kwargs, request, format, **extra)
|
|
|
|
**Versioning Requirements**:
|
|
|
|
- **URL Path Format**: Use ``/api/v{version}/`` format
|
|
- **Backward Compatibility**: Support multiple API versions simultaneously
|
|
- **Default Version**: Explicit default version configuration
|
|
- **Version Discovery**: Available versions discoverable from API root
|
|
- **Graceful Deprecation**: Clear deprecation timeline for old versions
|
|
|
|
8.2 Version-Specific Behavior
|
|
-----------------------------
|
|
|
|
**REQUIRED**: Handle version differences in views and serializers:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/views/versioned.py
|
|
class VersionedViewMixin:
|
|
"""Mixin for version-aware view behavior"""
|
|
|
|
def get_serializer_class(self):
|
|
"""Return version-specific serializer"""
|
|
base_serializer = super().get_serializer_class()
|
|
|
|
if self.request.version == 'v1':
|
|
# Use legacy serializer for v1
|
|
return getattr(self, 'v1_serializer_class', base_serializer)
|
|
elif self.request.version == 'v2':
|
|
# Use current serializer for v2
|
|
return base_serializer
|
|
|
|
return base_serializer
|
|
|
|
def get_queryset(self):
|
|
"""Return version-specific queryset"""
|
|
queryset = super().get_queryset()
|
|
|
|
# Apply version-specific filtering
|
|
if self.request.version == 'v1':
|
|
# Legacy filtering logic
|
|
queryset = self._apply_v1_filters(queryset)
|
|
|
|
return queryset
|
|
|
|
**Version Management Requirements**:
|
|
|
|
- **Serializer Versioning**: Version-specific serializer classes
|
|
- **Behavior Versioning**: Different view behavior for different versions
|
|
- **Data Migration**: Automatic data format conversion between versions
|
|
- **Feature Flags**: Version-specific feature availability
|
|
|
|
----
|
|
|
|
9. Testing Requirements
|
|
=======================
|
|
|
|
9.1 Comprehensive Test Coverage
|
|
-------------------------------
|
|
|
|
**MANDATORY**: Complete API test coverage requirements:
|
|
|
|
.. important::
|
|
**Critical Testing Rule**: Every API URL/endpoint MUST have unit test coverage with both positive and negative test cases.
|
|
|
|
.. code-block:: python
|
|
|
|
# tests/api/test_resource.py
|
|
class TestResourceAPI:
|
|
"""Comprehensive API testing for Resource endpoints"""
|
|
|
|
def test_list_resources_success(self, api_client, user):
|
|
"""Test successful resource listing"""
|
|
# Setup test data
|
|
resources = ResourceFactory.create_batch(3)
|
|
|
|
# Make request
|
|
response = api_client.get('/api/v2/resources/')
|
|
|
|
# Assertions
|
|
assert response.status_code == 200
|
|
assert response.data['count'] == 3
|
|
assert len(response.data['results']) == 3
|
|
|
|
def test_list_resources_permission_denied(self, api_client, user_without_permissions):
|
|
"""Test resource listing without permissions"""
|
|
ResourceFactory.create_batch(3)
|
|
|
|
api_client.force_authenticate(user=user_without_permissions)
|
|
response = api_client.get('/api/v2/resources/')
|
|
|
|
assert response.status_code == 403
|
|
assert 'permission' in response.data['error'].lower()
|
|
|
|
def test_create_resource_success(self, api_client, user):
|
|
"""Test successful resource creation"""
|
|
data = {
|
|
'name': 'Test Resource',
|
|
'description': 'Test Description',
|
|
'configuration': {'key': 'value'}
|
|
}
|
|
|
|
response = api_client.post('/api/v2/resources/', data)
|
|
|
|
assert response.status_code == 201
|
|
assert response.data['name'] == data['name']
|
|
assert 'id' in response.data
|
|
|
|
@pytest.mark.parametrize('invalid_data,expected_error', [
|
|
({'name': ''}, 'name'),
|
|
({'name': 'test'}, 'description'),
|
|
({'name': 'test', 'description': 'test', 'configuration': 'invalid'}, 'configuration'),
|
|
])
|
|
def test_create_resource_validation_errors(self, api_client, user, invalid_data, expected_error):
|
|
"""Test resource creation validation"""
|
|
response = api_client.post('/api/v2/resources/', invalid_data)
|
|
|
|
assert response.status_code == 400
|
|
assert expected_error in response.data
|
|
|
|
**Testing Requirements**:
|
|
|
|
- **Positive Test Cases**: Test all successful operation scenarios
|
|
- **Negative Test Cases**: Test all error conditions and edge cases
|
|
- **Permission Testing**: Test access control for all user types
|
|
- **Validation Testing**: Test all field validation rules
|
|
- **Parameterized Testing**: Use data-driven test approaches
|
|
- **Factory Pattern**: Use factories for test data generation
|
|
|
|
9.2 Performance Testing
|
|
-----------------------
|
|
|
|
**REQUIRED**: API performance validation:
|
|
|
|
.. code-block:: python
|
|
|
|
# tests/api/test_performance.py
|
|
class TestAPIPerformance:
|
|
"""Performance testing for API endpoints"""
|
|
|
|
def test_query_count_constant(self, api_client, django_assert_num_queries):
|
|
"""Ensure query count doesn't vary with result set size"""
|
|
|
|
# Test with small dataset
|
|
ResourceFactory.create_batch(10)
|
|
with django_assert_num_queries(5): # Expected query count
|
|
response = api_client.get('/api/v2/resources/')
|
|
assert response.status_code == 200
|
|
|
|
# Test with larger dataset - query count should remain same
|
|
ResourceFactory.create_batch(90) # Total 100 resources
|
|
with django_assert_num_queries(5): # Same query count
|
|
response = api_client.get('/api/v2/resources/')
|
|
assert response.status_code == 200
|
|
|
|
def test_response_time_target(self, api_client):
|
|
"""Ensure response time meets performance targets"""
|
|
import time
|
|
|
|
start_time = time.time()
|
|
response = api_client.get('/api/v2/resources/')
|
|
end_time = time.time()
|
|
|
|
response_time = end_time - start_time
|
|
assert response_time < 0.25 # 250ms target
|
|
assert response.status_code == 200
|
|
|
|
**Performance Test Requirements**:
|
|
|
|
- **Query Count Testing**: Verify constant-time database queries
|
|
- **Response Time Testing**: Validate performance targets (≤250ms)
|
|
- **Load Testing**: Test with realistic data volumes
|
|
- **Memory Usage**: Monitor memory consumption patterns
|
|
|
|
----
|
|
|
|
10. Security Requirements
|
|
=========================
|
|
|
|
10.1 Authentication Security
|
|
----------------------------
|
|
|
|
**REQUIRED**: Secure authentication implementation:
|
|
|
|
.. code-block:: python
|
|
|
|
# Security configuration
|
|
AUTHENTICATION_SECURITY = {
|
|
'PASSWORD_RESET_TIMEOUT': 3600, # 1 hour
|
|
'LOGIN_ATTEMPT_LIMIT': 5,
|
|
'LOGIN_LOCKOUT_DURATION': 300, # 5 minutes
|
|
'JWT_ACCESS_TOKEN_LIFETIME': 3600, # 1 hour
|
|
'JWT_REFRESH_TOKEN_LIFETIME': 604800, # 7 days
|
|
'SESSION_COOKIE_AGE': 1800, # 30 minutes
|
|
'CSRF_COOKIE_HTTPONLY': True,
|
|
'SESSION_COOKIE_SECURE': True,
|
|
'SESSION_COOKIE_SAMESITE': 'Lax',
|
|
}
|
|
|
|
**Authentication Security Requirements**:
|
|
|
|
- **Rate Limiting**: Implement login attempt limiting
|
|
- **Token Expiration**: Short-lived access tokens with refresh mechanism
|
|
- **Secure Cookies**: HTTPOnly, Secure, and SameSite cookie attributes
|
|
- **Brute Force Protection**: Account lockout after failed attempts
|
|
- **Session Management**: Configurable session timeouts
|
|
|
|
10.2 Authorization Security
|
|
---------------------------
|
|
|
|
**REQUIRED**: Comprehensive access control:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/permissions.py
|
|
class SecureModelAccessPermission(permissions.BasePermission):
|
|
"""Security-enhanced permission checking"""
|
|
|
|
def has_permission(self, request, view):
|
|
# Log all permission checks for security audit
|
|
logger.info(
|
|
f"Permission check: {request.user} accessing "
|
|
f"{view.__class__.__name__} via {request.method}"
|
|
)
|
|
|
|
# Check user account status
|
|
if not request.user.is_active:
|
|
logger.warning(f"Inactive user {request.user} attempted access")
|
|
return False
|
|
|
|
# Check IP whitelist if configured
|
|
if hasattr(settings, 'API_IP_WHITELIST'):
|
|
client_ip = self._get_client_ip(request)
|
|
if client_ip not in settings.API_IP_WHITELIST:
|
|
logger.warning(f"Access denied from IP {client_ip}")
|
|
return False
|
|
|
|
return super().has_permission(request, view)
|
|
|
|
**Authorization Security Requirements**:
|
|
|
|
- **Audit Logging**: Log all access attempts and permission checks
|
|
- **Account Status Checking**: Verify user account is active
|
|
- **IP Whitelisting**: Optional IP-based access control
|
|
- **Principle of Least Privilege**: Minimal required permissions only
|
|
- **Session Validation**: Verify session integrity on each request
|
|
|
|
10.3 Data Protection
|
|
--------------------
|
|
|
|
**REQUIRED**: Secure data handling:
|
|
|
|
.. code-block:: python
|
|
|
|
# api/serializers.py
|
|
class SecureSerializer(BaseSerializer):
|
|
"""Security-enhanced serializer"""
|
|
|
|
sensitive_fields = ['password', 'secret_key', 'private_key', 'token']
|
|
|
|
def to_representation(self, instance):
|
|
"""Remove sensitive data from responses"""
|
|
data = super().to_representation(instance)
|
|
|
|
# Mask sensitive fields
|
|
for field in self.sensitive_fields:
|
|
if field in data:
|
|
data[field] = '***REDACTED***'
|
|
|
|
return data
|
|
|
|
def validate(self, attrs):
|
|
"""Enhanced validation with security checks"""
|
|
attrs = super().validate(attrs)
|
|
|
|
# Validate against known security patterns
|
|
for field, value in attrs.items():
|
|
if self._contains_injection_attempt(value):
|
|
logger.warning(f"Potential injection attempt in field {field}")
|
|
raise serializers.ValidationError(f"Invalid value for {field}")
|
|
|
|
return attrs
|
|
|
|
**Data Protection Requirements**:
|
|
|
|
- **Sensitive Data Masking**: Automatic masking of sensitive fields in responses
|
|
- **Input Sanitization**: Validate and sanitize all input data
|
|
- **Injection Prevention**: Protect against SQL injection and XSS attacks
|
|
- **Data Encryption**: Encrypt sensitive data at rest and in transit
|
|
- **Audit Trail**: Log all data access and modification events
|
|
|
|
----
|
|
|
|
Compliance Checklist
|
|
=====================
|
|
|
|
API Design Standards
|
|
--------------------
|
|
|
|
.. list-table::
|
|
:header-rows: 1
|
|
:widths: 50 10
|
|
|
|
* - Requirement
|
|
- Status
|
|
* - RESTful URL patterns implemented
|
|
- ☐
|
|
* - Proper HTTP method usage
|
|
- ☐
|
|
* - Consistent error responses
|
|
- ☐
|
|
* - Comprehensive pagination
|
|
- ☐
|
|
* - API versioning strategy
|
|
- ☐
|
|
* - OpenAPI documentation
|
|
- ☐
|
|
* - API discoverability from root
|
|
- ☐
|
|
* - Performance targets met (≤250ms)
|
|
- ☐
|
|
|
|
Security Requirements
|
|
---------------------
|
|
|
|
.. list-table::
|
|
:header-rows: 1
|
|
:widths: 50 10
|
|
|
|
* - Requirement
|
|
- Status
|
|
* - Multi-factor authentication
|
|
- ☐
|
|
* - RBAC implementation
|
|
- ☐
|
|
* - Secure token management
|
|
- ☐
|
|
* - Input validation and sanitization
|
|
- ☐
|
|
* - Audit logging
|
|
- ☐
|
|
* - Rate limiting
|
|
- ☐
|
|
* - Data encryption
|
|
- ☐
|
|
* - Security headers configured
|
|
- ☐
|
|
|
|
Performance Standards
|
|
---------------------
|
|
|
|
.. list-table::
|
|
:header-rows: 1
|
|
:widths: 50 10
|
|
|
|
* - Requirement
|
|
- Status
|
|
* - Constant-time database queries
|
|
- ☐
|
|
* - Query optimization implemented
|
|
- ☐
|
|
* - Effective caching strategy
|
|
- ☐
|
|
* - Pagination performance optimized
|
|
- ☐
|
|
* - Response time monitoring
|
|
- ☐
|
|
* - Memory usage optimized
|
|
- ☐
|
|
* - Database indexing proper
|
|
- ☐
|
|
* - No N+1 query problems
|
|
- ☐
|
|
|
|
Testing Coverage
|
|
----------------
|
|
|
|
.. list-table::
|
|
:header-rows: 1
|
|
:widths: 50 10
|
|
|
|
* - Requirement
|
|
- Status
|
|
* - 100% endpoint test coverage
|
|
- ☐
|
|
* - Positive and negative test cases
|
|
- ☐
|
|
* - Permission testing complete
|
|
- ☐
|
|
* - Performance testing implemented
|
|
- ☐
|
|
* - Security testing coverage
|
|
- ☐
|
|
* - Integration testing
|
|
- ☐
|
|
* - Load testing completed
|
|
- ☐
|
|
* - Automated test execution
|
|
- ☐
|
|
|
|
----
|
|
|
|
References
|
|
==========
|
|
|
|
- **Django REST Framework Documentation**: https://www.django-rest-framework.org/
|
|
- **OpenAPI Specification**: https://swagger.io/specification/
|
|
- **HTTP Status Code Definitions**: https://tools.ietf.org/html/rfc7231#section-6
|
|
- **OAuth 2.0 Authorization Framework**: https://tools.ietf.org/html/rfc6749
|
|
- **JWT Best Practices**: https://tools.ietf.org/html/rfc8725
|
|
- **AWX Source Code**: https://github.com/ansible/awx
|
|
|
|
----
|
|
|
|
| **Document Maintainer**: API Development Team
|
|
| **Last Updated**: September 2025
|
|
| **Review Schedule**: Quarterly
|
|
| **Compliance Audit**: Monthly |