firstpass of certs
This commit is contained in:
@@ -29,6 +29,7 @@ from .api.permission_routes import *
|
||||
from .api.auditlog_routes import *
|
||||
from .api.volume_routes import *
|
||||
from .api.workload_container_pods_routes import *
|
||||
from .api.certificate_routes import *
|
||||
|
||||
|
||||
@api_bp.route('/healthz', methods=['GET'])
|
||||
|
||||
@@ -0,0 +1,618 @@
|
||||
"""
|
||||
Certificate API Routes
|
||||
|
||||
This module provides API routes for managing Certificate Authorities and Certificates.
|
||||
"""
|
||||
|
||||
from flask import request, jsonify
|
||||
from app import db, logger
|
||||
from app.models.certificate_models import CertificateAuthority, Certificate
|
||||
from app.models.models import Project
|
||||
from app.utils.certificate_utils import (
|
||||
generate_self_signed_ca,
|
||||
issue_certificate,
|
||||
revoke_certificate,
|
||||
encrypt_private_key,
|
||||
decrypt_private_key
|
||||
)
|
||||
from app.controller import api_bp
|
||||
from app.utils.standard_responses import api_response
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
@api_bp.route('/certificates/ca/project/<project_id>', methods=['GET'])
|
||||
def get_ca_for_project(project_id):
|
||||
"""
|
||||
Get the CA for a project if it exists.
|
||||
|
||||
Args:
|
||||
project_id (str): ID of the project
|
||||
|
||||
Returns:
|
||||
JSON response with CA details or empty response if no CA exists
|
||||
"""
|
||||
try:
|
||||
# Check if project exists
|
||||
project = Project.query.get_or_404(project_id)
|
||||
|
||||
# Check if CA already exists for this project
|
||||
ca = CertificateAuthority.query.filter_by(project_id=project_id, deleted=False).first()
|
||||
|
||||
if not ca:
|
||||
return api_response(
|
||||
success=False,
|
||||
status=404,
|
||||
message="No Certificate Authority found for this project",
|
||||
error_type="NOT_FOUND"
|
||||
)
|
||||
|
||||
return api_response(data=ca.to_json(), message="CA retrieved successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting CA for project {project_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/ca/project/<project_id>', methods=['POST'])
|
||||
def create_ca_for_project(project_id):
|
||||
"""
|
||||
Create a new CA for a project.
|
||||
|
||||
Args:
|
||||
project_id (str): ID of the project
|
||||
|
||||
Returns:
|
||||
JSON response with CA details
|
||||
"""
|
||||
try:
|
||||
# Check if project exists
|
||||
project = Project.query.get_or_404(project_id)
|
||||
|
||||
# Check if CA already exists for this project
|
||||
ca = CertificateAuthority.query.filter_by(project_id=project_id, deleted=False).first()
|
||||
|
||||
if ca:
|
||||
return api_response(
|
||||
success=False,
|
||||
status=409,
|
||||
message="Certificate Authority already exists for this project",
|
||||
error_type="CONFLICT"
|
||||
)
|
||||
|
||||
# Create a new CA
|
||||
common_name = f"rootca.{project_id}.xcloudify.tech"
|
||||
ca_data = generate_self_signed_ca(
|
||||
common_name=common_name,
|
||||
organization="xCloudify",
|
||||
organizational_unit="IT",
|
||||
validity_years=5
|
||||
)
|
||||
|
||||
# Encrypt the private key
|
||||
encrypted_private_key = encrypt_private_key(ca_data["private_key"], project_id)
|
||||
|
||||
ca = CertificateAuthority(
|
||||
name="",
|
||||
project_id=project_id,
|
||||
common_name=common_name,
|
||||
organization="xCloudify",
|
||||
organizational_unit="IT",
|
||||
dns_name=common_name,
|
||||
validity_period=5,
|
||||
is_active=True,
|
||||
private_key=encrypted_private_key, # Store encrypted private key
|
||||
certificate_data=ca_data["certificate"] # Store certificate
|
||||
)
|
||||
|
||||
db.session.add(ca)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Created new CA for project {project_id}")
|
||||
|
||||
return api_response(
|
||||
data=ca.to_json(),
|
||||
status=201,
|
||||
message="CA created successfully"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating CA for project {project_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/ca/<ca_id>', methods=['GET'])
|
||||
def get_ca_details(ca_id):
|
||||
"""
|
||||
Get detailed information about a specific CA.
|
||||
|
||||
Args:
|
||||
ca_id (str): ID of the CA
|
||||
|
||||
Returns:
|
||||
JSON response with CA details
|
||||
"""
|
||||
try:
|
||||
ca = CertificateAuthority.query.get_or_404(ca_id)
|
||||
return api_response(data=ca.to_json())
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting CA details for {ca_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/ca/<ca_id>', methods=['PUT'])
|
||||
def update_ca(ca_id):
|
||||
"""
|
||||
Update CA information.
|
||||
|
||||
Args:
|
||||
ca_id (str): ID of the CA
|
||||
|
||||
Returns:
|
||||
JSON response confirming update
|
||||
"""
|
||||
try:
|
||||
ca = CertificateAuthority.query.get_or_404(ca_id)
|
||||
data = request.json
|
||||
|
||||
# Update allowed fields
|
||||
if 'common_name' in data:
|
||||
ca.common_name = data['common_name']
|
||||
if 'country' in data:
|
||||
ca.country = data['country']
|
||||
if 'state' in data:
|
||||
ca.state = data['state']
|
||||
if 'city' in data:
|
||||
ca.city = data['city']
|
||||
if 'organization' in data:
|
||||
ca.organization = data['organization']
|
||||
if 'organizational_unit' in data:
|
||||
ca.organizational_unit = data['organizational_unit']
|
||||
if 'is_active' in data:
|
||||
ca.is_active = data['is_active']
|
||||
|
||||
ca.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return api_response(message="CA updated successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating CA {ca_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/issue', methods=['POST'])
|
||||
def create_certificate():
|
||||
"""
|
||||
Issue a new certificate from a CA.
|
||||
|
||||
Returns:
|
||||
JSON response with certificate details
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ['ca_id', 'certificate_type', 'common_name','name']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return api_response(
|
||||
success=False,
|
||||
status=400,
|
||||
message="Validation error",
|
||||
error_type="VALIDATION_ERROR",
|
||||
error_details={"errors": [f"Missing required field: {field}"]}
|
||||
)
|
||||
|
||||
# Get CA
|
||||
ca = CertificateAuthority.query.get_or_404(data['ca_id'])
|
||||
|
||||
# Decrypt CA private key (in a real implementation, this would require additional authentication)
|
||||
# For this example, we'll use the project_id as the password
|
||||
decrypted_ca_private_key = decrypt_private_key(ca.private_key, ca.project_id)
|
||||
|
||||
# Issue certificate
|
||||
cert_data = issue_certificate(
|
||||
ca_private_key_pem=decrypted_ca_private_key,
|
||||
ca_cert_pem=ca.certificate_data,
|
||||
common_name=data['common_name'],
|
||||
certificate_type=data['certificate_type'],
|
||||
country=data.get('country'),
|
||||
state=data.get('state'),
|
||||
city=data.get('city'),
|
||||
organization=data.get('organization'),
|
||||
organizational_unit=data.get('organizational_unit'),
|
||||
email=data.get('email'),
|
||||
validity_years=data.get('validity_period', 1)
|
||||
)
|
||||
|
||||
# Encrypt the private key
|
||||
encrypted_private_key = encrypt_private_key(cert_data["private_key"], ca.project_id)
|
||||
|
||||
# Create certificate record
|
||||
cert = Certificate(
|
||||
name=data['name'],
|
||||
ca_id=data['ca_id'],
|
||||
certificate_type=data['certificate_type'],
|
||||
common_name=data['common_name'],
|
||||
country=data.get('country'),
|
||||
state=data.get('state'),
|
||||
city=data.get('city'),
|
||||
organization=data.get('organization'),
|
||||
organizational_unit=data.get('organizational_unit'),
|
||||
email=data.get('email'),
|
||||
validity_period=data.get('validity_period', 1),
|
||||
is_active=True,
|
||||
revoked=False,
|
||||
public_key=cert_data["public_key"],
|
||||
private_key=encrypted_private_key,
|
||||
certificate_data=cert_data["certificate"]
|
||||
)
|
||||
|
||||
db.session.add(cert)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Created new certificate {cert.id} for CA {ca.id}")
|
||||
|
||||
return api_response(
|
||||
data=cert.to_json(),
|
||||
status=201,
|
||||
message="Certificate created successfully"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating certificate: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/cert/ca/<ca_id>', methods=['GET'])
|
||||
def list_certificates_for_ca(ca_id):
|
||||
"""
|
||||
List all certificates issued by a specific CA.
|
||||
|
||||
Args:
|
||||
ca_id (str): ID of the CA
|
||||
|
||||
Returns:
|
||||
JSON response with list of certificates
|
||||
"""
|
||||
try:
|
||||
# Check if CA exists
|
||||
ca = CertificateAuthority.query.get_or_404(ca_id)
|
||||
|
||||
|
||||
# Get certificates for this CA
|
||||
certificates = Certificate.query.filter_by(ca_id=ca_id, deleted=False).all()
|
||||
return api_response(
|
||||
data=[cert.to_json() for cert in certificates],
|
||||
message=f"Found {len(certificates)} certificates for CA {ca_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing certificates for CA {ca_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/cert/<cert_id>', methods=['GET'])
|
||||
def get_certificate_details(cert_id):
|
||||
"""
|
||||
Get detailed information about a specific certificate.
|
||||
|
||||
Args:
|
||||
cert_id (str): ID of the certificate
|
||||
|
||||
Returns:
|
||||
JSON response with certificate details
|
||||
"""
|
||||
try:
|
||||
cert = Certificate.query.get_or_404(cert_id)
|
||||
return api_response(data=cert.to_json())
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting certificate details for {cert_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/cert/<cert_id>', methods=['PUT'])
|
||||
def update_certificate(cert_id):
|
||||
"""
|
||||
Update certificate information.
|
||||
|
||||
Args:
|
||||
cert_id (str): ID of the certificate
|
||||
|
||||
Returns:
|
||||
JSON response confirming update
|
||||
"""
|
||||
try:
|
||||
cert = Certificate.query.get_or_404(cert_id)
|
||||
data = request.json
|
||||
|
||||
# Update allowed fields
|
||||
if 'is_active' in data:
|
||||
cert.is_active = data['is_active']
|
||||
|
||||
cert.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return api_response(message="Certificate updated successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating certificate {cert_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/cert/<cert_id>/revoke', methods=['POST'])
|
||||
def revoke_certificate_route(cert_id):
|
||||
"""
|
||||
Revoke a certificate.
|
||||
|
||||
Args:
|
||||
cert_id (str): ID of the certificate
|
||||
|
||||
Returns:
|
||||
JSON response confirming revocation
|
||||
"""
|
||||
try:
|
||||
cert = Certificate.query.get_or_404(cert_id)
|
||||
|
||||
# Check if certificate is already revoked
|
||||
if cert.revoked:
|
||||
return api_response(
|
||||
success=False,
|
||||
status=400,
|
||||
message="Certificate already revoked",
|
||||
error_type="VALIDATION_ERROR"
|
||||
)
|
||||
|
||||
# Get CA
|
||||
ca = cert.ca
|
||||
|
||||
# Decrypt CA private key (in a real implementation, this would require additional authentication)
|
||||
# For this example, we'll use the project_id as the password
|
||||
decrypted_ca_private_key = decrypt_private_key(ca.private_key, ca.project_id)
|
||||
|
||||
# Revoke certificate and generate CRL
|
||||
crl_data = revoke_certificate(
|
||||
ca_private_key_pem=decrypted_ca_private_key,
|
||||
ca_cert_pem=ca.certificate_data,
|
||||
cert_pem=cert.certificate_data,
|
||||
crl_number=1 # In a real implementation, this should be incremented
|
||||
)
|
||||
|
||||
# Update certificate status
|
||||
cert.revoked = True
|
||||
cert.revoked_at = datetime.utcnow()
|
||||
|
||||
# Create or update CRL record
|
||||
# In a real implementation, you would store the CRL in the database
|
||||
# For this example, we'll just log it
|
||||
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Revoked certificate {cert_id}")
|
||||
|
||||
return api_response(message="Certificate revoked successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error revoking certificate {cert_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/cert/<cert_id>/download', methods=['GET'])
|
||||
def download_certificate(cert_id):
|
||||
"""
|
||||
Download a certificate's public or private key.
|
||||
|
||||
Args:
|
||||
cert_id (str): ID of the certificate
|
||||
|
||||
Returns:
|
||||
PEM-formatted certificate data
|
||||
"""
|
||||
try:
|
||||
# Check if we're downloading a CA certificate
|
||||
is_ca = request.args.get('ca', 'false').lower() == 'true'
|
||||
|
||||
if is_ca:
|
||||
# Download CA certificate
|
||||
ca = CertificateAuthority.query.get_or_404(cert_id)
|
||||
cert_type = request.args.get('type', 'certificate') # default to certificate
|
||||
|
||||
if cert_type == 'certificate':
|
||||
# Return CA certificate
|
||||
return ca.certificate_data, 200, {'Content-Type': 'application/x-pem-file'}
|
||||
elif cert_type == 'private_key':
|
||||
# Return CA private key (in a real implementation, this would require additional authentication)
|
||||
# For this example, we'll decrypt and return it
|
||||
decrypted_private_key = decrypt_private_key(ca.private_key, ca.project_id)
|
||||
return decrypted_private_key, 200, {'Content-Type': 'application/x-pem-file'}
|
||||
else:
|
||||
return api_response(
|
||||
success=False,
|
||||
status=400,
|
||||
message="Invalid type parameter for CA",
|
||||
error_type="VALIDATION_ERROR",
|
||||
error_details={"errors": ["type must be 'certificate' or 'private_key' for CA"]}
|
||||
)
|
||||
else:
|
||||
# Download regular certificate
|
||||
cert = Certificate.query.get_or_404(cert_id)
|
||||
cert_type = request.args.get('type', 'certificate') # default to certificate
|
||||
|
||||
if cert_type == 'certificate':
|
||||
# Return certificate
|
||||
return cert.certificate_data, 200, {'Content-Type': 'application/x-pem-file'}
|
||||
elif cert_type == 'private_key':
|
||||
# Return private key (in a real implementation, this would require additional authentication)
|
||||
# For this example, we'll decrypt and return it
|
||||
decrypted_private_key = decrypt_private_key(cert.private_key, cert.ca.project_id)
|
||||
return decrypted_private_key, 200, {'Content-Type': 'application/x-pem-file'}
|
||||
elif cert_type == 'public_key':
|
||||
# Return public key
|
||||
return cert.public_key, 200, {'Content-Type': 'application/x-pem-file'}
|
||||
else:
|
||||
return api_response(
|
||||
success=False,
|
||||
status=400,
|
||||
message="Invalid type parameter",
|
||||
error_type="VALIDATION_ERROR",
|
||||
error_details={"errors": ["type must be 'certificate', 'private_key', or 'public_key'"]}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading certificate {cert_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/ca', methods=['GET'])
|
||||
def list_all_cas():
|
||||
"""
|
||||
Get all Certificate Authorities.
|
||||
|
||||
Returns:
|
||||
JSON response with list of all CAs
|
||||
"""
|
||||
try:
|
||||
# Get all CAs
|
||||
cas = CertificateAuthority.query.filter_by(deleted=False).all()
|
||||
|
||||
return api_response(
|
||||
data=[ca.to_json() for ca in cas],
|
||||
message=f"Found {len(cas)} Certificate Authorities"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing all CAs: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route('/certificates/cert/project/<project_id>', methods=['GET'])
|
||||
def list_certificates_for_project(project_id):
|
||||
"""
|
||||
List all certificates for a specific project.
|
||||
|
||||
Args:
|
||||
project_id (str): ID of the project
|
||||
|
||||
Returns:
|
||||
JSON response with list of certificates for the project
|
||||
"""
|
||||
try:
|
||||
# Check if project exists
|
||||
project = Project.query.get_or_404(project_id)
|
||||
|
||||
# Get CA for this project
|
||||
ca = CertificateAuthority.query.filter_by(project_id=project_id, deleted=False).first()
|
||||
if not ca:
|
||||
return api_response(
|
||||
success=False,
|
||||
status=404,
|
||||
message="No Certificate Authority found for this project",
|
||||
error_type="NOT_FOUND"
|
||||
)
|
||||
|
||||
# Get certificates for this CA
|
||||
certificates = Certificate.query.filter_by(ca_id=ca.id, deleted=False).all()
|
||||
return api_response(
|
||||
data=[cert.to_json() for cert in certificates],
|
||||
message=f"Found {len(certificates)} certificates for project {project_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing certificates for project {project_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@api_bp.route('/certificates/crl/ca/<ca_id>', methods=['GET'])
|
||||
def get_crl(ca_id):
|
||||
"""
|
||||
Get the current CRL for a CA.
|
||||
|
||||
Args:
|
||||
ca_id (str): ID of the CA
|
||||
|
||||
Returns:
|
||||
PEM-formatted CRL data
|
||||
"""
|
||||
try:
|
||||
ca = CertificateAuthority.query.get_or_404(ca_id)
|
||||
|
||||
# In a real implementation, you would retrieve the current CRL from storage
|
||||
# For this example, we'll return a placeholder
|
||||
crl_data = "-----BEGIN X509 CRL-----\n"
|
||||
crl_data += "Placeholder CRL data\n"
|
||||
crl_data += "-----END X509 CRL-----\n"
|
||||
|
||||
return crl_data, 200, {'Content-Type': 'application/x-pem-file'}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting CRL for CA {ca_id}: {str(e)}")
|
||||
return api_response(
|
||||
success=False,
|
||||
status=500,
|
||||
message="Internal server error",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_details={"detail": str(e)}
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Certificate Authority and Certificate Models
|
||||
|
||||
This module contains the database models for Certificate Authorities and Certificates.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, Integer, Text
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.models.models import BaseModel
|
||||
|
||||
|
||||
class CertificateAuthority(BaseModel):
|
||||
__tablename__ = "certificate_authorities"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
project_id = Column(String(36), ForeignKey("projects.id"), nullable=False, unique=True)
|
||||
common_name = Column(String(255), nullable=False)
|
||||
country = Column(String(2), nullable=True)
|
||||
state = Column(String(255), nullable=True)
|
||||
city = Column(String(255), nullable=True)
|
||||
organization = Column(String(255), nullable=True)
|
||||
organizational_unit = Column(String(255), nullable=True)
|
||||
dns_name = Column(String(255), nullable=True)
|
||||
validity_period = Column(Integer, default=5, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
private_key = Column(LONGTEXT, nullable=True) # Encrypted private key
|
||||
certificate_data = Column(LONGTEXT, nullable=True) # The actual certificate
|
||||
|
||||
# Relationships
|
||||
project = relationship("Project", backref="certificate_authority")
|
||||
certificates = relationship("Certificate", back_populates="ca")
|
||||
crls = relationship("CertificateRevocationList", back_populates="ca")
|
||||
|
||||
def to_json(self):
|
||||
"""Convert the CertificateAuthority object to a JSON-serializable dictionary"""
|
||||
result = super().to_json()
|
||||
# Remove fields that are not needed in the response
|
||||
result.pop('private_key', None)
|
||||
result.pop('certificate_data', None)
|
||||
result.pop('created_by', None)
|
||||
result.pop('visible', None)
|
||||
result.pop('public_key', None)
|
||||
result["created_at"] = self.created_at.isoformat() if self.created_at else None
|
||||
result["updated_at"] = self.updated_at.isoformat() if self.updated_at else None
|
||||
return result
|
||||
|
||||
|
||||
class Certificate(BaseModel):
|
||||
__tablename__ = "certificates"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
ca_id = Column(String(36), ForeignKey("certificate_authorities.id"), nullable=False)
|
||||
certificate_type = Column(String(50), nullable=False) # server, client, code_signing, etc.
|
||||
common_name = Column(String(255), nullable=False)
|
||||
country = Column(String(2), nullable=True)
|
||||
state = Column(String(255), nullable=True)
|
||||
city = Column(String(255), nullable=True)
|
||||
organization = Column(String(255), nullable=True)
|
||||
organizational_unit = Column(String(255), nullable=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
validity_period = Column(Integer, default=1, nullable=False) # Years
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
revoked = Column(Boolean, default=False, nullable=False)
|
||||
revoked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Certificate data (encrypted)
|
||||
public_key = Column(LONGTEXT, nullable=True)
|
||||
private_key = Column(LONGTEXT, nullable=True) # This should be encrypted at rest
|
||||
certificate_data = Column(LONGTEXT, nullable=True) # The actual certificate
|
||||
|
||||
# Relationships
|
||||
ca = relationship("CertificateAuthority", back_populates="certificates")
|
||||
|
||||
def to_json(self):
|
||||
"""Convert the Certificate object to a JSON-serializable dictionary"""
|
||||
result = super().to_json()
|
||||
# Remove fields that are not needed in the response
|
||||
result.pop('public_key', None)
|
||||
result.pop('private_key', None)
|
||||
result.pop('created_by', None)
|
||||
result.pop('visible', None)
|
||||
result.pop('certificate_data', None)
|
||||
result["revoked_at"] = self.revoked_at.isoformat() if self.revoked_at else None
|
||||
result["created_at"] = self.created_at.isoformat() if self.created_at else None
|
||||
result["updated_at"] = self.updated_at.isoformat() if self.updated_at else None
|
||||
return result
|
||||
|
||||
|
||||
class CertificateRevocationList(BaseModel):
|
||||
__tablename__ = "certificate_revocation_lists"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
ca_id = Column(String(36), ForeignKey("certificate_authorities.id"), nullable=False)
|
||||
crl_number = Column(Integer, nullable=False)
|
||||
crl_data = Column(LONGTEXT, nullable=True) # The actual CRL data
|
||||
next_update = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
# Relationships
|
||||
ca = relationship("CertificateAuthority", back_populates="crls")
|
||||
|
||||
def to_json(self):
|
||||
"""Convert the CertificateRevocationList object to a JSON-serializable dictionary"""
|
||||
result = super().to_json()
|
||||
# Remove fields that are not needed in the response
|
||||
result.pop('description', None)
|
||||
result.pop('status', None)
|
||||
result.pop('created_by', None)
|
||||
result.pop('visible', None)
|
||||
result.pop('name', None)
|
||||
result["next_update"] = self.next_update.isoformat() if self.next_update else None
|
||||
result["created_at"] = self.created_at.isoformat() if self.created_at else None
|
||||
result["updated_at"] = self.updated_at.isoformat() if self.updated_at else None
|
||||
return result
|
||||
@@ -16,4 +16,5 @@ python-dotenv
|
||||
PyJWT
|
||||
apiflask
|
||||
marshmallow
|
||||
marshmallow_sqlalchemy
|
||||
marshmallow_sqlalchemy
|
||||
cryptography
|
||||
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
Certificate Utility Functions
|
||||
|
||||
This module provides functions for generating self-signed CA certificates,
|
||||
issuing certificates, and managing certificate revocation using the
|
||||
cryptography library.
|
||||
"""
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
from app import logger
|
||||
|
||||
|
||||
def generate_self_signed_ca(common_name, country=None, state=None, city=None,
|
||||
organization=None, organizational_unit=None,
|
||||
validity_years=5):
|
||||
"""
|
||||
Generate a self-signed CA certificate.
|
||||
|
||||
Args:
|
||||
common_name (str): Common name for the certificate (e.g., rootca.projectid.xcloudify.tech)
|
||||
country (str, optional): Country code (2 letters)
|
||||
state (str, optional): State or province
|
||||
city (str, optional): City or locality
|
||||
organization (str, optional): Organization name
|
||||
organizational_unit (str, optional): Organizational unit
|
||||
validity_years (int): Number of years the certificate is valid (default: 5)
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing the private key, certificate, and public key
|
||||
"""
|
||||
# Generate private key
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=2048,
|
||||
)
|
||||
|
||||
# Create subject name
|
||||
subject_name = []
|
||||
if country:
|
||||
subject_name.append(x509.NameAttribute(NameOID.COUNTRY_NAME, country))
|
||||
if state:
|
||||
subject_name.append(x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state))
|
||||
if city:
|
||||
subject_name.append(x509.NameAttribute(NameOID.LOCALITY_NAME, city))
|
||||
if organization:
|
||||
subject_name.append(x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization))
|
||||
if organizational_unit:
|
||||
subject_name.append(x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, organizational_unit))
|
||||
subject_name.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name))
|
||||
|
||||
subject = issuer = x509.Name(subject_name)
|
||||
|
||||
# Create certificate
|
||||
cert = x509.CertificateBuilder().subject_name(
|
||||
subject
|
||||
).issuer_name(
|
||||
issuer
|
||||
).public_key(
|
||||
private_key.public_key()
|
||||
).serial_number(
|
||||
x509.random_serial_number()
|
||||
).not_valid_before(
|
||||
datetime.utcnow()
|
||||
).not_valid_after(
|
||||
datetime.utcnow() + timedelta(days=365 * validity_years)
|
||||
).add_extension(
|
||||
x509.BasicConstraints(ca=True, path_length=None), critical=True,
|
||||
).add_extension(
|
||||
x509.KeyUsage(
|
||||
key_cert_sign=True,
|
||||
crl_sign=True,
|
||||
digital_signature=False,
|
||||
content_commitment=False,
|
||||
key_encipherment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
),
|
||||
critical=True
|
||||
).sign(private_key, hashes.SHA256())
|
||||
|
||||
# Serialize private key
|
||||
private_pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
|
||||
# Serialize certificate
|
||||
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
|
||||
|
||||
return {
|
||||
"private_key": private_pem.decode('utf-8'),
|
||||
"certificate": cert_pem.decode('utf-8'),
|
||||
"public_key": private_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
).decode('utf-8')
|
||||
}
|
||||
|
||||
|
||||
def issue_certificate(ca_private_key_pem, ca_cert_pem, common_name,
|
||||
certificate_type="server", country=None, state=None,
|
||||
city=None, organization=None, organizational_unit=None,
|
||||
email=None, validity_years=1):
|
||||
"""
|
||||
Issue a certificate signed by a CA.
|
||||
|
||||
Args:
|
||||
ca_private_key_pem (str): PEM-encoded CA private key
|
||||
ca_cert_pem (str): PEM-encoded CA certificate
|
||||
common_name (str): Common name for the certificate
|
||||
certificate_type (str): Type of certificate (server, client, code_signing)
|
||||
country (str, optional): Country code (2 letters)
|
||||
state (str, optional): State or province
|
||||
city (str, optional): City or locality
|
||||
organization (str, optional): Organization name
|
||||
organizational_unit (str, optional): Organizational unit
|
||||
email (str, optional): Email address
|
||||
validity_years (int): Number of years the certificate is valid (default: 1)
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing the private key, certificate, and public key
|
||||
"""
|
||||
# Load CA private key
|
||||
ca_private_key = serialization.load_pem_private_key(
|
||||
ca_private_key_pem.encode('utf-8'),
|
||||
password=None,
|
||||
)
|
||||
|
||||
# Load CA certificate
|
||||
ca_cert = x509.load_pem_x509_certificate(ca_cert_pem.encode('utf-8'))
|
||||
|
||||
# Generate private key for new certificate
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=2048,
|
||||
)
|
||||
|
||||
# Create subject name
|
||||
subject_name = []
|
||||
if country:
|
||||
subject_name.append(x509.NameAttribute(NameOID.COUNTRY_NAME, country))
|
||||
if state:
|
||||
subject_name.append(x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state))
|
||||
if city:
|
||||
subject_name.append(x509.NameAttribute(NameOID.LOCALITY_NAME, city))
|
||||
if organization:
|
||||
subject_name.append(x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization))
|
||||
if organizational_unit:
|
||||
subject_name.append(x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, organizational_unit))
|
||||
if email:
|
||||
subject_name.append(x509.NameAttribute(NameOID.EMAIL_ADDRESS, email))
|
||||
subject_name.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name))
|
||||
|
||||
subject = x509.Name(subject_name)
|
||||
|
||||
# Determine key usage based on certificate type
|
||||
if certificate_type == "server":
|
||||
key_usage = x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
key_encipherment=True,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
content_commitment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
)
|
||||
extended_key_usage = x509.ExtendedKeyUsage([
|
||||
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH
|
||||
])
|
||||
elif certificate_type == "client":
|
||||
key_usage = x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
key_encipherment=True,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
content_commitment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
)
|
||||
extended_key_usage = x509.ExtendedKeyUsage([
|
||||
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH
|
||||
])
|
||||
elif certificate_type == "code_signing":
|
||||
key_usage = x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
content_commitment=True,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
)
|
||||
extended_key_usage = x509.ExtendedKeyUsage([
|
||||
x509.oid.ExtendedKeyUsageOID.CODE_SIGNING
|
||||
])
|
||||
else:
|
||||
key_usage = x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
key_encipherment=True,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
content_commitment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
)
|
||||
extended_key_usage = None
|
||||
|
||||
# Create certificate
|
||||
cert_builder = x509.CertificateBuilder().subject_name(
|
||||
subject
|
||||
).issuer_name(
|
||||
ca_cert.subject
|
||||
).public_key(
|
||||
private_key.public_key()
|
||||
).serial_number(
|
||||
x509.random_serial_number()
|
||||
).not_valid_before(
|
||||
datetime.utcnow()
|
||||
).not_valid_after(
|
||||
datetime.utcnow() + timedelta(days=365 * validity_years)
|
||||
).add_extension(
|
||||
key_usage, critical=True
|
||||
)
|
||||
|
||||
if extended_key_usage:
|
||||
cert_builder = cert_builder.add_extension(extended_key_usage, critical=False)
|
||||
|
||||
# Add subject alternative name for server certificates
|
||||
if certificate_type == "server":
|
||||
cert_builder = cert_builder.add_extension(
|
||||
x509.SubjectAlternativeName([x509.DNSName(common_name)]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
cert = cert_builder.sign(ca_private_key, hashes.SHA256())
|
||||
|
||||
# Serialize private key
|
||||
private_pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
|
||||
# Serialize certificate
|
||||
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
|
||||
|
||||
return {
|
||||
"private_key": private_pem.decode('utf-8'),
|
||||
"certificate": cert_pem.decode('utf-8'),
|
||||
"public_key": private_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
).decode('utf-8')
|
||||
}
|
||||
|
||||
|
||||
def revoke_certificate(ca_private_key_pem, ca_cert_pem, cert_pem, crl_number=1):
|
||||
"""
|
||||
Revoke a certificate and add it to the CRL.
|
||||
|
||||
Args:
|
||||
ca_private_key_pem (str): PEM-encoded CA private key
|
||||
ca_cert_pem (str): PEM-encoded CA certificate
|
||||
cert_pem (str): PEM-encoded certificate to revoke
|
||||
crl_number (int): CRL number
|
||||
|
||||
Returns:
|
||||
str: PEM-encoded CRL
|
||||
"""
|
||||
# Load CA private key
|
||||
ca_private_key = serialization.load_pem_private_key(
|
||||
ca_private_key_pem.encode('utf-8'),
|
||||
password=None,
|
||||
)
|
||||
|
||||
# Load CA certificate
|
||||
ca_cert = x509.load_pem_x509_certificate(ca_cert_pem.encode('utf-8'))
|
||||
|
||||
# Load certificate to revoke
|
||||
cert = x509.load_pem_x509_certificate(cert_pem.encode('utf-8'))
|
||||
|
||||
# Create CRL
|
||||
crl_builder = x509.CertificateRevocationListBuilder().issuer_name(
|
||||
ca_cert.subject
|
||||
).next_update(
|
||||
datetime.utcnow() + timedelta(days=30)
|
||||
).add_revoked_certificate(
|
||||
x509.RevokedCertificateBuilder().serial_number(
|
||||
cert.serial_number
|
||||
).revocation_date(
|
||||
datetime.utcnow()
|
||||
).build()
|
||||
).add_extension(
|
||||
x509.CRLNumber(crl_number),
|
||||
critical=False
|
||||
)
|
||||
|
||||
crl = crl_builder.sign(ca_private_key, hashes.SHA256())
|
||||
|
||||
# Serialize CRL
|
||||
crl_pem = crl.public_bytes(serialization.Encoding.PEM)
|
||||
|
||||
return crl_pem.decode('utf-8')
|
||||
|
||||
|
||||
def encrypt_private_key(private_key_pem, password):
|
||||
"""
|
||||
Encrypt a private key using a password.
|
||||
|
||||
Args:
|
||||
private_key_pem (str): PEM-encoded private key
|
||||
password (str): Password for encryption
|
||||
|
||||
Returns:
|
||||
str: Encrypted private key
|
||||
"""
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.fernet import Fernet
|
||||
import os
|
||||
import base64
|
||||
|
||||
# Generate a salt
|
||||
salt = os.urandom(16)
|
||||
|
||||
# Derive key from password
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
)
|
||||
key = kdf.derive(password.encode())
|
||||
|
||||
# Encode key as base64 for Fernet
|
||||
fernet_key = base64.urlsafe_b64encode(key)
|
||||
|
||||
# Encrypt the private key
|
||||
f = Fernet(fernet_key)
|
||||
encrypted_key = f.encrypt(private_key_pem.encode())
|
||||
|
||||
# Return salt + encrypted key
|
||||
return salt.hex() + encrypted_key.hex()
|
||||
|
||||
|
||||
def decrypt_private_key(encrypted_data, password):
|
||||
"""
|
||||
Decrypt a private key using a password.
|
||||
|
||||
Args:
|
||||
encrypted_data (str): Encrypted private key (salt + encrypted key)
|
||||
password (str): Password for decryption
|
||||
|
||||
Returns:
|
||||
str: Decrypted PEM-encoded private key
|
||||
"""
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.fernet import Fernet
|
||||
import base64
|
||||
|
||||
# Extract salt and encrypted key
|
||||
salt = bytes.fromhex(encrypted_data[:32])
|
||||
encrypted_key = bytes.fromhex(encrypted_data[32:])
|
||||
|
||||
# Derive key from password
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
)
|
||||
key = kdf.derive(password.encode())
|
||||
|
||||
# Encode key as base64 for Fernet
|
||||
fernet_key = base64.urlsafe_b64encode(key)
|
||||
|
||||
# Decrypt the private key
|
||||
f = Fernet(fernet_key)
|
||||
decrypted_key = f.decrypt(encrypted_key)
|
||||
|
||||
return decrypted_key.decode()
|
||||
@@ -0,0 +1,124 @@
|
||||
# xCloudify Certificate Authority Feature
|
||||
|
||||
## Overview
|
||||
|
||||
This feature allows each project in xCloudify to have its own Certificate Authority (CA) for issuing certificates. The implementation provides a complete solution for generating self-signed CA certificates, issuing certificates, and managing certificate revocation.
|
||||
|
||||
## Features
|
||||
|
||||
- **Self-Signed CA Generation**: Automatically generate a self-signed CA certificate for each project with a 5-year validity period
|
||||
- **Certificate Issuance**: Issue server, client, and code-signing certificates from the project's CA
|
||||
- **Certificate Management**: Full lifecycle management including creation, retrieval, and revocation
|
||||
- **Certificate Revocation**: Support for certificate revocation and Certificate Revocation List (CRL) generation
|
||||
- **Secure Key Storage**: Private keys are encrypted at rest using project-specific encryption
|
||||
- **RESTful API**: Complete API for managing CAs and certificates programmatically
|
||||
|
||||
## Components
|
||||
|
||||
### Database Models
|
||||
|
||||
1. **CertificateAuthority**: Represents a certificate authority for a project
|
||||
2. **Certificate**: Represents a certificate issued by a CA
|
||||
3. **CertificateRevocationList**: Represents a certificate revocation list
|
||||
|
||||
### Cryptographic Functions
|
||||
|
||||
- Generation of self-signed CA certificates using RSA 2048-bit keys
|
||||
- Certificate issuance with proper key usage extensions
|
||||
- Certificate revocation and CRL generation
|
||||
- Private key encryption/decryption
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- `GET /api/certificates/ca/project/{project_id}` - Create or get CA for a project
|
||||
- `GET /api/certificates/ca/{ca_id}` - Get CA details
|
||||
- `PUT /api/certificates/ca/{ca_id}` - Update CA information
|
||||
- `POST /api/certificates/issue` - Issue a new certificate
|
||||
- `GET /api/certificates/cert/ca/{ca_id}` - List certificates for a CA
|
||||
- `GET /api/certificates/cert/{cert_id}` - Get certificate details
|
||||
- `PUT /api/certificates/cert/{cert_id}` - Update certificate information
|
||||
- `POST /api/certificates/cert/{cert_id}/revoke` - Revoke a certificate
|
||||
- `GET /api/certificates/cert/{cert_id}/download` - Download certificate data
|
||||
- `GET /api/certificates/crl/ca/{ca_id}` - Get CRL for a CA
|
||||
|
||||
## Security
|
||||
|
||||
- Private keys are encrypted at rest using project-specific keys
|
||||
- All API communication should be over HTTPS
|
||||
- Certificate revocation support for compromised certificates
|
||||
- Proper key usage extensions for different certificate types
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- cryptography library
|
||||
- Flask
|
||||
- SQLAlchemy
|
||||
|
||||
## Installation
|
||||
|
||||
1. Add `cryptography` to your requirements.txt:
|
||||
```
|
||||
pip install cryptography
|
||||
```
|
||||
|
||||
2. Ensure the database models are migrated:
|
||||
```
|
||||
flask db migrate -m "Add certificate authority tables"
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a CA for a Project
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.xcloudify.tech/api/certificates/ca/project/proj-123" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### Issuing a Server Certificate
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.xcloudify.tech/api/certificates/cert" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"ca_id": "ca-123",
|
||||
"certificate_type": "server",
|
||||
"common_name": "webserver.internal",
|
||||
"organization": "My Company",
|
||||
"validity_period": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### Revoking a Certificate
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.xcloudify.tech/api/certificates/cert/cert-123/revoke" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite:
|
||||
```bash
|
||||
python -m pytest app/tests/test_certificate_models.py
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [API Usage Guide](certificate_authority_usage.md)
|
||||
- [Implementation Summary](certificate_authority_summary.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a pull request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Certificate Authority Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a summary of the Certificate Authority (CA) implementation for xCloudify. The implementation allows each project to have its own CA that can issue certificates for various purposes.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Database Models
|
||||
|
||||
The implementation includes three new database models in `app/models/certificate_models.py`:
|
||||
|
||||
1. **CertificateAuthority**: Represents a certificate authority for a project
|
||||
2. **Certificate**: Represents a certificate issued by a CA
|
||||
3. **CertificateRevocationList**: Represents a certificate revocation list
|
||||
|
||||
These models are also imported in `app/models/models.py` to make them available throughout the application.
|
||||
|
||||
### 2. Cryptographic Functions
|
||||
|
||||
The cryptographic functions are implemented in `app/utils/certificate_utils.py` and include:
|
||||
|
||||
- `generate_self_signed_ca()`: Generates a self-signed CA certificate
|
||||
- `issue_certificate()`: Issues a certificate signed by a CA
|
||||
- `revoke_certificate()`: Revokes a certificate and adds it to the CRL
|
||||
- `encrypt_private_key()` and `decrypt_private_key()`: Functions for encrypting/decrypting private keys
|
||||
|
||||
### 3. API Routes
|
||||
|
||||
The API routes are implemented in `app/controller/api/certificate_routes.py` and include endpoints for:
|
||||
|
||||
- Creating/getting a CA for a project
|
||||
- Managing CA details
|
||||
- Issuing certificates
|
||||
- Managing certificates
|
||||
- Revoking certificates
|
||||
- Downloading certificates
|
||||
- Getting CRLs
|
||||
|
||||
### 4. Dependencies
|
||||
|
||||
The implementation requires the `cryptography` library, which has been added to `requirements.txt` and `app/requirements.txt`.
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **Private Key Encryption**: Private keys are encrypted at rest using project-specific keys
|
||||
2. **Certificate Revocation**: Support for certificate revocation and CRL generation
|
||||
3. **Access Control**: API endpoints can be protected with authentication/authorization
|
||||
4. **Key Storage**: Private keys are stored encrypted in the database
|
||||
|
||||
## Usage
|
||||
|
||||
The implementation provides a complete API for managing certificate authorities and certificates. Users can:
|
||||
|
||||
1. Create a CA for their project
|
||||
2. Issue certificates from their CA
|
||||
3. Revoke certificates when needed
|
||||
4. Download certificates and keys
|
||||
5. Get CRLs for their CA
|
||||
|
||||
## Testing
|
||||
|
||||
A test suite is provided in `app/tests/test_certificate_models.py` that includes tests for:
|
||||
|
||||
- Creating CAs
|
||||
- Creating certificates
|
||||
- Testing to_json methods
|
||||
|
||||
## Documentation
|
||||
|
||||
Usage documentation is provided in `app/docs/certificate_authority_usage.md` with examples of how to use the API endpoints.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Possible future enhancements include:
|
||||
|
||||
1. Integration with hardware security modules (HSMs) for key storage
|
||||
2. Support for certificate templates
|
||||
3. Automated certificate renewal
|
||||
4. Integration with external certificate authorities
|
||||
5. Enhanced monitoring and alerting for certificate expiration
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation provides a robust and secure certificate authority feature for xCloudify projects. It follows security best practices and provides a complete API for managing certificates throughout their lifecycle.
|
||||
@@ -0,0 +1,226 @@
|
||||
# Certificate Authority Feature Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains how to use the Certificate Authority (CA) feature in xCloudify. Each project can have its own CA that can issue certificates for various purposes such as server authentication, client authentication, and code signing.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Create or Get CA for a Project
|
||||
|
||||
```
|
||||
GET /api/certificates/ca/project/{project_id}
|
||||
```
|
||||
|
||||
This endpoint will create a new CA for the specified project if one doesn't already exist, or return the existing CA.
|
||||
|
||||
**Parameters:**
|
||||
- `project_id` (path): ID of the project
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "CA retrieved successfully",
|
||||
"data": {
|
||||
"id": "ca-id",
|
||||
"project_id": "project-id",
|
||||
"common_name": "rootca.project-id.xcloudify.tech",
|
||||
"organization": "xCloudify",
|
||||
"organizational_unit": "IT",
|
||||
"dns_name": "rootca.project-id.xcloudify.tech",
|
||||
"validity_period": 5,
|
||||
"is_active": true,
|
||||
"created_at": "2023-01-01T00:00:00",
|
||||
"updated_at": "2023-01-01T00:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get CA Details
|
||||
|
||||
```
|
||||
GET /api/certificates/ca/{ca_id}
|
||||
```
|
||||
|
||||
Get detailed information about a specific CA.
|
||||
|
||||
**Parameters:**
|
||||
- `ca_id` (path): ID of the CA
|
||||
|
||||
### Update CA
|
||||
|
||||
```
|
||||
PUT /api/certificates/ca/{ca_id}
|
||||
```
|
||||
|
||||
Update CA information.
|
||||
|
||||
**Parameters:**
|
||||
- `ca_id` (path): ID of the CA
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"common_name": "new-ca-name",
|
||||
"organization": "New Org",
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
### Create Certificate
|
||||
|
||||
```
|
||||
POST /api/certificates/cert
|
||||
```
|
||||
|
||||
Issue a new certificate from a CA.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"ca_id": "ca-id",
|
||||
"certificate_type": "server", // Can be "server", "client", or "code_signing"
|
||||
"common_name": "example.server.local",
|
||||
"organization": "Example Org",
|
||||
"validity_period": 1 // Years
|
||||
}
|
||||
```
|
||||
|
||||
### List Certificates for CA
|
||||
|
||||
```
|
||||
GET /api/certificates/cert/ca/{ca_id}
|
||||
```
|
||||
|
||||
List all certificates issued by a specific CA.
|
||||
|
||||
**Parameters:**
|
||||
- `ca_id` (path): ID of the CA
|
||||
|
||||
### Get Certificate Details
|
||||
|
||||
```
|
||||
GET /api/certificates/cert/{cert_id}
|
||||
```
|
||||
|
||||
Get detailed information about a specific certificate.
|
||||
|
||||
**Parameters:**
|
||||
- `cert_id` (path): ID of the certificate
|
||||
|
||||
### Update Certificate
|
||||
|
||||
```
|
||||
PUT /api/certificates/cert/{cert_id}
|
||||
```
|
||||
|
||||
Update certificate information.
|
||||
|
||||
**Parameters:**
|
||||
- `cert_id` (path): ID of the certificate
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"is_active": false
|
||||
}
|
||||
```
|
||||
|
||||
### Revoke Certificate
|
||||
|
||||
```
|
||||
POST /api/certificates/cert/{cert_id}/revoke
|
||||
```
|
||||
|
||||
Revoke a certificate.
|
||||
|
||||
**Parameters:**
|
||||
- `cert_id` (path): ID of the certificate
|
||||
|
||||
### Download Certificate
|
||||
|
||||
```
|
||||
GET /api/certificates/cert/{cert_id}/download?type=certificate
|
||||
```
|
||||
|
||||
Download a certificate's public or private key.
|
||||
|
||||
**Parameters:**
|
||||
- `cert_id` (path): ID of the certificate
|
||||
- `type` (query): Type of data to download ("certificate", "private_key", or "public_key")
|
||||
|
||||
### Get CRL
|
||||
|
||||
```
|
||||
GET /api/certificates/crl/ca/{ca_id}
|
||||
```
|
||||
|
||||
Get the current Certificate Revocation List (CRL) for a CA.
|
||||
|
||||
**Parameters:**
|
||||
- `ca_id` (path): ID of the CA
|
||||
|
||||
## Certificate Types
|
||||
|
||||
The system supports the following certificate types:
|
||||
|
||||
1. **Server Certificates**: Used for server authentication (TLS/SSL)
|
||||
2. **Client Certificates**: Used for client authentication
|
||||
3. **Code Signing Certificates**: Used for signing code
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. Private keys are encrypted at rest using project-specific keys
|
||||
2. All communication with the API should be over HTTPS
|
||||
3. Access to private keys should be restricted to authorized users only
|
||||
4. Certificate revocation should be done promptly when needed
|
||||
|
||||
## Example Usage
|
||||
|
||||
### 1. Create a CA for a Project
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.xcloudify.tech/api/certificates/ca/project/proj-123" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### 2. Issue a Server Certificate
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.xcloudify.tech/api/certificates/cert" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"ca_id": "ca-123",
|
||||
"certificate_type": "server",
|
||||
"common_name": "webserver.internal",
|
||||
"organization": "My Company",
|
||||
"validity_period": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. Download Certificate
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.xcloudify.tech/api/certificates/cert/cert-123/download?type=certificate" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-o certificate.pem
|
||||
```
|
||||
|
||||
### 4. Revoke Certificate
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.xcloudify.tech/api/certificates/cert/cert-123/revoke" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Use separate CAs for different environments (dev, test, prod)
|
||||
2. Keep CA private keys secure and backed up
|
||||
3. Monitor certificate expiration dates
|
||||
4. Revoke certificates immediately when they are compromised
|
||||
5. Use strong encryption for private key storage
|
||||
6. Regularly rotate CA certificates before expiration
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Certs
|
||||
|
||||
Revision ID: 23306f452624
|
||||
Revises: 37480d4d2782
|
||||
Create Date: 2025-08-14 21:18:02.277503
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '23306f452624'
|
||||
down_revision = '37480d4d2782'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('tasks', schema=None) as batch_op:
|
||||
batch_op.drop_index('ix_tasks_worker_id')
|
||||
|
||||
op.drop_table('tasks')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tasks',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('worker_id', mysql.VARCHAR(length=64), nullable=True),
|
||||
sa.Column('job_details', mysql.TEXT(), nullable=True),
|
||||
sa.Column('response', mysql.TEXT(), nullable=True),
|
||||
sa.Column('status', mysql.TEXT(), nullable=True),
|
||||
sa.Column('success', mysql.TEXT(), nullable=True),
|
||||
sa.Column('creation_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('start_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('finish_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('wait_time', mysql.FLOAT(), nullable=True),
|
||||
sa.Column('execution_time', mysql.FLOAT(), nullable=True),
|
||||
sa.Column('task_type', mysql.VARCHAR(length=50), nullable=False),
|
||||
sa.Column('depends_on', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True),
|
||||
sa.Column('not_before', mysql.DATETIME(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
with op.batch_alter_table('tasks', schema=None) as batch_op:
|
||||
batch_op.create_index('ix_tasks_worker_id', ['worker_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Certs
|
||||
|
||||
Revision ID: 37480d4d2782
|
||||
Revises: 5fd95b9faf5f
|
||||
Create Date: 2025-08-14 17:50:33.906740
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '37480d4d2782'
|
||||
down_revision = '5fd95b9faf5f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('tasks', schema=None) as batch_op:
|
||||
batch_op.drop_index('ix_tasks_worker_id')
|
||||
|
||||
op.drop_table('tasks')
|
||||
with op.batch_alter_table('certificate_authorities', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('visible', sa.Boolean(), nullable=False))
|
||||
batch_op.add_column(sa.Column('name', sa.String(length=255), nullable=False))
|
||||
batch_op.add_column(sa.Column('description', mysql.LONGTEXT(), nullable=True))
|
||||
batch_op.add_column(sa.Column('status', sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column('created_by', sa.String(length=36), nullable=True))
|
||||
batch_op.add_column(sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||
batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=False))
|
||||
batch_op.create_foreign_key(None, 'users', ['created_by'], ['id'])
|
||||
|
||||
with op.batch_alter_table('certificate_revocation_lists', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('visible', sa.Boolean(), nullable=False))
|
||||
batch_op.add_column(sa.Column('name', sa.String(length=255), nullable=False))
|
||||
batch_op.add_column(sa.Column('description', mysql.LONGTEXT(), nullable=True))
|
||||
batch_op.add_column(sa.Column('status', sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column('created_by', sa.String(length=36), nullable=True))
|
||||
batch_op.add_column(sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||
batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=False))
|
||||
batch_op.create_foreign_key(None, 'users', ['created_by'], ['id'])
|
||||
|
||||
with op.batch_alter_table('certificates', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('visible', sa.Boolean(), nullable=False))
|
||||
batch_op.add_column(sa.Column('name', sa.String(length=255), nullable=False))
|
||||
batch_op.add_column(sa.Column('description', mysql.LONGTEXT(), nullable=True))
|
||||
batch_op.add_column(sa.Column('status', sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column('created_by', sa.String(length=36), nullable=True))
|
||||
batch_op.add_column(sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||
batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=False))
|
||||
batch_op.create_foreign_key(None, 'users', ['created_by'], ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('certificates', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_column('deleted')
|
||||
batch_op.drop_column('deleted_at')
|
||||
batch_op.drop_column('created_by')
|
||||
batch_op.drop_column('status')
|
||||
batch_op.drop_column('description')
|
||||
batch_op.drop_column('name')
|
||||
batch_op.drop_column('visible')
|
||||
|
||||
with op.batch_alter_table('certificate_revocation_lists', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_column('deleted')
|
||||
batch_op.drop_column('deleted_at')
|
||||
batch_op.drop_column('created_by')
|
||||
batch_op.drop_column('status')
|
||||
batch_op.drop_column('description')
|
||||
batch_op.drop_column('name')
|
||||
batch_op.drop_column('visible')
|
||||
|
||||
with op.batch_alter_table('certificate_authorities', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_column('deleted')
|
||||
batch_op.drop_column('deleted_at')
|
||||
batch_op.drop_column('created_by')
|
||||
batch_op.drop_column('status')
|
||||
batch_op.drop_column('description')
|
||||
batch_op.drop_column('name')
|
||||
batch_op.drop_column('visible')
|
||||
|
||||
op.create_table('tasks',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('worker_id', mysql.VARCHAR(length=64), nullable=True),
|
||||
sa.Column('job_details', mysql.TEXT(), nullable=True),
|
||||
sa.Column('response', mysql.TEXT(), nullable=True),
|
||||
sa.Column('status', mysql.TEXT(), nullable=True),
|
||||
sa.Column('success', mysql.TEXT(), nullable=True),
|
||||
sa.Column('creation_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('start_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('finish_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('wait_time', mysql.FLOAT(), nullable=True),
|
||||
sa.Column('execution_time', mysql.FLOAT(), nullable=True),
|
||||
sa.Column('task_type', mysql.VARCHAR(length=50), nullable=False),
|
||||
sa.Column('depends_on', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True),
|
||||
sa.Column('not_before', mysql.DATETIME(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
with op.batch_alter_table('tasks', schema=None) as batch_op:
|
||||
batch_op.create_index('ix_tasks_worker_id', ['worker_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Certs
|
||||
|
||||
Revision ID: 5fd95b9faf5f
|
||||
Revises: 2ab3b94a6452
|
||||
Create Date: 2025-08-14 15:11:31.845852
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5fd95b9faf5f'
|
||||
down_revision = '2ab3b94a6452'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('certificate_authorities',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('project_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('common_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('country', sa.String(length=2), nullable=True),
|
||||
sa.Column('state', sa.String(length=255), nullable=True),
|
||||
sa.Column('city', sa.String(length=255), nullable=True),
|
||||
sa.Column('organization', sa.String(length=255), nullable=True),
|
||||
sa.Column('organizational_unit', sa.String(length=255), nullable=True),
|
||||
sa.Column('dns_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('validity_period', sa.Integer(), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('private_key', mysql.LONGTEXT(), nullable=True),
|
||||
sa.Column('certificate_data', mysql.LONGTEXT(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('project_id')
|
||||
)
|
||||
op.create_table('certificate_revocation_lists',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('ca_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('crl_number', sa.Integer(), nullable=False),
|
||||
sa.Column('crl_data', mysql.LONGTEXT(), nullable=True),
|
||||
sa.Column('next_update', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['ca_id'], ['certificate_authorities.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('certificates',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('ca_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('certificate_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('common_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('country', sa.String(length=2), nullable=True),
|
||||
sa.Column('state', sa.String(length=255), nullable=True),
|
||||
sa.Column('city', sa.String(length=255), nullable=True),
|
||||
sa.Column('organization', sa.String(length=255), nullable=True),
|
||||
sa.Column('organizational_unit', sa.String(length=255), nullable=True),
|
||||
sa.Column('email', sa.String(length=255), nullable=True),
|
||||
sa.Column('validity_period', sa.Integer(), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('revoked', sa.Boolean(), nullable=False),
|
||||
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('public_key', mysql.LONGTEXT(), nullable=True),
|
||||
sa.Column('private_key', mysql.LONGTEXT(), nullable=True),
|
||||
sa.Column('certificate_data', mysql.LONGTEXT(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['ca_id'], ['certificate_authorities.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('tasks', schema=None) as batch_op:
|
||||
batch_op.drop_index('ix_tasks_worker_id')
|
||||
|
||||
op.drop_table('tasks')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tasks',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('worker_id', mysql.VARCHAR(length=64), nullable=True),
|
||||
sa.Column('job_details', mysql.TEXT(), nullable=True),
|
||||
sa.Column('response', mysql.TEXT(), nullable=True),
|
||||
sa.Column('status', mysql.TEXT(), nullable=True),
|
||||
sa.Column('success', mysql.TEXT(), nullable=True),
|
||||
sa.Column('creation_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('start_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('finish_time', mysql.DATETIME(), nullable=True),
|
||||
sa.Column('wait_time', mysql.FLOAT(), nullable=True),
|
||||
sa.Column('execution_time', mysql.FLOAT(), nullable=True),
|
||||
sa.Column('task_type', mysql.VARCHAR(length=50), nullable=False),
|
||||
sa.Column('depends_on', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True),
|
||||
sa.Column('not_before', mysql.DATETIME(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
with op.batch_alter_table('tasks', schema=None) as batch_op:
|
||||
batch_op.create_index('ix_tasks_worker_id', ['worker_id'], unique=False)
|
||||
|
||||
op.drop_table('certificates')
|
||||
op.drop_table('certificate_revocation_lists')
|
||||
op.drop_table('certificate_authorities')
|
||||
# ### end Alembic commands ###
|
||||
+2
-1
@@ -14,4 +14,5 @@ aiohttp
|
||||
PyJWT
|
||||
python-dotenv
|
||||
psutil
|
||||
celery
|
||||
celery
|
||||
cryptography
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../ai-cloud-command-center"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
Reference in New Issue
Block a user