majuna/app/models/mirrors.py

388 lines
13 KiB
Python
Raw Normal View History

from __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
2022-05-04 15:36:36 +01:00
2024-11-10 13:38:51 +00:00
import tldextract
from sqlalchemy.orm import Mapped, mapped_column, relationship
2022-05-04 15:36:36 +01:00
from tldextract import extract
from werkzeug.datastructures import FileStorage
2022-05-04 15:36:36 +01:00
2022-06-15 11:50:15 +01:00
from app.brm.brn import BRN
2024-12-06 18:15:47 +00:00
from app.brm.utils import create_data_uri, normalize_color, thumbnail_uploaded_image
2022-05-17 08:28:37 +01:00
from app.extensions import db
2023-10-29 15:45:10 +00:00
from app.models import AbstractConfiguration, AbstractResource, Deprecation
from app.models.base import Group, Pool
2022-05-04 15:36:36 +01:00
from app.models.onions import Onion
from app.models.types import AwareDateTime
2022-04-22 14:01:16 +01:00
2023-10-29 15:45:10 +00:00
country_origin = db.Table(
2024-12-06 18:15:47 +00:00
"country_origin",
2023-10-29 15:45:10 +00:00
db.metadata,
2024-12-06 18:15:47 +00:00
db.Column("country_id", db.ForeignKey("country.id"), primary_key=True),
db.Column("origin_id", db.ForeignKey("origin.id"), primary_key=True),
2023-10-29 15:45:10 +00:00
extend_existing=True,
)
2022-04-22 14:01:16 +01:00
class OriginDict(TypedDict):
Id: int
Description: str
DomainName: str
RiskLevel: Dict[str, int]
RiskLevelOverride: Optional[int]
2022-04-22 14:01:16 +01:00
class Origin(AbstractConfiguration):
group_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey("group.id"))
2024-11-16 13:17:39 +00:00
domain_name: Mapped[str] = mapped_column(unique=True)
auto_rotation: Mapped[bool]
smart: Mapped[bool]
assets: Mapped[bool]
risk_level_override: Mapped[Optional[int]]
2022-04-22 14:01:16 +01:00
group: Mapped[Group] = relationship("Group", back_populates="origins")
proxies: Mapped[List[Proxy]] = relationship("Proxy", back_populates="origin")
2024-12-06 18:15:47 +00:00
countries: Mapped[List[Country]] = relationship(
"Country", secondary=country_origin, back_populates="origins"
)
@property
def brn(self) -> BRN:
return BRN(
group_id=self.group_id,
product="mirror",
provider="conf",
resource_type="origin",
2024-12-06 18:15:47 +00:00
resource_id=self.domain_name,
)
2022-04-22 14:01:16 +01:00
@classmethod
2022-05-16 11:44:03 +01:00
def csv_header(cls) -> List[str]:
2022-04-22 14:01:16 +01:00
return super().csv_header() + [
2024-12-06 18:15:47 +00:00
"group_id",
"domain_name",
"auto_rotation",
"smart",
"assets",
"country",
2022-04-22 14:01:16 +01:00
]
2022-05-16 11:44:03 +01:00
def destroy(self) -> None:
2022-04-22 14:01:16 +01:00
super().destroy()
for proxy in self.proxies:
proxy.destroy()
@property
def normalised_domain_name(self) -> str:
extracted_domain = tldextract.extract(self.domain_name)
return extracted_domain.registered_domain
2022-05-04 15:36:36 +01:00
def onion(self) -> Optional[str]:
tld = extract(self.domain_name).registered_domain
onion = Onion.query.filter(Onion.domain_name == tld).first()
if not onion:
return None
2022-05-16 11:44:03 +01:00
domain_name: str = self.domain_name
return f"https://{domain_name.replace(tld, onion.onion_name)}.onion"
2022-05-04 15:36:36 +01:00
2023-10-29 15:45:10 +00:00
@property
def risk_level(self) -> Dict[str, int]:
if self.risk_level_override:
2024-12-06 18:15:47 +00:00
return {
country.country_code: self.risk_level_override
for country in self.countries
}
frequency_factor = 0.0
recency_factor = 0.0
2023-10-29 15:45:10 +00:00
recent_deprecations = (
db.session.query(Deprecation)
2024-12-06 18:15:47 +00:00
.join(Proxy, Deprecation.resource_id == Proxy.id)
2023-10-29 15:45:10 +00:00
.join(Origin, Origin.id == Proxy.origin_id)
.filter(
Origin.id == self.id,
2024-12-06 18:15:47 +00:00
Deprecation.resource_type == "Proxy",
Deprecation.deprecated_at
>= datetime.now(tz=timezone.utc) - timedelta(hours=168),
Deprecation.reason != "destroyed",
2023-10-29 15:45:10 +00:00
)
.distinct(Proxy.id)
.all()
)
for deprecation in recent_deprecations:
2024-12-06 18:15:47 +00:00
recency_factor += 1 / max(
(
datetime.now(tz=timezone.utc) - deprecation.deprecated_at
).total_seconds()
// 3600,
1,
)
2023-10-29 15:45:10 +00:00
frequency_factor += 1
risk_levels: Dict[str, int] = {}
for country in self.countries:
2024-12-06 18:15:47 +00:00
risk_levels[country.country_code.upper()] = (
int(max(1, min(10, frequency_factor * recency_factor)))
+ country.risk_level
)
2023-10-29 15:45:10 +00:00
return risk_levels
def to_dict(self) -> OriginDict:
2024-11-10 13:38:51 +00:00
return {
"Id": self.id,
"Description": self.description,
"DomainName": self.domain_name,
"RiskLevel": self.risk_level,
"RiskLevelOverride": self.risk_level_override,
}
2023-10-29 15:45:10 +00:00
class Country(AbstractConfiguration):
@property
def brn(self) -> BRN:
return BRN(
group_id=0,
product="country",
provider="iso3166-1",
resource_type="alpha2",
2024-12-06 18:15:47 +00:00
resource_id=self.country_code,
2023-10-29 15:45:10 +00:00
)
2024-11-16 13:17:39 +00:00
country_code: Mapped[str]
risk_level_override: Mapped[Optional[int]]
2023-10-29 15:45:10 +00:00
2024-12-06 18:15:47 +00:00
origins = db.relationship(
"Origin", secondary=country_origin, back_populates="countries"
)
2023-10-29 15:45:10 +00:00
@property
def risk_level(self) -> int:
if self.risk_level_override:
return int(self.risk_level_override // 2)
frequency_factor = 0.0
recency_factor = 0.0
2023-10-29 15:45:10 +00:00
recent_deprecations = (
db.session.query(Deprecation)
2024-12-06 18:15:47 +00:00
.join(Proxy, Deprecation.resource_id == Proxy.id)
2023-10-29 15:45:10 +00:00
.join(Origin, Origin.id == Proxy.origin_id)
.join(Origin.countries)
.filter(
Country.id == self.id,
2024-12-06 18:15:47 +00:00
Deprecation.resource_type == "Proxy",
Deprecation.deprecated_at
>= datetime.now(tz=timezone.utc) - timedelta(hours=168),
Deprecation.reason != "destroyed",
2023-10-29 15:45:10 +00:00
)
.distinct(Proxy.id)
.all()
)
for deprecation in recent_deprecations:
2024-12-06 18:15:47 +00:00
recency_factor += 1 / max(
(
datetime.now(tz=timezone.utc) - deprecation.deprecated_at
).total_seconds()
// 3600,
1,
)
2023-10-29 15:45:10 +00:00
frequency_factor += 1
return int(max(1, min(10, frequency_factor * recency_factor)))
2022-04-22 14:01:16 +01:00
class StaticOrigin(AbstractConfiguration):
group_id = mapped_column(db.Integer, db.ForeignKey("group.id"), nullable=False)
2024-12-06 18:15:47 +00:00
storage_cloud_account_id = mapped_column(
db.Integer(), db.ForeignKey("cloud_account.id"), nullable=False
)
source_cloud_account_id = mapped_column(
db.Integer(), db.ForeignKey("cloud_account.id"), nullable=False
)
source_project = mapped_column(db.String(255), nullable=False)
auto_rotate = mapped_column(db.Boolean, nullable=False)
matrix_homeserver = mapped_column(db.String(255), nullable=True)
keanu_convene_path = mapped_column(db.String(255), nullable=True)
keanu_convene_config = mapped_column(db.String(), nullable=True)
clean_insights_backend = mapped_column(db.String(255), nullable=True)
origin_domain_name = mapped_column(db.String(255), nullable=True)
@property
def brn(self) -> BRN:
return BRN(
group_id=self.group_id,
product="mirror",
provider="aws",
resource_type="static",
2024-12-06 18:15:47 +00:00
resource_id=self.domain_name,
)
group = db.relationship("Group", back_populates="statics")
2024-12-06 18:15:47 +00:00
storage_cloud_account = db.relationship(
"CloudAccount",
back_populates="statics",
foreign_keys=[storage_cloud_account_id],
)
source_cloud_account = db.relationship(
"CloudAccount", back_populates="statics", foreign_keys=[source_cloud_account_id]
)
def destroy(self) -> None:
# TODO: The StaticMetaAutomation will clean up for now, but it should probably happen here for consistency
super().destroy()
def update(
2024-12-06 18:15:47 +00:00
self,
source_project: str,
description: str,
auto_rotate: bool,
matrix_homeserver: Optional[str],
keanu_convene_path: Optional[str],
keanu_convene_logo: Optional[FileStorage],
keanu_convene_color: Optional[str],
clean_insights_backend: Optional[Union[str, bool]],
db_session_commit: bool,
) -> None:
if isinstance(source_project, str):
self.source_project = source_project
else:
raise ValueError("source project must be a str")
if isinstance(description, str):
self.description = description
else:
raise ValueError("description must be a str")
if isinstance(auto_rotate, bool):
self.auto_rotate = auto_rotate
else:
raise ValueError("auto_rotate must be a bool")
if isinstance(matrix_homeserver, str):
self.matrix_homeserver = matrix_homeserver
else:
raise ValueError("matrix_homeserver must be a str")
if isinstance(keanu_convene_path, str):
self.keanu_convene_path = keanu_convene_path
else:
raise ValueError("keanu_convene_path must be a str")
if self.keanu_convene_config is None:
self.keanu_convene_config = "{}"
keanu_convene_config: Dict[str, Any] = json.loads(self.keanu_convene_config)
if keanu_convene_logo is None:
pass
elif isinstance(keanu_convene_logo, FileStorage):
if keanu_convene_logo.filename: # if False, no file was uploaded
keanu_convene_config["logo"] = create_data_uri(
2024-12-06 18:15:47 +00:00
thumbnail_uploaded_image(keanu_convene_logo),
keanu_convene_logo.filename,
)
else:
raise ValueError("keanu_convene_logo must be a FileStorage")
try:
if isinstance(keanu_convene_color, str):
2024-12-06 18:15:47 +00:00
keanu_convene_config["color"] = normalize_color(
keanu_convene_color
) # can raise ValueError
else:
raise ValueError() # re-raised below with message
except ValueError:
2024-12-06 18:15:47 +00:00
raise ValueError(
"keanu_convene_path must be a str containing an HTML color (CSS name or hex)"
)
self.keanu_convene_config = json.dumps(
keanu_convene_config, separators=(",", ":")
)
del keanu_convene_config # done with this temporary variable
2024-12-06 18:15:47 +00:00
if clean_insights_backend is None or (
isinstance(clean_insights_backend, bool) and not clean_insights_backend
):
self.clean_insights_backend = None
elif isinstance(clean_insights_backend, bool) and clean_insights_backend:
self.clean_insights_backend = "metrics.cleaninsights.org"
elif isinstance(clean_insights_backend, str):
self.clean_insights_backend = clean_insights_backend
else:
raise ValueError("clean_insights_backend must be a str, bool, or None")
if db_session_commit:
db.session.commit()
self.updated = datetime.now(tz=timezone.utc)
2024-12-06 18:15:47 +00:00
ResourceStatus = Union[
Literal["active"], Literal["pending"], Literal["expiring"], Literal["destroyed"]
]
class ProxyDict(TypedDict):
Id: int
OriginDomain: str
MirrorDomain: Optional[str]
Status: ResourceStatus
2022-04-22 14:01:16 +01:00
class Proxy(AbstractResource):
2024-12-06 18:15:47 +00:00
origin_id: Mapped[int] = mapped_column(
db.Integer, db.ForeignKey("origin.id"), nullable=False
)
pool_id: Mapped[Optional[int]] = mapped_column(db.Integer, db.ForeignKey("pool.id"))
provider: Mapped[str] = mapped_column(db.String(20), nullable=False)
psg: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
slug: Mapped[Optional[str]] = mapped_column(db.String(20), nullable=True)
2024-12-06 18:15:47 +00:00
terraform_updated: Mapped[Optional[datetime]] = mapped_column(
AwareDateTime(), nullable=True
)
url: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
2022-04-22 14:01:16 +01:00
origin: Mapped[Origin] = relationship("Origin", back_populates="proxies")
pool: Mapped[Pool] = relationship("Pool", back_populates="proxies")
@property
2022-06-15 11:50:15 +01:00
def brn(self) -> BRN:
return BRN(
group_id=self.origin.group_id,
product="mirror",
provider=self.provider,
resource_type="proxy",
2024-12-06 18:15:47 +00:00
resource_id=str(self.id),
2022-06-15 11:50:15 +01:00
)
2022-04-22 14:01:16 +01:00
@classmethod
2022-05-16 11:44:03 +01:00
def csv_header(cls) -> List[str]:
2022-04-22 14:01:16 +01:00
return super().csv_header() + [
2024-12-06 18:15:47 +00:00
"origin_id",
"provider",
"psg",
"slug",
"terraform_updated",
"url",
2022-04-22 14:01:16 +01:00
]
def to_dict(self) -> ProxyDict:
status: ResourceStatus = "active"
2024-11-10 13:38:51 +00:00
if self.url is None:
status = "pending"
if self.deprecated is not None:
status = "expiring"
if self.destroyed is not None:
status = "destroyed"
return {
"Id": self.id,
"OriginDomain": self.origin.domain_name,
"MirrorDomain": self.url.replace("https://", "") if self.url else None,
"Status": status,
}
class SmartProxy(AbstractResource):
group_id = mapped_column(db.Integer(), db.ForeignKey("group.id"), nullable=False)
instance_id = mapped_column(db.String(100), nullable=True)
provider = mapped_column(db.String(20), nullable=False)
region = mapped_column(db.String(20), nullable=False)
group = db.relationship("Group", back_populates="smart_proxies")
@property
2022-06-15 11:50:15 +01:00
def brn(self) -> BRN:
return BRN(
group_id=self.group_id,
product="mirror",
provider=self.provider,
resource_type="smart_proxy",
2024-12-06 18:15:47 +00:00
resource_id=str(1),
2022-06-15 11:50:15 +01:00
)