Feat: Implemented SUDO Department & API Key, CA Serial
This commit is contained in:
@@ -12,6 +12,7 @@ from gatehouse_app.models.organization.department_cert_policy import (
|
||||
)
|
||||
from gatehouse_app.models.organization.principal import Principal, PrincipalMembership
|
||||
from gatehouse_app.models.organization.org_invite_token import OrgInviteToken
|
||||
from gatehouse_app.models.organization.organization_api_key import OrganizationApiKey
|
||||
|
||||
__all__ = [
|
||||
"Organization",
|
||||
@@ -24,4 +25,5 @@ __all__ = [
|
||||
"Principal",
|
||||
"PrincipalMembership",
|
||||
"OrgInviteToken",
|
||||
"OrganizationApiKey",
|
||||
]
|
||||
|
||||
@@ -27,6 +27,7 @@ class Department(BaseModel):
|
||||
)
|
||||
name = db.Column(db.String(255), nullable=False, index=True)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
can_sudo = db.Column(db.Boolean, default=False, nullable=False)
|
||||
|
||||
# Relationships
|
||||
organization = db.relationship("Organization", back_populates="departments")
|
||||
|
||||
@@ -4,12 +4,13 @@ from gatehouse_app.extensions import db
|
||||
from gatehouse_app.models.base import BaseModel
|
||||
|
||||
|
||||
# Standard SSH certificate extensions
|
||||
# Standard SSH certificate extensions — must be in strict lexical order
|
||||
# (OpenSSH RFC 4251 §5 / golang.org/x/crypto/ssh requires lexical ordering)
|
||||
STANDARD_EXTENSIONS = [
|
||||
"permit-X11-forwarding",
|
||||
"permit-agent-forwarding",
|
||||
"permit-pty",
|
||||
"permit-port-forwarding",
|
||||
"permit-pty",
|
||||
"permit-user-rc",
|
||||
]
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ class Organization(BaseModel):
|
||||
cas = db.relationship(
|
||||
"CA", back_populates="organization", cascade="all, delete-orphan"
|
||||
)
|
||||
api_keys = db.relationship(
|
||||
"OrganizationApiKey", back_populates="organization", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of Organization."""
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Organization API Key model — API keys for organizations for external integrations."""
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from gatehouse_app.extensions import db
|
||||
from gatehouse_app.models.base import BaseModel
|
||||
|
||||
|
||||
class OrganizationApiKey(BaseModel):
|
||||
"""API Key model representing an API key for an organization.
|
||||
|
||||
API keys are used to authenticate external integrations or services
|
||||
that need programmatic access to the organization's resources.
|
||||
Each key is tied to an organization and can be revoked/deleted as needed.
|
||||
"""
|
||||
|
||||
__tablename__ = "organization_api_keys"
|
||||
|
||||
organization_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("organizations.id"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
# Human-readable name for the API key
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
|
||||
# Hashed key value (never store plain text)
|
||||
key_hash = db.Column(db.String(255), nullable=False, unique=True, index=True)
|
||||
|
||||
# Last used timestamp for tracking activity
|
||||
last_used_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Revocation status
|
||||
is_revoked = db.Column(db.Boolean, default=False, nullable=False, index=True)
|
||||
revoked_at = db.Column(db.DateTime, nullable=True)
|
||||
revoke_reason = db.Column(db.String(255), nullable=True)
|
||||
|
||||
# Description/purpose of the key
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Relationships
|
||||
organization = db.relationship("Organization", back_populates="api_keys")
|
||||
|
||||
__table_args__ = (
|
||||
db.Index("idx_org_api_key_org_active", "organization_id", "is_revoked"),
|
||||
db.Index("idx_api_key_last_used", "last_used_at"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of OrganizationApiKey."""
|
||||
return f"<OrganizationApiKey name={self.name} org_id={self.organization_id}>"
|
||||
|
||||
@staticmethod
|
||||
def generate_key() -> str:
|
||||
"""Generate a random API key.
|
||||
|
||||
Returns:
|
||||
A random 32-byte hex string suitable for use as an API key
|
||||
"""
|
||||
return secrets.token_hex(32)
|
||||
|
||||
@classmethod
|
||||
def create_key(
|
||||
cls,
|
||||
organization_id: str,
|
||||
name: str,
|
||||
description: str = None,
|
||||
) -> tuple:
|
||||
"""Create and store a new API key for an organization.
|
||||
|
||||
Args:
|
||||
organization_id: ID of the organization
|
||||
name: Human-readable name for the key
|
||||
description: Optional description/purpose of the key
|
||||
|
||||
Returns:
|
||||
Tuple of (OrganizationApiKey instance, plain_text_key_string)
|
||||
The plain text key is only returned on creation and should be
|
||||
stored securely by the user. It cannot be retrieved later.
|
||||
"""
|
||||
# Generate a plain text key
|
||||
plain_key = cls.generate_key()
|
||||
|
||||
# Hash it using the key_hash method
|
||||
key_hash = cls.hash_key(plain_key)
|
||||
|
||||
# Create the database record
|
||||
api_key = cls(
|
||||
organization_id=organization_id,
|
||||
name=name,
|
||||
key_hash=key_hash,
|
||||
description=description,
|
||||
)
|
||||
api_key.save()
|
||||
|
||||
return api_key, plain_key
|
||||
|
||||
@staticmethod
|
||||
def hash_key(plain_key: str) -> str:
|
||||
"""Hash an API key for storage.
|
||||
|
||||
Args:
|
||||
plain_key: The plain text API key
|
||||
|
||||
Returns:
|
||||
Hashed version of the key
|
||||
"""
|
||||
import hashlib
|
||||
return hashlib.sha256(plain_key.encode()).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def verify_key(cls, organization_id: str, plain_key: str) -> "OrganizationApiKey":
|
||||
"""Verify an API key for an organization.
|
||||
|
||||
Args:
|
||||
organization_id: ID of the organization
|
||||
plain_key: The plain text API key to verify
|
||||
|
||||
Returns:
|
||||
OrganizationApiKey instance if valid and active, None otherwise
|
||||
"""
|
||||
key_hash = cls.hash_key(plain_key)
|
||||
|
||||
api_key = cls.query.filter_by(
|
||||
organization_id=organization_id,
|
||||
key_hash=key_hash,
|
||||
is_revoked=False,
|
||||
deleted_at=None,
|
||||
).first()
|
||||
|
||||
if api_key:
|
||||
# Update last used timestamp
|
||||
api_key.last_used_at = datetime.now(timezone.utc)
|
||||
api_key.save()
|
||||
|
||||
return api_key
|
||||
|
||||
def revoke(self, reason: str = None) -> None:
|
||||
"""Revoke this API key.
|
||||
|
||||
Args:
|
||||
reason: Optional reason for revocation
|
||||
"""
|
||||
self.is_revoked = True
|
||||
self.revoked_at = datetime.now(timezone.utc)
|
||||
self.revoke_reason = reason
|
||||
self.save()
|
||||
|
||||
def to_dict(self, exclude=None):
|
||||
"""Convert API key to dictionary.
|
||||
|
||||
The key_hash is excluded by default for security.
|
||||
"""
|
||||
exclude = exclude or []
|
||||
if "key_hash" not in exclude:
|
||||
exclude.append("key_hash")
|
||||
return super().to_dict(exclude=exclude)
|
||||
Reference in New Issue
Block a user