doorcontrol: Move HID card number decoding out of database query

Not really needed, and hard to make portable
This commit is contained in:
Adam Goldsmith 2024-08-26 23:23:53 -04:00
parent 32a91315ef
commit cbe684d918
6 changed files with 66 additions and 91 deletions

View File

@ -44,9 +44,6 @@ class HIDEventAdmin(DjangoObjectActions, admin.ModelAdmin):
readonly_fields = ["decoded_card_number"] readonly_fields = ["decoded_card_number"]
changelist_actions = ("refresh_all_doors",) changelist_actions = ("refresh_all_doors",)
def get_queryset(self, request):
return super().get_queryset(request).with_decoded_card_number()
@admin.display(boolean=True) @admin.display(boolean=True)
def _is_red(self, obj): def _is_red(self, obj):
return obj.is_red return obj.is_red

View File

@ -1,41 +1,62 @@
import dataclasses
from typing import Literal
import bitstring import bitstring
# Reference for H10301 card format: # Reference for H10301 card format:
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf # 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: class Credential:
def __init__(self, code=None, hex_code=None): bits: bitstring.Bits
if code is None and hex_code is None:
raise TypeError("Must set either code or hex for a Credential") @classmethod
elif code is not None and hex_code is not None: def from_code(cls, facility=int, card_number=int) -> "Credential":
raise TypeError("Cannot set both code and hex for a Credential") bits = bitstring.pack(
elif code is not None: "0b000000, 0b0, uint:8=facility, uint:16=card_number, 0b0",
self.bits = bitstring.pack( facility=facility,
"0b000000, 0b0, uint:8=facility, uint:16=number, 0b0", card_number=card_number,
facility=code[0],
number=code[1],
) )
self.bits[6] = self.bits[7:19].count(1) % 2 # even parity bits[6] = bits[7:19].count(1) % 2 # even parity
self.bits[31] = not (self.bits[19:31].count(1) % 2) # odd parity bits[31] = bits[19:31].count(0) % 2 # odd parity
elif hex_code is not None: return cls(bits)
self.bits = bitstring.Bits(hex=hex_code)
def __repr__(self): @classmethod
return f"Credential({self.code})" def from_hex(cls, hex_code: str) -> "Credential":
bits = bitstring.Bits(hex=hex_code)
def __eq__(self, other): if bits[:6].any(1):
return self.bits == other.bits 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 cls(bits)
return self.bits.int
@property @property
def code(self): def facility_code(self) -> int:
facility = self.bits[7:15].uint return self.bits[7:15].uint
code = self.bits[15:31].uint
return (facility, code)
@property @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() return self.bits.hex.upper()

View File

@ -3,14 +3,14 @@ from typing import Self
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.db.models import F, Func, OuterRef, Q, Subquery from django.db.models import OuterRef, Q, Subquery
from django.db.models.functions import Mod
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from membershipworks.models import Flag as MembershipWorksFlag from membershipworks.models import Flag as MembershipWorksFlag
from membershipworks.models import Member from membershipworks.models import Member
from .hid.Credential import Credential, InvalidHexCode
from .hid.DoorController import DoorController from .hid.DoorController import DoorController
@ -102,42 +102,6 @@ class AttributeScheduleRule(AbstractScheduleRule):
class HIDEventQuerySet(models.QuerySet): 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): def with_member_id(self):
return self.annotate( return self.annotate(
member_id=Subquery( 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}") return event_types.get(self.event_type, f"Unknown Event Type {self.event_type}")
def decoded_card_number(self) -> str | None: def decoded_card_number(self) -> str | None:
"""Requires annotations from `with_decoded_card_number`"""
if self.raw_card_number is None: if self.raw_card_number is None:
return None return None
elif self.card_is_26_bit: try:
if self.card_is_valid_26_bit: cred = Credential.from_hex(self.raw_card_number)
return f"{self.card_facility_code_26_bit} - {self.card_number_26_bit}" return f"{cred.facility_code} - {cred.card_number}"
else: except InvalidHexCode as e:
return "Invalid" return f"Invalid: {e}"
else:
return "Not 26 bit card"

View File

@ -20,6 +20,7 @@ class UnitTimeTable(tables.Table):
class DeniedAccessTable(tables.Table): class DeniedAccessTable(tables.Table):
decoded_card_number = tables.Column(orderable=False)
name = tables.TemplateColumn( name = tables.TemplateColumn(
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}" "{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
) )

View File

@ -38,12 +38,10 @@ class DoorMember:
def from_membershipworks_member(cls, member: Member, door: Door): def from_membershipworks_member(cls, member: Member, door: Door):
if member.access_card_facility_code and member.access_card_number: if member.access_card_facility_code and member.access_card_number:
credentials = { credentials = {
Credential( Credential.from_code(
code=(
member.access_card_facility_code, member.access_card_facility_code,
member.access_card_number, member.access_card_number,
) )
)
} }
else: else:
credentials = set() credentials = set()
@ -108,7 +106,7 @@ class DoorMember:
}, },
cardholderID=data.attrib["cardholderID"], cardholderID=data.attrib["cardholderID"],
credentials={ credentials={
Credential(hex_code=(c.attrib["rawCardNumber"])) Credential.from_hex(c.attrib["rawCardNumber"])
for c in data.findall("{*}Credential") for c in data.findall("{*}Credential")
}, },
schedules={r.attrib["scheduleName"] for r in data.findall("{*}Role")}, schedules={r.attrib["scheduleName"] for r in data.findall("{*}Role")},
@ -172,9 +170,11 @@ class DoorMember:
xml_credentials = [ xml_credentials = [
E.Credential( E.Credential(
{ {
"formatName": str(credential.code[0]), "formatName": str(credential.facility_code),
"cardNumber": str(credential.code[1]), "cardNumber": str(credential.card_number),
"formatID": self.door.card_formats[str(credential.code[0])], "formatID": self.door.card_formats[
str(credential.facility_code)
],
"isCard": "true", "isCard": "true",
"cardholderID": self.cardholderID, "cardholderID": self.cardholderID,
} }
@ -222,7 +222,7 @@ def update_door(door: Door, dry_run: bool = False):
} }
existing_door_credentials = { existing_door_credentials = {
Credential(hex_code=c.attrib["rawCardNumber"]) Credential.from_hex(c.attrib["rawCardNumber"])
for c in door.controller.get_credentials() for c in door.controller.get_credentials()
} }

View File

@ -206,12 +206,7 @@ class DeniedAccess(BaseAccessReport):
denied_event_types = [ denied_event_types = [
t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS") t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS")
] ]
return ( return super().get_table_data().filter(event_type__in=denied_event_types)
super()
.get_table_data()
.filter(event_type__in=denied_event_types)
.with_decoded_card_number()
)
@register_report @register_report