doorcontrol: Move HID card number decoding out of database query
Not really needed, and hard to make portable
This commit is contained in:
parent
32a91315ef
commit
cbe684d918
@ -44,9 +44,6 @@ class HIDEventAdmin(DjangoObjectActions, admin.ModelAdmin):
|
||||
readonly_fields = ["decoded_card_number"]
|
||||
changelist_actions = ("refresh_all_doors",)
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).with_decoded_card_number()
|
||||
|
||||
@admin.display(boolean=True)
|
||||
def _is_red(self, obj):
|
||||
return obj.is_red
|
||||
|
@ -1,41 +1,62 @@
|
||||
import dataclasses
|
||||
from typing import Literal
|
||||
|
||||
import bitstring
|
||||
|
||||
# Reference for H10301 card format:
|
||||
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
|
||||
|
||||
|
||||
class InvalidHexCode(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Not26Bit(InvalidHexCode):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Card number > 26 bits")
|
||||
|
||||
|
||||
class InvalidParity(InvalidHexCode):
|
||||
def __init__(self, even_odd: Literal["even", "odd"]) -> None:
|
||||
super().__init__(f"Bad {even_odd} parity")
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Credential:
|
||||
def __init__(self, code=None, hex_code=None):
|
||||
if code is None and hex_code is None:
|
||||
raise TypeError("Must set either code or hex for a Credential")
|
||||
elif code is not None and hex_code is not None:
|
||||
raise TypeError("Cannot set both code and hex for a Credential")
|
||||
elif code is not None:
|
||||
self.bits = bitstring.pack(
|
||||
"0b000000, 0b0, uint:8=facility, uint:16=number, 0b0",
|
||||
facility=code[0],
|
||||
number=code[1],
|
||||
bits: bitstring.Bits
|
||||
|
||||
@classmethod
|
||||
def from_code(cls, facility=int, card_number=int) -> "Credential":
|
||||
bits = bitstring.pack(
|
||||
"0b000000, 0b0, uint:8=facility, uint:16=card_number, 0b0",
|
||||
facility=facility,
|
||||
card_number=card_number,
|
||||
)
|
||||
self.bits[6] = self.bits[7:19].count(1) % 2 # even parity
|
||||
self.bits[31] = not (self.bits[19:31].count(1) % 2) # odd parity
|
||||
elif hex_code is not None:
|
||||
self.bits = bitstring.Bits(hex=hex_code)
|
||||
bits[6] = bits[7:19].count(1) % 2 # even parity
|
||||
bits[31] = bits[19:31].count(0) % 2 # odd parity
|
||||
return cls(bits)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Credential({self.code})"
|
||||
@classmethod
|
||||
def from_hex(cls, hex_code: str) -> "Credential":
|
||||
bits = bitstring.Bits(hex=hex_code)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.bits == other.bits
|
||||
if bits[:6].any(1):
|
||||
raise Not26Bit
|
||||
if bits[6] != bits[7:19].count(1) % 2:
|
||||
raise InvalidParity("even")
|
||||
if bits[31] != (bits[19:31].count(0) % 2):
|
||||
raise InvalidParity("odd")
|
||||
|
||||
def __hash__(self):
|
||||
return self.bits.int
|
||||
return cls(bits)
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
facility = self.bits[7:15].uint
|
||||
code = self.bits[15:31].uint
|
||||
return (facility, code)
|
||||
def facility_code(self) -> int:
|
||||
return self.bits[7:15].uint
|
||||
|
||||
@property
|
||||
def hex(self):
|
||||
def card_number(self) -> int:
|
||||
return self.bits[15:31].uint
|
||||
|
||||
@property
|
||||
def hex(self) -> str:
|
||||
return self.bits.hex.upper()
|
||||
|
@ -3,14 +3,14 @@ from typing import Self
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import F, Func, OuterRef, Q, Subquery
|
||||
from django.db.models.functions import Mod
|
||||
from django.db.models import OuterRef, Q, Subquery
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from membershipworks.models import Flag as MembershipWorksFlag
|
||||
from membershipworks.models import Member
|
||||
|
||||
from .hid.Credential import Credential, InvalidHexCode
|
||||
from .hid.DoorController import DoorController
|
||||
|
||||
|
||||
@ -102,42 +102,6 @@ class AttributeScheduleRule(AbstractScheduleRule):
|
||||
|
||||
|
||||
class HIDEventQuerySet(models.QuerySet):
|
||||
def with_decoded_card_number(self):
|
||||
# TODO: CONV and BIT_COUNT are MySQL/MariaDB specific
|
||||
class Conv(Func):
|
||||
function = "CONV"
|
||||
arity = 3
|
||||
# This is technically not true, but fine for my purposes
|
||||
output_field = models.IntegerField()
|
||||
|
||||
class BitCount(Func):
|
||||
function = "BIT_COUNT"
|
||||
arity = 1
|
||||
|
||||
return (
|
||||
self.alias(card_number=Conv(F("raw_card_number"), 16, 10))
|
||||
.alias(more_than_26_bits=F("card_number").bitrightshift(26))
|
||||
.annotate(card_is_26_bit=Q(more_than_26_bits=0))
|
||||
.alias(
|
||||
parity_a=Mod(
|
||||
BitCount(F("card_number").bitrightshift(1).bitand(0xFFF)), 2
|
||||
),
|
||||
parity_b=Mod(
|
||||
BitCount(F("card_number").bitrightshift(13).bitand(0xFFF)), 2
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
card_is_valid_26_bit=~Q(parity_a=F("card_number").bitand(1))
|
||||
& Q(parity_b=F("card_number").bitrightshift(25).bitand(1))
|
||||
)
|
||||
.annotate(
|
||||
card_number_26_bit=F("card_number").bitrightshift(1).bitand(0xFFFF),
|
||||
card_facility_code_26_bit=F("card_number")
|
||||
.bitrightshift(17)
|
||||
.bitand(0xFF),
|
||||
)
|
||||
)
|
||||
|
||||
def with_member_id(self):
|
||||
return self.annotate(
|
||||
member_id=Subquery(
|
||||
@ -302,13 +266,10 @@ class HIDEvent(models.Model):
|
||||
return event_types.get(self.event_type, f"Unknown Event Type {self.event_type}")
|
||||
|
||||
def decoded_card_number(self) -> str | None:
|
||||
"""Requires annotations from `with_decoded_card_number`"""
|
||||
if self.raw_card_number is None:
|
||||
return None
|
||||
elif self.card_is_26_bit:
|
||||
if self.card_is_valid_26_bit:
|
||||
return f"{self.card_facility_code_26_bit} - {self.card_number_26_bit}"
|
||||
else:
|
||||
return "Invalid"
|
||||
else:
|
||||
return "Not 26 bit card"
|
||||
try:
|
||||
cred = Credential.from_hex(self.raw_card_number)
|
||||
return f"{cred.facility_code} - {cred.card_number}"
|
||||
except InvalidHexCode as e:
|
||||
return f"Invalid: {e}"
|
||||
|
@ -20,6 +20,7 @@ class UnitTimeTable(tables.Table):
|
||||
|
||||
|
||||
class DeniedAccessTable(tables.Table):
|
||||
decoded_card_number = tables.Column(orderable=False)
|
||||
name = tables.TemplateColumn(
|
||||
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
|
||||
)
|
||||
|
@ -38,12 +38,10 @@ class DoorMember:
|
||||
def from_membershipworks_member(cls, member: Member, door: Door):
|
||||
if member.access_card_facility_code and member.access_card_number:
|
||||
credentials = {
|
||||
Credential(
|
||||
code=(
|
||||
Credential.from_code(
|
||||
member.access_card_facility_code,
|
||||
member.access_card_number,
|
||||
)
|
||||
)
|
||||
}
|
||||
else:
|
||||
credentials = set()
|
||||
@ -108,7 +106,7 @@ class DoorMember:
|
||||
},
|
||||
cardholderID=data.attrib["cardholderID"],
|
||||
credentials={
|
||||
Credential(hex_code=(c.attrib["rawCardNumber"]))
|
||||
Credential.from_hex(c.attrib["rawCardNumber"])
|
||||
for c in data.findall("{*}Credential")
|
||||
},
|
||||
schedules={r.attrib["scheduleName"] for r in data.findall("{*}Role")},
|
||||
@ -172,9 +170,11 @@ class DoorMember:
|
||||
xml_credentials = [
|
||||
E.Credential(
|
||||
{
|
||||
"formatName": str(credential.code[0]),
|
||||
"cardNumber": str(credential.code[1]),
|
||||
"formatID": self.door.card_formats[str(credential.code[0])],
|
||||
"formatName": str(credential.facility_code),
|
||||
"cardNumber": str(credential.card_number),
|
||||
"formatID": self.door.card_formats[
|
||||
str(credential.facility_code)
|
||||
],
|
||||
"isCard": "true",
|
||||
"cardholderID": self.cardholderID,
|
||||
}
|
||||
@ -222,7 +222,7 @@ def update_door(door: Door, dry_run: bool = False):
|
||||
}
|
||||
|
||||
existing_door_credentials = {
|
||||
Credential(hex_code=c.attrib["rawCardNumber"])
|
||||
Credential.from_hex(c.attrib["rawCardNumber"])
|
||||
for c in door.controller.get_credentials()
|
||||
}
|
||||
|
||||
|
@ -206,12 +206,7 @@ class DeniedAccess(BaseAccessReport):
|
||||
denied_event_types = [
|
||||
t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS")
|
||||
]
|
||||
return (
|
||||
super()
|
||||
.get_table_data()
|
||||
.filter(event_type__in=denied_event_types)
|
||||
.with_decoded_card_number()
|
||||
)
|
||||
return super().get_table_data().filter(event_type__in=denied_event_types)
|
||||
|
||||
|
||||
@register_report
|
||||
|
Loading…
Reference in New Issue
Block a user