firstpass of certs

This commit is contained in:
2025-08-15 13:18:09 +09:30
parent 7f659f91c2
commit 3aedc95f5a
13 changed files with 1858 additions and 2 deletions
+1
View File
@@ -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'])
+618
View File
@@ -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)}
)
+116
View File
@@ -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
+2 -1
View File
@@ -16,4 +16,5 @@ python-dotenv
PyJWT
apiflask
marshmallow
marshmallow_sqlalchemy
marshmallow_sqlalchemy
cryptography
+397
View File
@@ -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()
+124
View File
@@ -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.
+85
View File
@@ -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.
+226
View File
@@ -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
+53
View File
@@ -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 ###
+113
View File
@@ -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 ###
+110
View File
@@ -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
View File
@@ -14,4 +14,5 @@ aiohttp
PyJWT
python-dotenv
psutil
celery
celery
cryptography
+11
View File
@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../ai-cloud-command-center"
}
],
"settings": {}
}