FAQ | This is a LIVE service | Changelog

Commit 42c07d19 authored by Monty Dawson's avatar Monty Dawson
Browse files

Add script to revoke duplicated issued cards.

parent 94e99245
......@@ -3,7 +3,6 @@ FROM registry.gitlab.developers.cam.ac.uk/uis/devops/infra/dockerimages-private/
# oracle-instantclient image is based on ubuntu
WORKDIR /usr/src/app
RUN pip install --upgrade pip
# Install specific requirements for the package.
RUN pip3 install --upgrade pip
......
......@@ -9,7 +9,7 @@ Usage:
enhanced-cards|card-photos|temporary-cards|available-barcodes|card-notes|
card-logos|holders|cards|barcodes|orphaned-cardholders|diff-enhanced-cards|
diff-temporary-cards|backsync-cards|reconcile-crsids|update-crsids|
cardholders-with-multiple-issued-cards
cardholders-with-multiple-issued-cards|revoke-duplicate-issued-cards
)
Options:
......@@ -49,6 +49,7 @@ from . import image_source
from . import upload
from . import reconcile_crsids
from . import update_crsids
from . import revoke_duplicate_cards
LOG = logging.getLogger(os.path.basename(sys.argv[0]))
......@@ -139,12 +140,16 @@ def main():
"available-barcodes": source.AVAILABLE_BARCODES_SELECT,
"card-notes": source.CARD_NOTES_SELECT,
"cardholders-with-multiple-issued-cards": (
source.CARDHOLDERS_WITH_MULTIPLE_ISSUED_CARDS_SELECT
revoke_duplicate_cards.SELECT_CARDHOLDERS_WITH_MULTIPLE_ISSUED_CARDS
)
}
connection = source.get_connection(settings['source'])
if opts['revoke-duplicate-issued-cards']:
data = revoke_duplicate_cards.revoke_duplicate_issued_cards(connection)
print(upload.write(settings, 'revoked_duplicate_cards', data, write_dir, export_format))
if opts['reconcile-crsids']:
if not settings.get('jackdaw') or not settings.get('jackdaw').get('dsn'):
raise ValueError('No `jackdaw` configuration provided in configuration file')
......
......@@ -3,6 +3,8 @@ from cx_Oracle import Connection
from logging import getLogger
from Levenshtein import distance
from .source import select
LOG = getLogger(__name__)
......@@ -74,22 +76,6 @@ SELECT_CONNECTED_ORG_NAMES = """
"""
def select(connection: Connection, select_statement: str, **kwargs):
"""
Utility method to run select against the given connection and return a generator of dicts.
"""
cursor = connection.cursor()
cursor.execute(select_statement, **kwargs)
field_names = [d[0].lower() for d in cursor.description]
for row in cursor:
yield {
field_name: row[index]
for index, field_name in enumerate(field_names)
}
def get_connected_org_names(card_connection: Connection, cam_uid: str):
"""
Return a string of all the connected organisation names for the given cardholder
......
from cx_Oracle import Connection
from logging import getLogger
from .source import select
LOG = getLogger(__name__)
"""
Revokes duplicate issued cards which exist within the card database. Duplicates are detected by
finding a cardholders with multiple issued cards and then revoking all but the card with the
latest issue number or card id (where multiple cards are found with the same issue number).
The root cause of the duplicate issued cards is tracked here:
* https://gitlab.developers.cam.ac.uk/uis/devops/iam/card-database/card-system/-/issues/32
This can be run locally using:
```shell
logan --docker-run-args "-v $(pwd)/config.yml:/config.yml -v $(pwd)/output:/output" \
-- -c /config.yml revoke-duplicate-issued-cards --dir /output
```
"""
SELECT_CARDHOLDERS_WITH_MULTIPLE_ISSUED_CARDS = """
SELECT card.cam_uid, card.display_name, count(*) as issued_card_count FROM cards card
WHERE
card.TERM_DATE IS NULL AND
card.RETURNED_DATE IS NULL AND
card.ISSUE_DATE IS NOT NULL AND
(card.EXPIRY_DATE IS NULL OR card.EXPIRY_DATE >= trunc(sysdate))
group by card.cam_uid, card.display_name
having count(*) > 1
"""
SELECT_ISSUED_CARDS_FOR_CARDHOLDER = """
SELECT card.card_id, card.issue_number FROM cards card
WHERE
card.TERM_DATE IS NULL AND
card.RETURNED_DATE IS NULL AND
card.ISSUE_DATE IS NOT NULL AND
(card.EXPIRY_DATE IS NULL OR card.EXPIRY_DATE >= trunc(sysdate)) AND
card.cam_uid = :cam_uid
"""
REVOKE_CARD_AS_DUPLICATE = """
UPDATE cards
set
TERM_DATE = trunc(sysdate),
REASON = 'Duplicate'
WHERE
card_id = :card_id
"""
def revoke_duplicate_issued_cards_for_cam_uid(connection: Connection, cam_uid: str):
"""
Revokes duplicate issued cards for a given cardholder identified by cam_uid, removing
all but the most recently issued card.
"""
issued_cards_for_cardholder = select(
connection, SELECT_ISSUED_CARDS_FOR_CARDHOLDER, cam_uid=cam_uid
)
# We want to revoke all but the card with the highest issue_number or card_id, so
# we create a list of card_ids sorted by issue_number and card_id and then split out
# the last element.
card_ids_to_revoke = [
card['card_id']
for card in sorted(issued_cards_for_cardholder, key=lambda card: (
card['issue_number'], card['card_id']
))
][:-1]
for card_id in card_ids_to_revoke:
cursor = connection.cursor()
cursor.execute(REVOKE_CARD_AS_DUPLICATE, card_id=card_id)
if cursor.rowcount != 1:
raise ValueError(
f'Could not revoke card {card_id} for cardholder {cam_uid}, '
f'number of affected rows is expected to be 1 but is {cursor.rowcount}'
)
cursor.execute('COMMIT')
return card_ids_to_revoke
def revoke_duplicate_issued_cards(connection: Connection):
"""
Fetches cardholders with duplicated cards and revokes all but the most recently
issued card held by a given cardholder.
"""
cam_uids_with_duplicate_cards = [
card['cam_uid']
for card in select(connection, SELECT_CARDHOLDERS_WITH_MULTIPLE_ISSUED_CARDS)
]
data = [['cam_uid', 'card_id']]
for cam_uid in cam_uids_with_duplicate_cards:
LOG.info(f'Revoking duplicate issued cards for {cam_uid}')
revoked_card_ids = revoke_duplicate_issued_cards_for_cam_uid(connection, cam_uid)
for card_id in revoked_card_ids:
data.append([cam_uid, card_id])
return data
......@@ -90,17 +90,6 @@ CARD_NOTES_SELECT = """
ORDER BY CARD_NOTE_ID
"""
CARDHOLDERS_WITH_MULTIPLE_ISSUED_CARDS_SELECT = """
SELECT card.cam_uid, card.display_name, count(*) as issued_card_count FROM cards card
WHERE
card.TERM_DATE IS NULL AND
card.RETURNED_DATE IS NULL AND
card.ISSUE_DATE IS NOT NULL AND
(card.EXPIRY_DATE IS NULL OR card.EXPIRY_DATE >= trunc(sysdate))
group by card.cam_uid, card.display_name
having count(*) > 1
"""
SELECT_CARDHOLDER_CRSID_BY_CAM_UID = """
SELECT CRSID FROM CARDHOLDERS WHERE CAM_UID=:camuid AND ROWNUM=1
"""
......@@ -160,6 +149,22 @@ def get_orphan_cards(connection: Connection,
return results
def select(connection: Connection, select_statement: str, **kwargs):
"""
Utility method to run select against the given connection and return a generator of dicts.
"""
cursor = connection.cursor()
cursor.execute(select_statement, **kwargs)
field_names = [d[0].lower() for d in cursor.description]
for row in cursor:
yield {
field_name: row[index]
for index, field_name in enumerate(field_names)
}
def make_select(select, order_by=None):
if order_by:
head, sep, _ = select.partition('ORDER BY')
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment