Properly add, remove, and reassign credentials for existing members

This commit is contained in:
Adam Goldsmith 2019-11-08 15:51:44 -05:00
parent 8367c8bbc1
commit 21a9aa5b5c
4 changed files with 101 additions and 52 deletions

View File

@ -1,34 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import bitstring from common import doors
import requests
import csv
from io import StringIO
from common import *
# Reference for H10301 card format:
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
def hexToCode(h):
b = bitstring.Bits(hex=h)
facility = b[7:15].uint
code = b[15:31].uint
return ((facility, code))
def codeToHex(facility, code):
b = bitstring.pack('0b000000, uint:1, uint:8, uint:16, uint:1', 0,
facility, code, 0)
# calculate parity bits
b[6] = b[7:19].count(1) % 2 # even parity
b[31] = not (b[19:31].count(1) % 2) # odd parity
return b.hex.upper()
# hexToCode("01E29DA1") <-> codeToHex(241, 20176)
def forEachDoor(fxn): def forEachDoor(fxn):
for door in doors.values(): for door in doors.values():

38
hid/Credential.py Normal file
View File

@ -0,0 +1,38 @@
import bitstring
# Reference for H10301 card format:
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
class Credential:
def __init__(self, code=None, hex=None):
if code is None and hex is None:
raise TypeError("Must set either code or hex for a Credential")
elif code is not None and hex 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])
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 is not None:
self.bits = bitstring.Bits(hex=hex)
def __repr__(self):
return f"Credential({self.code})"
def __eq__(self, other):
return self.bits == other.bits
def __hash__(self):
return self.bits.int
@property
def code(self):
facility = self.bits[7:15].uint
code = self.bits[15:31].uint
return (facility, code)
@property
def hex(self):
return self.bits.hex.upper()

View File

@ -131,6 +131,12 @@ class DoorController():
"recordOffset": "0", "recordOffset": "0",
"recordCount": "1000"})))[0] "recordCount": "1000"})))[0]
def get_credentials(self):
return self.doXMLRequest(
ROOT(E.Credentials({"action": "LR",
"recordOffset": "0",
"recordCount": "1000"})))[0]
def get_lock(self): def get_lock(self):
el = ROOT( el = ROOT(
E.Doors({"action": "LR", "responseFormat": "status"})) E.Doors({"action": "LR", "responseFormat": "status"}))

View File

