Add latest changes
Dieser Commit ist enthalten in:
652
RESSOURCE_API_PLAN.md
Normale Datei
652
RESSOURCE_API_PLAN.md
Normale Datei
@@ -0,0 +1,652 @@
|
||||
# Resource Management API Implementation Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This plan outlines the implementation of a **Dynamic Resource Pool API** for the License Server that allows client applications to:
|
||||
1. View available resources from their license's pool
|
||||
2. Reserve resources for their active session
|
||||
3. Automatically release resources when session ends
|
||||
4. Prevent resource conflicts between concurrent sessions
|
||||
|
||||
The system works like a "hotel room" model - resources are assigned to a license (the hotel), but individual sessions (guests) reserve specific resources during their stay.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Existing Infrastructure
|
||||
- **Database**: Complete resource management tables (`resource_pools`, `license_resources`, `resource_history`)
|
||||
- **Admin Panel**: Full CRUD operations for resource management
|
||||
- **License Server**: No resource endpoints (critical gap)
|
||||
- **Resource Types**: domain, ipv4, phone
|
||||
- **Limits**: 0-10 resources per type per license
|
||||
|
||||
### Gap Analysis
|
||||
- Clients cannot retrieve their assigned resources
|
||||
- No validation endpoint for resource ownership
|
||||
- Resource data not included in license activation/verification
|
||||
|
||||
## Proposed API Architecture
|
||||
|
||||
### Resource Pool Concept
|
||||
|
||||
**Key Principles**:
|
||||
- Resources belong to a license pool, but reservation rules differ by type
|
||||
- **Domains**: Can be shared (soft reservation) - prefer free, but allow sharing
|
||||
- **IPv4 & Phone**: Exclusive use (hard reservation) - one session only
|
||||
|
||||
```
|
||||
License Pool (Admin-defined)
|
||||
├── 5 Domains total
|
||||
│ ├── 2 Used by Session A (can be shared)
|
||||
│ ├── 1 Used by Session B (can be shared)
|
||||
│ ├── 1 Used by both A & B (shared)
|
||||
│ └── 1 Completely free
|
||||
├── 3 IPv4 Addresses total
|
||||
│ ├── 1 Reserved by Session A (exclusive)
|
||||
│ └── 2 Available
|
||||
└── 2 Phone Numbers total
|
||||
├── 1 Reserved by Session B (exclusive)
|
||||
└── 1 Available
|
||||
```
|
||||
|
||||
**Reservation Rules**:
|
||||
- **Domains**: `prefer_exclusive = true` - Try to get unused domain first, but allow sharing if needed
|
||||
- **IPv4**: `exclusive_only = true` - Fail if none available
|
||||
- **Phone**: `exclusive_only = true` - Fail if none available
|
||||
|
||||
### 1. New API Endpoints
|
||||
|
||||
#### GET /api/resources/available
|
||||
**Purpose**: Get available (unreserved) resources from the license pool
|
||||
|
||||
**Request Headers:**
|
||||
```
|
||||
X-API-Key: AF-2025-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
X-License-Key: XXXX-XXXX-XXXX-XXXX
|
||||
X-Session-Token: 550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"pool_status": {
|
||||
"domains": {
|
||||
"total": 5,
|
||||
"reserved": 3,
|
||||
"available": 2,
|
||||
"reserved_by_you": 1
|
||||
},
|
||||
"ipv4_addresses": {
|
||||
"total": 3,
|
||||
"reserved": 1,
|
||||
"available": 2,
|
||||
"reserved_by_you": 0
|
||||
},
|
||||
"phone_numbers": {
|
||||
"total": 2,
|
||||
"reserved": 0,
|
||||
"available": 2,
|
||||
"reserved_by_you": 0
|
||||
}
|
||||
},
|
||||
"available_resources": {
|
||||
"domains": [
|
||||
{
|
||||
"id": 124,
|
||||
"value": "example2.com",
|
||||
"usage_count": 0, // Completely free
|
||||
"is_shared": false
|
||||
},
|
||||
{
|
||||
"id": 125,
|
||||
"value": "example3.com",
|
||||
"usage_count": 1, // Already used by 1 session
|
||||
"is_shared": true
|
||||
}
|
||||
],
|
||||
"ipv4_addresses": [
|
||||
{
|
||||
"id": 457,
|
||||
"value": "192.168.1.101"
|
||||
},
|
||||
{
|
||||
"id": 458,
|
||||
"value": "192.168.1.102"
|
||||
}
|
||||
],
|
||||
"phone_numbers": [
|
||||
{
|
||||
"id": 789,
|
||||
"value": "+49123456789"
|
||||
},
|
||||
{
|
||||
"id": 790,
|
||||
"value": "+49123456790"
|
||||
}
|
||||
]
|
||||
},
|
||||
"your_reservations": {
|
||||
"domains": [
|
||||
{
|
||||
"id": 123,
|
||||
"value": "example.com",
|
||||
"reserved_at": "2025-01-15T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/resources/reserve
|
||||
**Purpose**: Reserve specific resources for the current session
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"resource_type": "domain",
|
||||
"resource_id": 124,
|
||||
"prefer_exclusive": true // Optional, default true for domains
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Domain - Exclusive):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"reservation": {
|
||||
"resource_id": 124,
|
||||
"resource_type": "domain",
|
||||
"resource_value": "example2.com",
|
||||
"session_token": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"reserved_at": "2025-01-15T10:30:00Z",
|
||||
"is_shared": false,
|
||||
"usage_count": 1
|
||||
},
|
||||
"message": "Domain reserved exclusively"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Domain - Shared):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"reservation": {
|
||||
"resource_id": 125,
|
||||
"resource_type": "domain",
|
||||
"resource_value": "example3.com",
|
||||
"session_token": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"reserved_at": "2025-01-15T10:30:00Z",
|
||||
"is_shared": true,
|
||||
"usage_count": 2 // Now used by 2 sessions
|
||||
},
|
||||
"message": "Domain reserved (shared with 1 other session)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (IPv4/Phone - Failed):**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Resource already exclusively reserved by another session",
|
||||
"code": "RESOURCE_UNAVAILABLE"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/resources/release
|
||||
**Purpose**: Release a reserved resource (or auto-release on session end)
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"resource_id": 124
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Resource released successfully"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/resources/my-reservations
|
||||
**Purpose**: Get all resources reserved by current session
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"reservations": {
|
||||
"domains": [
|
||||
{
|
||||
"id": 123,
|
||||
"value": "example.com",
|
||||
"reserved_at": "2025-01-15T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"ipv4_addresses": [],
|
||||
"phone_numbers": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/resources/validate
|
||||
**Purpose**: Validate if a specific resource belongs to the license
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"resource_type": "domain",
|
||||
"resource_value": "example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"resource_id": 123,
|
||||
"assigned_at": "2025-01-15T10:00:00Z",
|
||||
"message": "Resource is assigned to your license"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/resources/types
|
||||
**Purpose**: Get available resource types and current usage
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"resource_types": [
|
||||
{
|
||||
"type": "domain",
|
||||
"display_name": "Domains",
|
||||
"limit": 5,
|
||||
"used": 2,
|
||||
"available": 3,
|
||||
"validation_pattern": "^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\\.[a-zA-Z]{2,}$"
|
||||
},
|
||||
{
|
||||
"type": "ipv4",
|
||||
"display_name": "IPv4 Addresses",
|
||||
"limit": 3,
|
||||
"used": 1,
|
||||
"available": 2,
|
||||
"validation_pattern": "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$"
|
||||
},
|
||||
{
|
||||
"type": "phone",
|
||||
"display_name": "Phone Numbers",
|
||||
"limit": 2,
|
||||
"used": 0,
|
||||
"available": 2,
|
||||
"validation_pattern": "^\\+?[0-9]{1,15}$"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Enhanced Existing Endpoints
|
||||
|
||||
#### POST /api/license/activate (Enhanced)
|
||||
Add resource information to activation response:
|
||||
```json
|
||||
{
|
||||
"message": "License activated successfully",
|
||||
"activation": { ... },
|
||||
"resources": {
|
||||
"domains": ["example.com", "test.com"],
|
||||
"ipv4_addresses": ["192.168.1.100"],
|
||||
"phone_numbers": []
|
||||
},
|
||||
"resource_limits": {
|
||||
"domain_count": 5,
|
||||
"ipv4_count": 3,
|
||||
"phone_count": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/license/verify (Enhanced)
|
||||
Include resource summary:
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"license": { ... },
|
||||
"resources_assigned": {
|
||||
"domains": 2,
|
||||
"ipv4_addresses": 1,
|
||||
"phone_numbers": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Database Schema Extension
|
||||
|
||||
1. **New Table: resource_reservations**
|
||||
```sql
|
||||
CREATE TABLE resource_reservations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE,
|
||||
reserved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(resource_id, session_token)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reservations_session ON resource_reservations(session_token);
|
||||
CREATE INDEX idx_reservations_resource ON resource_reservations(resource_id);
|
||||
|
||||
-- View for usage counts
|
||||
CREATE VIEW resource_usage_counts AS
|
||||
SELECT
|
||||
rp.id,
|
||||
rp.resource_type,
|
||||
rp.resource_value,
|
||||
COUNT(rr.id) as usage_count,
|
||||
CASE
|
||||
WHEN rp.resource_type = 'domain' AND COUNT(rr.id) > 0 THEN true
|
||||
ELSE false
|
||||
END as is_shared
|
||||
FROM resource_pools rp
|
||||
LEFT JOIN resource_reservations rr ON rp.id = rr.resource_id
|
||||
WHERE rp.status = 'allocated'
|
||||
GROUP BY rp.id, rp.resource_type, rp.resource_value;
|
||||
```
|
||||
|
||||
2. **Automatic Cleanup Trigger**
|
||||
```sql
|
||||
-- When session ends, release all reserved resources
|
||||
CREATE OR REPLACE FUNCTION release_session_resources()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
DELETE FROM resource_reservations
|
||||
WHERE session_token = OLD.session_token;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER cleanup_session_resources
|
||||
AFTER DELETE ON license_sessions
|
||||
FOR EACH ROW EXECUTE FUNCTION release_session_resources();
|
||||
```
|
||||
|
||||
### Phase 2: License Server Backend (Week 1)
|
||||
|
||||
1. **Create Resource Models** (`v2_lizenzserver/app/models/resource.py`)
|
||||
```python
|
||||
class ResourceReservation(BaseModel):
|
||||
id: int
|
||||
resource_id: int
|
||||
resource_type: ResourceType
|
||||
value: str
|
||||
session_token: str
|
||||
reserved_at: datetime
|
||||
|
||||
class PoolStatus(BaseModel):
|
||||
total: int
|
||||
reserved: int
|
||||
available: int
|
||||
reserved_by_you: int
|
||||
```
|
||||
|
||||
2. **Create Resource Schemas** (`v2_lizenzserver/app/schemas/resource.py`)
|
||||
```python
|
||||
class ResourcesResponse(BaseModel):
|
||||
success: bool
|
||||
resources: Dict[str, List[AssignedResource]]
|
||||
limits: Dict[str, int]
|
||||
```
|
||||
|
||||
3. **Create Resource Service** (`v2_lizenzserver/app/services/resource_service.py`)
|
||||
- Query resource_pools and license_resources tables
|
||||
- Implement caching for performance
|
||||
- Add resource validation logic
|
||||
|
||||
4. **Create Resource API Routes** (`v2_lizenzserver/app/api/resource.py`)
|
||||
- Implement all new endpoints
|
||||
- Add proper error handling
|
||||
- Include rate limiting
|
||||
|
||||
### Phase 2: Security & Authentication (Week 1)
|
||||
|
||||
1. **Multi-Factor Authentication**
|
||||
- Require API Key + License Key + Session Token
|
||||
- Validate session is active and belongs to license
|
||||
- Rate limit: 100 requests/minute per license
|
||||
|
||||
2. **Data Access Control**
|
||||
- Resources only visible to owning license
|
||||
- No cross-license data leakage
|
||||
- Audit log all resource API access
|
||||
|
||||
3. **Caching Strategy**
|
||||
- Cache resource assignments for 5 minutes
|
||||
- Invalidate on Admin Panel changes
|
||||
- Use Redis if available, in-memory fallback
|
||||
|
||||
### Phase 3: Integration & Testing (Week 2)
|
||||
|
||||
1. **Integration Tests**
|
||||
- Test all resource endpoints
|
||||
- Verify security boundaries
|
||||
- Load test with multiple concurrent requests
|
||||
|
||||
2. **Client SDK Updates**
|
||||
- Update Python client example
|
||||
- Update C# client example
|
||||
- Create resource caching example
|
||||
|
||||
3. **Documentation**
|
||||
- Update API_REFERENCE.md
|
||||
- Create resource API examples
|
||||
- Add troubleshooting guide
|
||||
|
||||
### Phase 4: Monitoring & Optimization (Week 2)
|
||||
|
||||
1. **Metrics**
|
||||
- Resource API request count
|
||||
- Cache hit/miss ratio
|
||||
- Response time percentiles
|
||||
|
||||
2. **Performance Optimization**
|
||||
- Database query optimization
|
||||
- Add indexes if needed
|
||||
- Implement connection pooling
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Authentication Layers**
|
||||
- API Key (system-level)
|
||||
- License Key (license-level)
|
||||
- Session Token (session-level)
|
||||
|
||||
2. **Rate Limiting**
|
||||
- Per-license: 100 req/min
|
||||
- Per-IP: 1000 req/min
|
||||
- Burst allowance: 10 requests
|
||||
|
||||
3. **Data Isolation**
|
||||
- Strict license-based filtering
|
||||
- No enumeration attacks possible
|
||||
- Resource IDs not sequential
|
||||
|
||||
4. **Audit Trail**
|
||||
- Log all resource API access
|
||||
- Track abnormal access patterns
|
||||
- Alert on suspicious activity
|
||||
|
||||
## Client Integration Guide
|
||||
|
||||
### Python Example
|
||||
```python
|
||||
class AccountForgerClient:
|
||||
def __init__(self):
|
||||
self.reserved_resources = {}
|
||||
|
||||
def get_available_resources(self):
|
||||
"""Get available resources from the pool"""
|
||||
headers = self._get_headers()
|
||||
response = requests.get(
|
||||
f"{self.base_url}/api/resources/available",
|
||||
headers=headers
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def reserve_resource(self, resource_type, resource_id):
|
||||
"""Reserve a specific resource for this session"""
|
||||
headers = self._get_headers()
|
||||
data = {
|
||||
'resource_type': resource_type,
|
||||
'resource_id': resource_id
|
||||
}
|
||||
response = requests.post(
|
||||
f"{self.base_url}/api/resources/reserve",
|
||||
headers=headers,
|
||||
json=data
|
||||
)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
# Cache locally for quick access
|
||||
if resource_type not in self.reserved_resources:
|
||||
self.reserved_resources[resource_type] = []
|
||||
self.reserved_resources[resource_type].append(result['reservation'])
|
||||
return response.json()
|
||||
|
||||
def release_resource(self, resource_id):
|
||||
"""Release a reserved resource"""
|
||||
headers = self._get_headers()
|
||||
data = {'resource_id': resource_id}
|
||||
response = requests.post(
|
||||
f"{self.base_url}/api/resources/release",
|
||||
headers=headers,
|
||||
json=data
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def get_my_reservations(self):
|
||||
"""Get all my reserved resources"""
|
||||
headers = self._get_headers()
|
||||
response = requests.get(
|
||||
f"{self.base_url}/api/resources/my-reservations",
|
||||
headers=headers
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def auto_reserve_resources(self, needed):
|
||||
"""Automatically reserve needed resources with smart domain selection"""
|
||||
# Example: needed = {'domains': 2, 'ipv4_addresses': 1}
|
||||
available = self.get_available_resources()
|
||||
reserved = {}
|
||||
|
||||
for resource_type, count in needed.items():
|
||||
reserved[resource_type] = []
|
||||
available_list = available['available_resources'].get(resource_type, [])
|
||||
|
||||
if resource_type == 'domain':
|
||||
# For domains: prefer free ones, but take shared if needed
|
||||
# Sort by usage_count (free domains first)
|
||||
sorted_domains = sorted(available_list, key=lambda x: x.get('usage_count', 0))
|
||||
|
||||
for domain in sorted_domains[:count]:
|
||||
result = self.reserve_resource('domain', domain['id'])
|
||||
if result['success']:
|
||||
reserved['domains'].append(result['reservation'])
|
||||
else:
|
||||
# For IPv4/Phone: only take truly available ones
|
||||
for i in range(min(count, len(available_list))):
|
||||
resource = available_list[i]
|
||||
result = self.reserve_resource(resource_type, resource['id'])
|
||||
if result['success']:
|
||||
reserved[resource_type].append(result['reservation'])
|
||||
else:
|
||||
# Stop trying if we hit exclusive reservation error
|
||||
break
|
||||
|
||||
return reserved
|
||||
```
|
||||
|
||||
### Resource Reservation Flow Example
|
||||
```python
|
||||
# 1. Start session
|
||||
session = client.start_session(license_key)
|
||||
|
||||
# 2. Check available resources
|
||||
available = client.get_available_resources()
|
||||
print(f"Available domains: {available['pool_status']['domains']['available']}")
|
||||
|
||||
# 3. Reserve what you need
|
||||
if available['available_resources']['domains']:
|
||||
domain = available['available_resources']['domains'][0]
|
||||
client.reserve_resource('domain', domain['id'])
|
||||
|
||||
# 4. Use the resources...
|
||||
|
||||
# 5. Resources auto-released when session ends
|
||||
client.end_session() # Triggers automatic cleanup
|
||||
```
|
||||
|
||||
### Handling Resource Conflicts
|
||||
```python
|
||||
def reserve_with_retry(client, resource_type, resource_id, max_retries=3):
|
||||
"""Handle race conditions when multiple sessions reserve simultaneously"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
result = client.reserve_resource(resource_type, resource_id)
|
||||
if result['success']:
|
||||
return result
|
||||
except Exception as e:
|
||||
if 'already reserved' in str(e) and attempt < max_retries - 1:
|
||||
# Get fresh list of available resources
|
||||
available = client.get_available_resources()
|
||||
# Pick different resource
|
||||
continue
|
||||
raise
|
||||
return None
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
### Required Indexes
|
||||
```sql
|
||||
-- Optimize resource queries
|
||||
CREATE INDEX idx_license_resources_active_license
|
||||
ON license_resources(license_id, is_active)
|
||||
WHERE is_active = TRUE;
|
||||
|
||||
CREATE INDEX idx_resource_pools_allocated
|
||||
ON resource_pools(allocated_to_license, resource_type)
|
||||
WHERE status = 'allocated';
|
||||
```
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
1. **Week 1**: Backend implementation + Security
|
||||
2. **Week 2**: Testing + Client integration
|
||||
3. **Week 3**: Staged rollout (10% → 50% → 100%)
|
||||
4. **Week 4**: Monitoring + Optimization
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- API response time < 100ms (p95)
|
||||
- Cache hit ratio > 80%
|
||||
- Zero security incidents
|
||||
- Client adoption > 90% within 30 days
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
1. **Performance Risk**: Pre-emptive caching and query optimization
|
||||
2. **Security Risk**: Multi-layer authentication and rate limiting
|
||||
3. **Compatibility Risk**: Maintain backward compatibility
|
||||
4. **Scalability Risk**: Horizontal scaling ready architecture
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Webhook Notifications**: Notify clients of resource changes
|
||||
2. **Resource Usage Analytics**: Track actual resource utilization
|
||||
3. **Dynamic Resource Allocation**: Auto-assign based on usage patterns
|
||||
4. **Resource Sharing**: Allow resource sharing between licenses
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren