feat: add legacy device migration flow
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -269,6 +270,9 @@ class LicenseEvent(models.Model):
|
||||
REVOKED = "revoked", "撤销"
|
||||
SEAT_ASSIGNED = "seat_assigned", "绑定席位"
|
||||
SEAT_RELEASED = "seat_released", "解绑席位"
|
||||
MIGRATION_GRANTED = "migration_granted", "迁移资格授予"
|
||||
CREDENTIAL_ISSUED = "credential_issued", "设备凭证签发"
|
||||
CREDENTIAL_REVOKED = "credential_revoked", "设备凭证吊销"
|
||||
|
||||
entitlement = models.ForeignKey(
|
||||
SoftwareEntitlement,
|
||||
@@ -319,6 +323,194 @@ class LicenseEvent(models.Model):
|
||||
return f"{self.entitlement} {self.action}"
|
||||
|
||||
|
||||
class LegacyMigrationGrant(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
ACTIVE = "active", "有效"
|
||||
REVOKED = "revoked", "已撤销"
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name="用户",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="legacy_migration_grants",
|
||||
)
|
||||
product_code = models.CharField(
|
||||
"产品代码",
|
||||
max_length=32,
|
||||
choices=ClientDevice.ProductCode.choices,
|
||||
)
|
||||
entitlement = models.OneToOneField(
|
||||
SoftwareEntitlement,
|
||||
verbose_name="迁移权益",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="legacy_migration_grant",
|
||||
)
|
||||
eligibility_snapshot = models.JSONField("资格快照", default=dict, blank=True)
|
||||
status = models.CharField(
|
||||
"状态",
|
||||
max_length=20,
|
||||
choices=Status.choices,
|
||||
default=Status.ACTIVE,
|
||||
)
|
||||
reason = models.CharField("迁移原因", max_length=255)
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name="操作人",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="legacy_migration_grants_performed",
|
||||
)
|
||||
revoked_at = models.DateTimeField("撤销时间", null=True, blank=True)
|
||||
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
||||
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "legacy_migration_grant"
|
||||
verbose_name = "存量迁移资格"
|
||||
verbose_name_plural = "存量迁移资格"
|
||||
ordering = ("-created_at", "-id")
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("user", "product_code"),
|
||||
name="legacy_migration_grant_user_product_unique",
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=("product_code", "status")),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user} {self.product_code} 迁移资格"
|
||||
|
||||
|
||||
class MigrationRequest(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = "pending", "待确认"
|
||||
CONFIRMED = "confirmed", "已确认"
|
||||
EXPIRED = "expired", "已过期"
|
||||
REVOKED = "revoked", "已撤销"
|
||||
|
||||
request_id = models.UUIDField("公开迁移请求 ID", default=uuid.uuid4, unique=True, editable=False)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name="用户",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="migration_requests",
|
||||
)
|
||||
device = models.ForeignKey(
|
||||
ClientDevice,
|
||||
verbose_name="当前设备",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="migration_requests",
|
||||
)
|
||||
migration_grant = models.ForeignKey(
|
||||
LegacyMigrationGrant,
|
||||
verbose_name="迁移资格",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="requests",
|
||||
)
|
||||
credential_token_hash = models.CharField("待签发凭证哈希", max_length=64, unique=True, editable=False)
|
||||
credential_token_prefix = models.CharField("待签发凭证前缀", max_length=20, editable=False)
|
||||
status = models.CharField(
|
||||
"状态",
|
||||
max_length=20,
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
)
|
||||
expires_at = models.DateTimeField("确认截止时间")
|
||||
confirmed_at = models.DateTimeField("确认时间", null=True, blank=True)
|
||||
revoked_at = models.DateTimeField("撤销时间", null=True, blank=True)
|
||||
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
||||
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "migration_request"
|
||||
verbose_name = "迁移确认请求"
|
||||
verbose_name_plural = "迁移确认请求"
|
||||
ordering = ("-created_at", "-id")
|
||||
indexes = [
|
||||
models.Index(fields=("user", "device", "status")),
|
||||
models.Index(fields=("status", "expires_at")),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user} {self.device} {self.status}"
|
||||
|
||||
@staticmethod
|
||||
def generate_plaintext_credential_token() -> str:
|
||||
return f"dvc_cmhub_{secrets.token_urlsafe(32)}"
|
||||
|
||||
@staticmethod
|
||||
def hash_credential_token(raw_token: str) -> str:
|
||||
return hashlib.sha256(raw_token.encode("utf-8")).hexdigest()
|
||||
|
||||
def is_pending_at(self, now=None) -> bool:
|
||||
now = now or timezone.now()
|
||||
return self.status == self.Status.PENDING and self.expires_at > now
|
||||
|
||||
|
||||
class DeviceCredential(models.Model):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name="用户",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="device_credentials",
|
||||
)
|
||||
product_code = models.CharField(
|
||||
"产品代码",
|
||||
max_length=32,
|
||||
choices=ClientDevice.ProductCode.choices,
|
||||
)
|
||||
device = models.ForeignKey(
|
||||
ClientDevice,
|
||||
verbose_name="设备",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="credentials",
|
||||
)
|
||||
entitlement = models.ForeignKey(
|
||||
SoftwareEntitlement,
|
||||
verbose_name="软件权益",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="device_credentials",
|
||||
)
|
||||
seat = models.ForeignKey(
|
||||
LicenseSeat,
|
||||
verbose_name="授权席位",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="device_credentials",
|
||||
)
|
||||
migration_request = models.OneToOneField(
|
||||
MigrationRequest,
|
||||
verbose_name="来源迁移请求",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="credential",
|
||||
)
|
||||
token_hash = models.CharField("设备凭证哈希", max_length=64, unique=True, editable=False)
|
||||
token_prefix = models.CharField("设备凭证前缀", max_length=20, editable=False)
|
||||
expires_at = models.DateTimeField("凭证到期时间")
|
||||
revoked_at = models.DateTimeField("吊销时间", null=True, blank=True)
|
||||
revoke_reason = models.CharField("吊销原因", max_length=255, blank=True)
|
||||
created_at = models.DateTimeField("签发时间", auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "device_credential"
|
||||
verbose_name = "设备凭证"
|
||||
verbose_name_plural = "设备凭证"
|
||||
ordering = ("-created_at", "-id")
|
||||
indexes = [
|
||||
models.Index(fields=("user", "product_code", "revoked_at")),
|
||||
models.Index(fields=("device", "revoked_at")),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user} {self.product_code} credential"
|
||||
|
||||
def is_active_at(self, now=None) -> bool:
|
||||
now = now or timezone.now()
|
||||
return self.revoked_at is None and self.expires_at > now
|
||||
|
||||
|
||||
class DeviceSession(models.Model):
|
||||
TOKEN_PREFIX_LENGTH = 12
|
||||
|
||||
|
||||
Reference in New Issue
Block a user