Initial commit
Dieser Commit ist enthalten in:
25
lizenzserver/services/auth/Dockerfile
Normale Datei
25
lizenzserver/services/auth/Dockerfile
Normale Datei
@ -0,0 +1,25 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5001
|
||||
|
||||
# Run with gunicorn
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5001", "--workers", "4", "--timeout", "120", "app:app"]
|
||||
279
lizenzserver/services/auth/app.py
Normale Datei
279
lizenzserver/services/auth/app.py
Normale Datei
@ -0,0 +1,279 @@
|
||||
import os
|
||||
import sys
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from functools import wraps
|
||||
from prometheus_flask_exporter import PrometheusMetrics
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from config import get_config
|
||||
from repositories.base import BaseRepository
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize Flask app
|
||||
app = Flask(__name__)
|
||||
config = get_config()
|
||||
app.config.from_object(config)
|
||||
CORS(app)
|
||||
|
||||
# Initialize Prometheus metrics
|
||||
metrics = PrometheusMetrics(app)
|
||||
metrics.info('auth_service_info', 'Auth Service Information', version='1.0.0')
|
||||
|
||||
# Initialize repository
|
||||
db_repo = BaseRepository(config.DATABASE_URL)
|
||||
|
||||
def create_token(payload: dict, expires_delta: timedelta) -> str:
|
||||
"""Create JWT token"""
|
||||
to_encode = payload.copy()
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
|
||||
|
||||
return jwt.encode(
|
||||
to_encode,
|
||||
config.JWT_SECRET,
|
||||
algorithm=config.JWT_ALGORITHM
|
||||
)
|
||||
|
||||
def decode_token(token: str) -> dict:
|
||||
"""Decode and validate JWT token"""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
config.JWT_SECRET,
|
||||
algorithms=[config.JWT_ALGORITHM]
|
||||
)
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise ValueError("Token has expired")
|
||||
except jwt.InvalidTokenError:
|
||||
raise ValueError("Invalid token")
|
||||
|
||||
def require_api_key(f):
|
||||
"""Decorator to require API key"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
api_key = request.headers.get('X-API-Key')
|
||||
|
||||
if not api_key:
|
||||
return jsonify({"error": "Missing API key"}), 401
|
||||
|
||||
# Validate API key
|
||||
query = """
|
||||
SELECT id, client_name, allowed_endpoints
|
||||
FROM api_clients
|
||||
WHERE api_key = %s AND is_active = true
|
||||
"""
|
||||
client = db_repo.execute_one(query, (api_key,))
|
||||
|
||||
if not client:
|
||||
return jsonify({"error": "Invalid API key"}), 401
|
||||
|
||||
# Check if endpoint is allowed
|
||||
endpoint = request.endpoint
|
||||
allowed = client.get('allowed_endpoints', [])
|
||||
if allowed and endpoint not in allowed:
|
||||
return jsonify({"error": "Endpoint not allowed"}), 403
|
||||
|
||||
# Add client info to request
|
||||
request.api_client = client
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""Health check endpoint"""
|
||||
return jsonify({
|
||||
"status": "healthy",
|
||||
"service": "auth",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
})
|
||||
|
||||
@app.route('/api/v1/auth/token', methods=['POST'])
|
||||
@require_api_key
|
||||
def create_access_token():
|
||||
"""Create access token for license validation"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'license_id' not in data:
|
||||
return jsonify({"error": "Missing license_id"}), 400
|
||||
|
||||
license_id = data['license_id']
|
||||
hardware_id = data.get('hardware_id')
|
||||
|
||||
# Verify license exists and is active
|
||||
query = """
|
||||
SELECT id, is_active, max_devices
|
||||
FROM licenses
|
||||
WHERE id = %s
|
||||
"""
|
||||
license = db_repo.execute_one(query, (license_id,))
|
||||
|
||||
if not license:
|
||||
return jsonify({"error": "License not found"}), 404
|
||||
|
||||
if not license['is_active']:
|
||||
return jsonify({"error": "License is not active"}), 403
|
||||
|
||||
# Create token payload
|
||||
payload = {
|
||||
"sub": license_id,
|
||||
"hwid": hardware_id,
|
||||
"client_id": request.api_client['id'],
|
||||
"type": "access"
|
||||
}
|
||||
|
||||
# Add features and limits based on license
|
||||
payload["features"] = data.get('features', [])
|
||||
payload["limits"] = {
|
||||
"api_calls": config.DEFAULT_RATE_LIMIT_PER_HOUR,
|
||||
"concurrent_sessions": config.MAX_CONCURRENT_SESSIONS
|
||||
}
|
||||
|
||||
# Create tokens
|
||||
access_token = create_token(payload, config.JWT_ACCESS_TOKEN_EXPIRES)
|
||||
|
||||
# Create refresh token
|
||||
refresh_payload = {
|
||||
"sub": license_id,
|
||||
"client_id": request.api_client['id'],
|
||||
"type": "refresh"
|
||||
}
|
||||
refresh_token = create_token(refresh_payload, config.JWT_REFRESH_TOKEN_EXPIRES)
|
||||
|
||||
return jsonify({
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": int(config.JWT_ACCESS_TOKEN_EXPIRES.total_seconds())
|
||||
})
|
||||
|
||||
@app.route('/api/v1/auth/refresh', methods=['POST'])
|
||||
def refresh_access_token():
|
||||
"""Refresh access token"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'refresh_token' not in data:
|
||||
return jsonify({"error": "Missing refresh_token"}), 400
|
||||
|
||||
try:
|
||||
# Decode refresh token
|
||||
payload = decode_token(data['refresh_token'])
|
||||
|
||||
if payload.get('type') != 'refresh':
|
||||
return jsonify({"error": "Invalid token type"}), 400
|
||||
|
||||
license_id = payload['sub']
|
||||
|
||||
# Verify license still active
|
||||
query = "SELECT is_active FROM licenses WHERE id = %s"
|
||||
license = db_repo.execute_one(query, (license_id,))
|
||||
|
||||
if not license or not license['is_active']:
|
||||
return jsonify({"error": "License is not active"}), 403
|
||||
|
||||
# Create new access token
|
||||
access_payload = {
|
||||
"sub": license_id,
|
||||
"client_id": payload['client_id'],
|
||||
"type": "access"
|
||||
}
|
||||
|
||||
access_token = create_token(access_payload, config.JWT_ACCESS_TOKEN_EXPIRES)
|
||||
|
||||
return jsonify({
|
||||
"access_token": access_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": int(config.JWT_ACCESS_TOKEN_EXPIRES.total_seconds())
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 401
|
||||
|
||||
@app.route('/api/v1/auth/verify', methods=['POST'])
|
||||
def verify_token():
|
||||
"""Verify token validity"""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return jsonify({"error": "Missing or invalid authorization header"}), 401
|
||||
|
||||
token = auth_header.split(' ')[1]
|
||||
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
|
||||
return jsonify({
|
||||
"valid": True,
|
||||
"license_id": payload['sub'],
|
||||
"expires_at": datetime.fromtimestamp(payload['exp']).isoformat()
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({
|
||||
"valid": False,
|
||||
"error": str(e)
|
||||
}), 401
|
||||
|
||||
@app.route('/api/v1/auth/api-key', methods=['POST'])
|
||||
def create_api_key():
|
||||
"""Create new API key (admin only)"""
|
||||
# This endpoint should be protected by admin authentication
|
||||
# For now, we'll use a simple secret header
|
||||
admin_secret = request.headers.get('X-Admin-Secret')
|
||||
|
||||
if admin_secret != os.getenv('ADMIN_SECRET', 'change-this-admin-secret'):
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'client_name' not in data:
|
||||
return jsonify({"error": "Missing client_name"}), 400
|
||||
|
||||
import secrets
|
||||
api_key = f"sk_{secrets.token_urlsafe(32)}"
|
||||
secret_key = secrets.token_urlsafe(64)
|
||||
|
||||
query = """
|
||||
INSERT INTO api_clients (client_name, api_key, secret_key, allowed_endpoints)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
allowed_endpoints = data.get('allowed_endpoints', [])
|
||||
client_id = db_repo.execute_insert(
|
||||
query,
|
||||
(data['client_name'], api_key, secret_key, allowed_endpoints)
|
||||
)
|
||||
|
||||
if not client_id:
|
||||
return jsonify({"error": "Failed to create API key"}), 500
|
||||
|
||||
return jsonify({
|
||||
"client_id": client_id,
|
||||
"api_key": api_key,
|
||||
"secret_key": secret_key,
|
||||
"client_name": data['client_name']
|
||||
}), 201
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return jsonify({"error": "Not found"}), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
logger.error(f"Internal error: {error}")
|
||||
return jsonify({"error": "Internal server error"}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||
15
lizenzserver/services/auth/config.py
Normale Datei
15
lizenzserver/services/auth/config.py
Normale Datei
@ -0,0 +1,15 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
def get_config():
|
||||
"""Get configuration from environment variables"""
|
||||
return {
|
||||
'DATABASE_URL': os.getenv('DATABASE_URL', 'postgresql://postgres:password@postgres:5432/v2_adminpanel'),
|
||||
'REDIS_URL': os.getenv('REDIS_URL', 'redis://redis:6379/1'),
|
||||
'JWT_SECRET': os.getenv('JWT_SECRET', 'dev-secret-key'),
|
||||
'JWT_ALGORITHM': 'HS256',
|
||||
'ACCESS_TOKEN_EXPIRE_MINUTES': 30,
|
||||
'REFRESH_TOKEN_EXPIRE_DAYS': 7,
|
||||
'FLASK_ENV': os.getenv('FLASK_ENV', 'production'),
|
||||
'LOG_LEVEL': os.getenv('LOG_LEVEL', 'INFO'),
|
||||
}
|
||||
9
lizenzserver/services/auth/requirements.txt
Normale Datei
9
lizenzserver/services/auth/requirements.txt
Normale Datei
@ -0,0 +1,9 @@
|
||||
flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
pyjwt==2.8.0
|
||||
psycopg2-binary==2.9.9
|
||||
redis==5.0.1
|
||||
python-dotenv==1.0.0
|
||||
gunicorn==21.2.0
|
||||
marshmallow==3.20.1
|
||||
prometheus-flask-exporter==0.23.0
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren