Feat(Fix): Key Timezone, Expiry, Depart Link
This commit is contained in:
@@ -30,7 +30,7 @@ from gatehouse_app.models.principal import (
|
||||
PrincipalMembership,
|
||||
)
|
||||
from gatehouse_app.models.ssh_key import SSHKey
|
||||
from gatehouse_app.models.ca import CA, KeyType, CertType
|
||||
from gatehouse_app.models.ca import CA, KeyType, CertType, CAPermission
|
||||
from gatehouse_app.models.ssh_certificate import SSHCertificate, CertificateStatus
|
||||
from gatehouse_app.models.certificate_audit_log import CertificateAuditLog
|
||||
from gatehouse_app.models.password_reset_token import PasswordResetToken
|
||||
@@ -66,6 +66,7 @@ __all__ = [
|
||||
"CA",
|
||||
"KeyType",
|
||||
"CertType",
|
||||
"CAPermission",
|
||||
"SSHCertificate",
|
||||
"CertificateStatus",
|
||||
"CertificateAuditLog",
|
||||
|
||||
@@ -82,7 +82,10 @@ class BaseModel(db.Model):
|
||||
if column.name not in exclude:
|
||||
value = getattr(self, column.name)
|
||||
if isinstance(value, datetime):
|
||||
result[column.name] = value.isoformat()
|
||||
if value.tzinfo is None:
|
||||
result[column.name] = value.isoformat() + "Z"
|
||||
else:
|
||||
result[column.name] = value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
else:
|
||||
result[column.name] = value
|
||||
return result
|
||||
|
||||
@@ -20,6 +20,13 @@ class CertType(str, Enum):
|
||||
HOST = "host"
|
||||
|
||||
|
||||
class CaType(str, Enum):
|
||||
"""CA signing type — whether this CA signs user or host certificates."""
|
||||
|
||||
USER = "user"
|
||||
HOST = "host"
|
||||
|
||||
|
||||
class CA(BaseModel):
|
||||
"""Certificate Authority (CA) model for SSH certificate signing.
|
||||
|
||||
@@ -40,7 +47,14 @@ class CA(BaseModel):
|
||||
# CA name and description
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
|
||||
|
||||
# CA signing type: 'user' signs user certificates, 'host' signs host certificates
|
||||
ca_type = db.Column(
|
||||
db.Enum(CaType, values_callable=lambda x: [e.value for e in x]),
|
||||
default=CaType.USER,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Key type (ED25519, RSA, ECDSA)
|
||||
key_type = db.Column(
|
||||
db.Enum(KeyType, values_callable=lambda x: [e.value for e in x]),
|
||||
@@ -91,6 +105,11 @@ class CA(BaseModel):
|
||||
back_populates="ca",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
permissions = db.relationship(
|
||||
"CAPermission",
|
||||
back_populates="ca",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint(
|
||||
@@ -153,3 +172,49 @@ class CA(BaseModel):
|
||||
self.rotated_at = datetime.utcnow()
|
||||
self.rotation_reason = reason
|
||||
self.save()
|
||||
|
||||
|
||||
class CAPermission(BaseModel):
|
||||
"""Per-user CA permission model.
|
||||
|
||||
Controls which users are allowed to sign certificates against a specific CA.
|
||||
When a CA has any permission rows the signing endpoint enforces the list;
|
||||
CAs with no rows are open to all org members (backwards-compatible default).
|
||||
|
||||
Permission values:
|
||||
sign – user may request certificate signing
|
||||
admin – user may sign AND manage the CA (rotate keys, delete, etc.)
|
||||
"""
|
||||
|
||||
__tablename__ = "ca_permissions"
|
||||
|
||||
ca_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("cas.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
user_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
permission = db.Column(db.String(50), nullable=False, default="sign")
|
||||
|
||||
# Relationships
|
||||
ca = db.relationship("CA", back_populates="permissions")
|
||||
user = db.relationship("User", back_populates="ca_permissions")
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint("ca_id", "user_id", name="uix_ca_permission"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CAPermission ca_id={self.ca_id} user_id={self.user_id} permission={self.permission}>"
|
||||
|
||||
def to_dict(self, exclude=None):
|
||||
data = super().to_dict(exclude=exclude or [])
|
||||
data["permission"] = self.permission
|
||||
return data
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""SSH Certificate model."""
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from gatehouse_app.extensions import db
|
||||
from gatehouse_app.models.base import BaseModel
|
||||
from gatehouse_app.models.ca import CertType
|
||||
@@ -137,8 +137,10 @@ class SSHCertificate(BaseModel):
|
||||
if self.revoked or self.status == CertificateStatus.REVOKED:
|
||||
return False
|
||||
|
||||
now = datetime.utcnow()
|
||||
return self.valid_after <= now <= self.valid_before
|
||||
now = datetime.now(timezone.utc)
|
||||
valid_after = self.valid_after.replace(tzinfo=timezone.utc) if self.valid_after.tzinfo is None else self.valid_after
|
||||
valid_before = self.valid_before.replace(tzinfo=timezone.utc) if self.valid_before.tzinfo is None else self.valid_before
|
||||
return valid_after <= now <= valid_before
|
||||
|
||||
def is_expired(self):
|
||||
"""Check if certificate has expired.
|
||||
@@ -146,7 +148,9 @@ class SSHCertificate(BaseModel):
|
||||
Returns:
|
||||
True if current time is past valid_before
|
||||
"""
|
||||
return datetime.utcnow() > self.valid_before
|
||||
now = datetime.now(timezone.utc)
|
||||
valid_before = self.valid_before.replace(tzinfo=timezone.utc) if self.valid_before.tzinfo is None else self.valid_before
|
||||
return now > valid_before
|
||||
|
||||
def days_until_expiry(self):
|
||||
"""Get number of days until certificate expires.
|
||||
@@ -154,7 +158,9 @@ class SSHCertificate(BaseModel):
|
||||
Returns:
|
||||
Number of days remaining (negative if already expired)
|
||||
"""
|
||||
delta = self.valid_before - datetime.utcnow()
|
||||
now = datetime.now(timezone.utc)
|
||||
valid_before = self.valid_before.replace(tzinfo=timezone.utc) if self.valid_before.tzinfo is None else self.valid_before
|
||||
delta = valid_before - now
|
||||
return delta.days + (1 if delta.seconds > 0 else 0)
|
||||
|
||||
def revoke(self, reason=None):
|
||||
|
||||
Reference in New Issue
Block a user