@ -4,12 +4,13 @@ from lxml import etree
from common import doors, getMembershipworksData, memberLevels from common import doors, getMembershipworksData, memberLevels
from hid.DoorController import E, ROOT from hid.DoorController import E, ROOT
from hid.Credential import Credential
class Member(): class Member():
def __init__(self, forename, surname, membershipWorksID, def __init__(self, forename, surname, membershipWorksID,
middleName="", email="", phone="", middleName="", email="", phone="",
cardholderID=None, doorAccess=[], onHold=False, cardholderID=None, doorAccess=[], onHold=False,
credentials=[], levels=[], schedules=[]): credentials=set(), levels=[], schedules=[]):
self.forename = forename self.forename = forename
self.surname = surname self.surname = surname
self.membershipWorksID = membershipWorksID self.membershipWorksID = membershipWorksID
@ -37,15 +38,13 @@ Schedules: {self.schedules}
""" """
@classmethod @classmethod
def from_cardholder(cls, data, cardFormats): def from_cardholder(cls, data):
# TODO: maybe keep all attribs, and just add them in # TODO: maybe keep all attribs, and just add them in
# from_MembershipWorks? # from_MembershipWorks?
cardFormatsByID = {v: k for k, v in cardFormats.items()} credentials = set(
credentials = [ Credential(hex=(c.attrib['rawCardNumber']))
(cardFormatsByID[c.attrib['formatID']], c.attrib['cardNumber']) for c in data.findall('{*}Credential'))
for c in data.findall('{*}Credential')] roles = [r.attrib['scheduleName'] for r in data.findall('{*}Role')]
roles = [r.attrib['scheduleName']
for r in data.findall('{*}Role')]
levels = data.attrib.get('custom1', "").split('|') levels = data.attrib.get('custom1', "").split('|')
return cls( return cls(
forename=data.get('forename', ""), forename=data.get('forename', ""),
@ -62,10 +61,11 @@ Schedules: {self.schedules}
@classmethod @classmethod
def from_MembershipWorks(cls, data): def from_MembershipWorks(cls, data):
if data["Access Card Number"] != "": if data["Access Card Number"] != "":
credentials = [(data["Access Card Facility Code"], credentials = set([Credential(
data["Access Card Number"])] code=(data["Access Card Facility Code"],
data["Access Card Number"]))])
else: else:
credentials = [] credentials = set()
levels = {k: v for k, v in memberLevels.items() if data[k] == k} levels = {k: v for k, v in memberLevels.items() if data[k] == k}
doorAccess = [door for door, doorData in doors.items() doorAccess = [door for door, doorData in doors.items()
@ -107,25 +107,27 @@ Schedules: {self.schedules}
return E.RoleSet({"action": "UD", "roleSetID": self.cardholderID}, return E.RoleSet({"action": "UD", "roleSetID": self.cardholderID},
E.Roles(*roles)) E.Roles(*roles))
def make_credentials(self, cardFormats): def make_credentials(self, newCredentials, cardFormats):
credentials = [ out = [
E.Credential( E.Credential(
{"formatName": credential[0], {"formatName": str(credential.code[0]),
"cardNumber": credential[1], "cardNumber": str(credential.code[1]),
"formatID": cardFormats[credential[0]], "formatID": cardFormats[str(credential.code[0])],
"isCard": "true", "isCard": "true",
"cardholderID": self.cardholderID}) "cardholderID": self.cardholderID})
for credential in self.credentials] for credential in newCredentials]
return E.Credentials({"action": "AD"}, *credentials) return E.Credentials({"action": "AD"}, *out)
def update_door(door, members): def update_door(door, members):
cardFormats = door.get_cardFormats() cardFormats = door.get_cardFormats()
cardholders = {member.membershipWorksID: member cardholders = {member.membershipWorksID: member
for member in [Member.from_cardholder(ch, cardFormats) for member in [Member.from_cardholder(ch)
for ch in door.get_cardholders()]} for ch in door.get_cardholders()]}
schedulesMap = door.get_scheduleMap() schedulesMap = door.get_scheduleMap()
allCredentials = set(Credential(hex=c.attrib['rawCardNumber'])
for c in door.get_credentials())
for member in members: for member in members:
# TODO: can I combine requests? # TODO: can I combine requests?
@ -144,7 +146,39 @@ def update_door(door, members):
if member.credentials != ch.credentials: if member.credentials != ch.credentials:
print(f"- Updating card for {member.forename} {member.surname}") print(f"- Updating card for {member.forename} {member.surname}")
print(f" - {ch.credentials} -> {member.credentials}") print(f" - {ch.credentials} -> {member.credentials}")
door.doXMLRequest(ROOT(member.make_credentials(cardFormats)))
oldCards = ch.credentials
newCards = member.credentials
allNewCards = set(card
for m in members if m != member
for card in m.credentials)
# cards removed, and won't be reassigned to someone else
for card in (oldCards - newCards) - allNewCards:
door.doXMLRequest(ROOT(E.Credentials(
{"action": "UD",
"rawCardNumber": card.hex,
"isCard": "true"},
E.Credential({"cardholderID": ""}))))
if newCards - oldCards: # cards added
for card in newCards & allNewCards: # new card exists in another member
raise Exception(f"Duplicate Card in input data! {card}")
# card existed in door, and needs to be reassigned
for card in newCards & allCredentials:
door.doXMLRequest(ROOT(E.Credentials(
{"action": "UD",
"rawCardNumber": card.hex,
"isCard": "true"},
E.Credential({"cardholderID": member.cardholderID}))))
# cards that never existed, and need to be created
if newCards - allCredentials:
door.doXMLRequest(ROOT(member.make_credentials(
newCards - allCredentials, cardFormats)))
if member.schedulesForDoor(door) != ch.schedules: if member.schedulesForDoor(door) != ch.schedules:
print("- Updating schedule for" + print("- Updating schedule for" +
@ -159,8 +193,8 @@ def update_door(door, members):
E.Cardholder(member.attribs())))) E.Cardholder(member.attribs()))))
member.cardholderID = resp.find('{*}Cardholders/{*}Cardholder') \ member.cardholderID = resp.find('{*}Cardholders/{*}Cardholder') \
.attrib["cardholderID"] .attrib["cardholderID"]
door.doXMLRequest(ROOT(member.make_credentials(cardFormats), door.doXMLRequest(ROOT(member.make_credentials(member.credentials, cardFormats),
member.update_schedules(door, schedulesMap))) member.make_schedules(door, schedulesMap)))
# TODO: delete cardholders that are no longer members? # TODO: delete cardholders that are no longer members?