diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 55326652..00000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -ignore = E231,E265,E501,E722,W503 diff --git a/Dockerfile b/Dockerfile index d0d56de0..21702815 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,50 +1,53 @@ # syntax=docker/dockerfile:1 # Prepare the base environment. -FROM python:3.12.4-slim AS builder_base_itassets +FROM python:3.12.6-alpine AS builder_base LABEL org.opencontainers.image.authors=asi@dbca.wa.gov.au LABEL org.opencontainers.image.source=https://github.com/dbca-wa/it-assets -RUN apt-get update -y \ - && apt-get upgrade -y \ - && apt-get install -y libmagic-dev gcc binutils gdal-bin proj-bin python3-dev libpq-dev gzip \ - && rm -rf /var/lib/apt/lists/* \ - && pip install --root-user-action=ignore --upgrade pip - -# Temporary additional steps to mitigate CVE-2023-45853 (zlibg). -#WORKDIR /zlib -# Additional requirements to build zlibg -#RUN apt-get update -y \ -# && apt-get install -y wget build-essential make libc-dev \ -#RUN wget -q https://zlib.net/zlib-1.3.1.tar.gz && tar xvzf zlib-1.3.1.tar.gz -#WORKDIR /zlib/zlib-1.3.1 -#RUN ./configure --prefix=/usr/lib --libdir=/usr/lib/x86_64-linux-gnu \ -# && make \ -# && make install \ -# && rm -rf /zlib +# Install system requirements to build Python packages. +RUN apk add --no-cache \ + gcc \ + libressl-dev \ + musl-dev \ + libffi-dev +# Create a non-root user to run the application. +ARG UID=10001 +ARG GID=10001 +RUN addgroup -g ${GID} appuser \ + && adduser -H -D -u ${UID} -G appuser appuser # Install Python libs using Poetry. -FROM builder_base_itassets AS python_libs_itassets +FROM builder_base AS python_libs_itassets +# Add system dependencies required to use GDAL +# Ref: https://stackoverflow.com/a/59040511/14508 +RUN apk add --no-cache \ + gdal \ + geos \ + proj \ + binutils \ + && ln -s /usr/lib/libproj.so.25 /usr/lib/libproj.so \ + && ln -s /usr/lib/libgdal.so.35 /usr/lib/libgdal.so \ + && ln -s /usr/lib/libgeos_c.so.1 /usr/lib/libgeos_c.so WORKDIR /app -ARG POETRY_VERSION=1.8.3 -RUN pip install --no-cache-dir --root-user-action=ignore poetry==${POETRY_VERSION} COPY poetry.lock pyproject.toml ./ -RUN poetry config virtualenvs.create false \ +ARG POETRY_VERSION=1.8.3 +RUN pip install --no-cache-dir --root-user-action=ignore poetry==${POETRY_VERSION} \ + && poetry config virtualenvs.create false \ && poetry install --no-interaction --no-ansi --only main - -# Create a non-root user. -ARG UID=10001 -ARG GID=10001 -RUN groupadd -g "${GID}" appuser \ - && useradd --no-create-home --no-log-init --uid "${UID}" --gid "${GID}" appuser +# Remove system libraries, no longer required. +RUN apk del \ + gcc \ + libressl-dev \ + musl-dev \ + libffi-dev # Install the project. -FROM python_libs_itassets +FROM python_libs_itassets AS project_itassets COPY gunicorn.py manage.py ./ COPY itassets ./itassets COPY registers ./registers COPY organisation ./organisation RUN python manage.py collectstatic --noinput - USER ${UID} EXPOSE 8080 CMD ["gunicorn", "itassets.wsgi", "--config", "gunicorn.py"] diff --git a/Dockerfile.debian b/Dockerfile.debian new file mode 100644 index 00000000..d0d56de0 --- /dev/null +++ b/Dockerfile.debian @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 +# Prepare the base environment. +FROM python:3.12.4-slim AS builder_base_itassets +LABEL org.opencontainers.image.authors=asi@dbca.wa.gov.au +LABEL org.opencontainers.image.source=https://github.com/dbca-wa/it-assets + +RUN apt-get update -y \ + && apt-get upgrade -y \ + && apt-get install -y libmagic-dev gcc binutils gdal-bin proj-bin python3-dev libpq-dev gzip \ + && rm -rf /var/lib/apt/lists/* \ + && pip install --root-user-action=ignore --upgrade pip + +# Temporary additional steps to mitigate CVE-2023-45853 (zlibg). +#WORKDIR /zlib +# Additional requirements to build zlibg +#RUN apt-get update -y \ +# && apt-get install -y wget build-essential make libc-dev \ +#RUN wget -q https://zlib.net/zlib-1.3.1.tar.gz && tar xvzf zlib-1.3.1.tar.gz +#WORKDIR /zlib/zlib-1.3.1 +#RUN ./configure --prefix=/usr/lib --libdir=/usr/lib/x86_64-linux-gnu \ +# && make \ +# && make install \ +# && rm -rf /zlib + +# Install Python libs using Poetry. +FROM builder_base_itassets AS python_libs_itassets +WORKDIR /app +ARG POETRY_VERSION=1.8.3 +RUN pip install --no-cache-dir --root-user-action=ignore poetry==${POETRY_VERSION} +COPY poetry.lock pyproject.toml ./ +RUN poetry config virtualenvs.create false \ + && poetry install --no-interaction --no-ansi --only main + +# Create a non-root user. +ARG UID=10001 +ARG GID=10001 +RUN groupadd -g "${GID}" appuser \ + && useradd --no-create-home --no-log-init --uid "${UID}" --gid "${GID}" appuser + +# Install the project. +FROM python_libs_itassets +COPY gunicorn.py manage.py ./ +COPY itassets ./itassets +COPY registers ./registers +COPY organisation ./organisation +RUN python manage.py collectstatic --noinput + +USER ${UID} +EXPOSE 8080 +CMD ["gunicorn", "itassets.wsgi", "--config", "gunicorn.py"] diff --git a/README.md b/README.md index 7ac11b17..d6fe9bc2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This project consists of a Django application used by the Department of Biodiversity, Conservation and Attractions to record and manage IT assets and analytics. -# Installation +## Installation The recommended way to set up this project for development is using [Poetry](https://python-poetry.org/docs/) to install and manage a virtual Python @@ -12,15 +12,19 @@ environment. With Poetry installed, change into the project directory and run: poetry install +Activate the virtualenv like so: + + poetry shell + To run Python commands in the virtualenv, thereafter run them like so: - poetry run python manage.py + python manage.py Manage new or updating project dependencies with Poetry also, like so: poetry add newpackage==1.0 -# Environment variables +## Environment variables This project uses confy to set environment variables (in a `.env` file). The following variables are required for the project to run: @@ -28,38 +32,38 @@ The following variables are required for the project to run: DATABASE_URL="postgis://USER:PASSWORD@HOST:PORT/DATABASE_NAME" SECRET_KEY="ThisIsASecretKey" -# Running +## Running Use `runserver` to run a local copy of the application: - poetry run python manage.py runserver 0:8080 + python manage.py runserver 0:8080 Run console commands manually: - poetry run python manage.py shell_plus + python manage.py shell_plus -# Unit tests +## Unit tests Start with `pip install coverage`. Run unit tests and obtain test coverage as follows: - poetry run coverage run --source='.' manage.py test -k - poetry run coverage report -m + coverage run --source='.' manage.py test -k + coverage report -m -# Docker image +## Docker image To build a new Docker image from the `Dockerfile`: docker image build -t ghcr.io/dbca-wa/it-assets . -# Pre-commit hooks +## Pre-commit hooks This project includes the following pre-commit hooks: -- TruffleHog: https://docs.trufflesecurity.com/docs/scanning-git/precommit-hooks/ +- TruffleHog (credential scanning): Pre-commit hooks may have additional system dependencies to run. Optionally -install pre-commit hooks locally like so: +install pre-commit hooks locally like so (with the virtualenv activated first): - poetry run pre-commit install --allow-missing-config + pre-commit install -Reference: https://pre-commit.com/ +Reference: diff --git a/kustomize/overlays/prod/kustomization.yaml b/kustomize/overlays/prod/kustomization.yaml index 9d92fc59..885d7596 100644 --- a/kustomize/overlays/prod/kustomization.yaml +++ b/kustomize/overlays/prod/kustomization.yaml @@ -35,4 +35,4 @@ patches: - path: postgres_fdw_service_patch.yaml images: - name: ghcr.io/dbca-wa/it-assets - newTag: 2.4.29 + newTag: 2.4.30 diff --git a/organisation/ascender.py b/organisation/ascender.py index b96aa79c..3d3b8a98 100644 --- a/organisation/ascender.py +++ b/organisation/ascender.py @@ -1,22 +1,17 @@ -from datetime import date, datetime, timedelta +import logging +import secrets +from datetime import date, datetime + +import requests from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.utils import timezone -import logging -from psycopg import connect -import requests -import secrets +from psycopg import connect, sql from itassets.utils import ms_graph_client_token from organisation.microsoft_products import MS_PRODUCTS -from organisation.models import ( - DepartmentUser, - DepartmentUserLog, - CostCentre, - Location, - AscenderActionLog, -) -from organisation.utils import title_except, ms_graph_subscribed_sku +from organisation.models import AscenderActionLog, CostCentre, DepartmentUser, DepartmentUserLog, Location +from organisation.utils import ms_graph_subscribed_sku, ms_graph_validate_password, title_except LOGGER = logging.getLogger("organisation") DATE_MAX = date(2049, 12, 31) @@ -193,16 +188,31 @@ def ascender_db_fetch(employee_id=None): """Returns an iterator which yields all rows from the Ascender database query. Optionally pass employee_id to filter on a single employee. """ + if employee_id: + # Validate `employee_id`: this value needs be castable as an integer, even though we use it as a string. + try: + int(employee_id) + except ValueError: + raise ValueError("Invalid employee ID value") + conn = get_ascender_connection() cur = conn.cursor() - columns = ", ".join(f[0] if isinstance(f, (list, tuple)) else f for f in FOREIGN_TABLE_FIELDS) - schema = settings.FOREIGN_SCHEMA - table = settings.FOREIGN_TABLE + columns = sql.SQL(",").join( + sql.Identifier(f[0]) if isinstance(f, (list, tuple)) else sql.Identifier(f) for f in FOREIGN_TABLE_FIELDS + ) + schema = sql.Identifier(settings.FOREIGN_SCHEMA) + table = sql.Identifier(settings.FOREIGN_TABLE) + employee_no = sql.Identifier("employee_no") + if employee_id: - query = f"SELECT {columns} FROM {schema}.{table} WHERE employee_no = '{employee_id}'" + # query = f"SELECT {columns} FROM {schema}.{table} WHERE employee_no = '{employee_id}'" + query = sql.SQL("SELECT {columns} FROM {schema}.{table} WHERE {employee_no} = %s").format( + columns=columns, schema=schema, table=table, employee_no=employee_no + ) + cur.execute(query, (employee_id,)) else: - query = f"SELECT {columns} FROM {schema}.{table}" - cur.execute(query) + query = sql.SQL("SELECT {columns} FROM {schema}.{table}").format(columns=columns, schema=schema, table=table) + cur.execute(query) while True: row = cur.fetchone() @@ -268,7 +278,7 @@ def ascender_employees_fetch_all(): return records -def check_ascender_user_account_rules(job, ignore_job_start_date=False, logging=False): +def check_ascender_user_account_rules(job, ignore_job_start_date=False, manager_override_email=None, logging=False): """Given a passed-in Ascender record and any qualifiers, determine whether a new Azure AD account can be provisioned for that user. The 'job start date' rule can be optionally bypassed. @@ -322,7 +332,15 @@ def check_ascender_user_account_rules(job, ignore_job_start_date=False, logging= licence_type = "Cloud" # Rule: user must have a manager recorded, and that manager must exist in our database. - if job["manager_emp_no"] and DepartmentUser.objects.filter(employee_id=job["manager_emp_no"]).exists(): + # Partial exception: if the email is specified, we can override the manager in Ascender. + # That specifed manager must still exist in our database to proceed. + if manager_override_email and DepartmentUser.objects.filter(email=manager_override_email).exists(): + manager = DepartmentUser.objects.get(email=manager_override_email) + elif manager_override_email and not DepartmentUser.objects.filter(email=manager_override_email).exists(): + if logging: + LOGGER.warning(f"Manager with email {manager_override_email} not present in IT Assets, aborting") + return False + elif job["manager_emp_no"] and DepartmentUser.objects.filter(employee_id=job["manager_emp_no"]).exists(): manager = DepartmentUser.objects.get(employee_id=job["manager_emp_no"]) elif job["manager_emp_no"] and not DepartmentUser.objects.filter(employee_id=job["manager_emp_no"]).exists(): if logging: @@ -486,7 +504,7 @@ def ascender_user_import_all(): create_ad_user_account(job, cc, job_start_date, licence_type, manager, location, token) -def ascender_user_import(employee_id, ignore_job_start_date=False): +def ascender_user_import(employee_id, ignore_job_start_date=False, manager_override_email=None): """A convenience function to import a single Ascender employee and create an AD account for them. This is to allow easier manual intervention where a record goes in after the start date, or an old employee returns to work and needs a new account created. @@ -499,7 +517,7 @@ def ascender_user_import(employee_id, ignore_job_start_date=False): return None job = jobs[0] - rules_passed = check_ascender_user_account_rules(job, ignore_job_start_date, logging=True) + rules_passed = check_ascender_user_account_rules(job, ignore_job_start_date, manager_override_email, logging=True) if not rules_passed: LOGGER.warning(f"Ascender employee ID {employee_id} import did not pass all rules") return None @@ -645,6 +663,15 @@ def create_ad_user_account(job, cc, job_start_date, licence_type, manager, locat LOGGER.info(f"Creating new Azure AD account: {display_name}, {email}, {licence_type} account") + # Generate an account password and validate its complexity. + # Reference: https://docs.python.org/3/library/secrets.html#secrets.token_urlsafe + password = None + while password is None: + password = secrets.token_urlsafe(20) + resp = ms_graph_validate_password(password) + if not resp.json()["isValid"]: + password = None + # Configuration setting to explicitly allow creation of new AD users. if not settings.ASCENDER_CREATE_AZURE_AD: LOGGER.info(f"Skipping creation of new Azure AD account: {ascender_record} (ASCENDER_CREATE_AZURE_AD == False)") @@ -671,9 +698,7 @@ def create_ad_user_account(job, cc, job_start_date, licence_type, manager, locat "mailNickname": mail_nickname, "passwordProfile": { "forceChangePasswordNextSignIn": True, - # Generated password should always meet our complexity requirements. - # Reference: https://docs.python.org/3/library/secrets.html#secrets.token_urlsafe - "password": secrets.token_urlsafe(16), + "password": password, }, } resp = requests.post(url, headers=headers, json=data) diff --git a/organisation/management/commands/azure_account_provision.py b/organisation/management/commands/azure_account_provision.py index 9e66999f..606a323d 100644 --- a/organisation/management/commands/azure_account_provision.py +++ b/organisation/management/commands/azure_account_provision.py @@ -1,5 +1,7 @@ -from django.core.management.base import BaseCommand import logging + +from django.core.management.base import BaseCommand + from organisation.ascender import ascender_user_import @@ -21,16 +23,33 @@ def add_arguments(self, parser): dest="ignore_job_start_date", help="Ignore restriction related to job starting date", ) + parser.add_argument( + "--manager-override-email", + action="store", + type=str, + dest="manager_override_email", + help="Override the manager in Ascender in favour of using the supplied email", + ) def handle(self, *args, **options): logger = logging.getLogger("organisation") - logger.info(f"Provisioning Azure user account for Ascender employee ID {options['employee_id']}") + + employee_id = options["employee_id"] + ignore_job_start_date = False + manager_override_email = None + if "ignore_job_start_date" in options and options["ignore_job_start_date"]: + ignore_job_start_date = True logger.info("Ignoring job start date restriction") - user = ascender_user_import(options["employee_id"], ignore_job_start_date=True) - else: - user = ascender_user_import(options["employee_id"]) + + if "manager_override_email" in options and options["manager_override_email"]: + manager_override_email = options["manager_override_email"] + logger.info(f"Overriding manager, using email {manager_override_email}") + + user = ascender_user_import( + employee_id, ignore_job_start_date=ignore_job_start_date, manager_override_email=manager_override_email + ) if user: logger.info(f"Azure user account for {user.email} provisioned") diff --git a/organisation/utils.py b/organisation/utils.py index d92d30cb..312ec91d 100644 --- a/organisation/utils.py +++ b/organisation/utils.py @@ -1,32 +1,59 @@ -from datetime import datetime, timedelta -from dateutil.parser import parse -from django.conf import settings -from django.utils import timezone -from io import BytesIO import os import re +from datetime import datetime, timedelta +from io import BytesIO + import requests import unicodecsv as csv -from itassets.utils import ms_graph_client_token, upload_blob, get_blob_json +from dateutil.parser import parse +from django.conf import settings +from django.utils import timezone + +from itassets.utils import get_blob_json, ms_graph_client_token, upload_blob from .microsoft_products import MS_PRODUCTS def title_except(s, exceptions=None, acronyms=None): - """Utility function to title-case words in a job title, except for all the exceptions and edge cases. - """ + """Utility function to title-case words in a job title, except for all the exceptions and edge cases.""" if not exceptions: - exceptions = ('the', 'of', 'for', 'and', 'or') + exceptions = ("the", "of", "for", "and", "or") if not acronyms: acronyms = ( - 'OIM', 'IT', 'PVS', 'SFM', 'OT', 'NP', 'FMDP', 'VRM', 'TEC', 'GIS', 'ODG', 'RIA', 'ICT', - 'RSD', 'CIS', 'PSB', 'FMB', 'CFO', 'BCS', 'CIO', 'EHP', 'FSB', 'FMP', 'DBCA', 'ZPA', 'FOI', - 'ARP', 'WA', 'HR', + "OIM", + "IT", + "PVS", + "SFM", + "OT", + "NP", + "FMDP", + "VRM", + "TEC", + "GIS", + "ODG", + "RIA", + "ICT", + "RSD", + "CIS", + "PSB", + "FMB", + "CFO", + "BCS", + "CIO", + "EHP", + "FSB", + "FMP", + "DBCA", + "ZPA", + "FOI", + "ARP", + "WA", + "HR", ) words = s.split() - if words[0].startswith('A/'): - words_title = ['A/' + words[0].replace('A/', '').capitalize()] + if words[0].startswith("A/"): + words_title = ["A/" + words[0].replace("A/", "").capitalize()] elif words[0] in acronyms: words_title = [words[0]] else: @@ -35,19 +62,19 @@ def title_except(s, exceptions=None, acronyms=None): for word in words[1:]: word = word.lower() - if word.startswith('('): - pre = '(' - word = word.replace('(', '') + if word.startswith("("): + pre = "(" + word = word.replace("(", "") else: - pre = '' + pre = "" - if word.endswith(')'): - post = ')' - word = word.replace(')', '') + if word.endswith(")"): + post = ")" + word = word.replace(")", "") else: - post = '' + post = "" - if word.replace(',', '').upper() in acronyms: + if word.replace(",", "").upper() in acronyms: word = word.upper() elif word in exceptions: pass @@ -56,7 +83,7 @@ def title_except(s, exceptions=None, acronyms=None): words_title.append(pre + word + post) - return ' '.join(words_title) + return " ".join(words_title) def ms_graph_subscribed_skus(token=None): @@ -76,13 +103,13 @@ def ms_graph_subscribed_skus(token=None): resp.raise_for_status() j = resp.json() - while '@odata.nextLink' in j: - skus = skus + j['value'] - resp = requests.get(j['@odata.nextLink'], headers=headers) + while "@odata.nextLink" in j: + skus = skus + j["value"] + resp = requests.get(j["@odata.nextLink"], headers=headers) resp.raise_for_status() j = resp.json() - skus = skus + j['value'] # Final page. + skus = skus + j["value"] # Final page. return skus @@ -121,44 +148,50 @@ def ms_graph_users(licensed=False, token=None): resp.raise_for_status() j = resp.json() - while '@odata.nextLink' in j: - users = users + j['value'] - resp = requests.get(j['@odata.nextLink'], headers=headers) + while "@odata.nextLink" in j: + users = users + j["value"] + resp = requests.get(j["@odata.nextLink"], headers=headers) resp.raise_for_status() j = resp.json() - users = users + j['value'] # Final page + users = users + j["value"] # Final page aad_users = [] for user in users: - if not user['mail'] or not user['mail'].lower().endswith('@dbca.wa.gov.au'): + if not user["mail"] or not user["mail"].lower().endswith("@dbca.wa.gov.au"): continue - aad_users.append({ - 'objectId': user['id'], - 'mail': user['mail'].lower(), - 'userPrincipalName': user['userPrincipalName'], - 'displayName': user['displayName'] if user['displayName'] else None, - 'givenName': user['givenName'] if user['givenName'] else None, - 'surname': user['surname'] if user['surname'] else None, - 'employeeId': user['employeeId'] if user['employeeId'] else None, - 'employeeType': user['employeeType'] if user['employeeType'] else None, - 'jobTitle': user['jobTitle'] if user['jobTitle'] else None, - 'telephoneNumber': user['businessPhones'][0] if user['businessPhones'] else None, - 'mobilePhone': user['mobilePhone'] if user['mobilePhone'] else None, - 'department': user['department'] if user['department'] else None, - 'companyName': user['companyName'] if user['companyName'] else None, - 'officeLocation': user['officeLocation'] if user['officeLocation'] else None, - 'proxyAddresses': [i.lower().replace('smtp:', '') for i in user['proxyAddresses'] if i.lower().startswith('smtp')], - 'accountEnabled': user['accountEnabled'], - 'onPremisesSyncEnabled': user['onPremisesSyncEnabled'], - 'onPremisesSamAccountName': user['onPremisesSamAccountName'], - 'lastPasswordChangeDateTime': user['lastPasswordChangeDateTime'], - 'assignedLicenses': [i['skuId'] for i in user['assignedLicenses']], - 'manager': {'id': user['manager']['id'], 'mail': user['manager']['mail']} if 'manager' in user else None, - }) + aad_users.append( + { + "objectId": user["id"], + "mail": user["mail"].lower(), + "userPrincipalName": user["userPrincipalName"], + "displayName": user["displayName"] if user["displayName"] else None, + "givenName": user["givenName"] if user["givenName"] else None, + "surname": user["surname"] if user["surname"] else None, + "employeeId": user["employeeId"] if user["employeeId"] else None, + "employeeType": user["employeeType"] if user["employeeType"] else None, + "jobTitle": user["jobTitle"] if user["jobTitle"] else None, + "telephoneNumber": user["businessPhones"][0] if user["businessPhones"] else None, + "mobilePhone": user["mobilePhone"] if user["mobilePhone"] else None, + "department": user["department"] if user["department"] else None, + "companyName": user["companyName"] if user["companyName"] else None, + "officeLocation": user["officeLocation"] if user["officeLocation"] else None, + "proxyAddresses": [ + i.lower().replace("smtp:", "") for i in user["proxyAddresses"] if i.lower().startswith("smtp") + ], + "accountEnabled": user["accountEnabled"], + "onPremisesSyncEnabled": user["onPremisesSyncEnabled"], + "onPremisesSamAccountName": user["onPremisesSamAccountName"], + "lastPasswordChangeDateTime": user["lastPasswordChangeDateTime"], + "assignedLicenses": [i["skuId"] for i in user["assignedLicenses"]], + "manager": {"id": user["manager"]["id"], "mail": user["manager"]["mail"]} + if "manager" in user + else None, + } + ) if licensed: - return [u for u in aad_users if u['assignedLicenses']] + return [u for u in aad_users if u["assignedLicenses"]] else: return aad_users @@ -184,21 +217,21 @@ def ms_graph_users_signinactivity(licensed=False, token=None): resp = requests.get(url, headers=headers) j = resp.json() - while '@odata.nextLink' in j: - users = users + j['value'] - resp = requests.get(j['@odata.nextLink'], headers=headers) + while "@odata.nextLink" in j: + users = users + j["value"] + resp = requests.get(j["@odata.nextLink"], headers=headers) resp.raise_for_status() j = resp.json() - users = users + j['value'] # Final page + users = users + j["value"] # Final page user_signins = [] for user in users: if licensed: - if 'signInActivity' in user and user['signInActivity'] and user['assignedLicenses']: + if "signInActivity" in user and user["signInActivity"] and user["assignedLicenses"]: user_signins.append(user) else: - if 'signInActivity' in user and user['signInActivity']: + if "signInActivity" in user and user["signInActivity"]: user_signins.append(user) return user_signins @@ -225,25 +258,29 @@ def ms_graph_dormant_accounts(days=90, licensed=False, token=None): then = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=days) accounts = [] for user in user_signins: - accounts.append({ - 'mail': user['mail'], - 'userPrincipalName': user['userPrincipalName'], - 'id': user['id'], - 'accountEnabled': user['accountEnabled'], - 'assignedLicenses': user['assignedLicenses'], - 'lastSignInDateTime': parse(user['signInActivity']['lastSignInDateTime']).astimezone(settings.TZ) if user['signInActivity']['lastSignInDateTime'] else None, - }) + accounts.append( + { + "mail": user["mail"], + "userPrincipalName": user["userPrincipalName"], + "id": user["id"], + "accountEnabled": user["accountEnabled"], + "assignedLicenses": user["assignedLicenses"], + "lastSignInDateTime": parse(user["signInActivity"]["lastSignInDateTime"]).astimezone(settings.TZ) + if user["signInActivity"]["lastSignInDateTime"] + else None, + } + ) # Excludes accounts with no 'last signed in' value. - dormant_accounts = [i for i in accounts if i['lastSignInDateTime']] + dormant_accounts = [i for i in accounts if i["lastSignInDateTime"]] # Determine the list of AD accounts not having been signed into for the last number of `days`. - dormant_accounts = [i for i in dormant_accounts if i['lastSignInDateTime'] <= then] + dormant_accounts = [i for i in dormant_accounts if i["lastSignInDateTime"] <= then] if licensed: # Filter the list to accounts having an E5/F3 license assigned. inactive_licensed = [] for i in dormant_accounts: - for license in i['assignedLicenses']: - if license['skuId'] in [MS_PRODUCTS['MICROSOFT 365 E5'], MS_PRODUCTS['MICROSOFT 365 F3']]: + for license in i["assignedLicenses"]: + if license["skuId"] in [MS_PRODUCTS["MICROSOFT 365 E5"], MS_PRODUCTS["MICROSOFT 365 F3"]]: inactive_licensed.append(i) return inactive_licensed else: @@ -251,8 +288,7 @@ def ms_graph_dormant_accounts(days=90, licensed=False, token=None): def ms_graph_user(azure_guid, token=None): - """Query the Microsoft Graph REST API details of a signle Azure AD user account in our tenancy. - """ + """Query the Microsoft Graph REST API details of a signle Azure AD user account in our tenancy.""" if not token: token = ms_graph_client_token() if not token: # The call to the MS API occasionally fails and returns None. @@ -266,6 +302,20 @@ def ms_graph_user(azure_guid, token=None): return resp +def ms_graph_validate_password(password, token=None): + """Query the Microsoft Graph REST API (beta) if a given password string validates complexity requirements.""" + if not token: + token = ms_graph_client_token() + if not token: # The call to the MS API occasionally fails and returns None. + return None + headers = { + "Authorization": "Bearer {}".format(token["access_token"]), + } + url = "https://graph.microsoft.com/beta/users/validatePassword" + resp = requests.post(url, headers=headers, json={"password": password}) + return resp + + def ms_graph_sites(team_sites=True, token=None): """Query the Microsoft Graph REST API for details about SharePoint Sites. Reference: https://learn.microsoft.com/en-us/graph/api/site-list @@ -284,15 +334,15 @@ def ms_graph_sites(team_sites=True, token=None): j = resp.json() sites = [] - while '@odata.nextLink' in j: - sites = sites + j['value'] - resp = requests.get(j['@odata.nextLink'], headers=headers) + while "@odata.nextLink" in j: + sites = sites + j["value"] + resp = requests.get(j["@odata.nextLink"], headers=headers) resp.raise_for_status() j = resp.json() - sites = sites + j['value'] # Final page. + sites = sites + j["value"] # Final page. if team_sites: - sites = [site for site in sites if 'teams' in site['webUrl']] + sites = [site for site in sites if "teams" in site["webUrl"]] return sites @@ -338,8 +388,7 @@ def ms_graph_site_storage_usage(period_value="D7", token=None): def ms_graph_site_storage_summary(ds=None, token=None): - """Parses the current SharePoint site usage report, and returns a subset of storage usage data. - """ + """Parses the current SharePoint site usage report, and returns a subset of storage usage data.""" if not token: token = ms_graph_client_token() if not token: # The call to the MS API occasionally fails and returns None. @@ -392,7 +441,7 @@ def parse_windows_ts(s): 100-nanoseconds elapsed since January 1, 1601 (UTC). """ try: - match = re.search('(?P[0-9]+)', s) + match = re.search("(?P[0-9]+)", s) return datetime.fromtimestamp(int(match.group()) / 1000) # POSIX timestamp is in ms. except: return None @@ -403,16 +452,18 @@ def department_user_ascender_sync(users): object of CSV data that should be synced to Ascender. """ f = BytesIO() - writer = csv.writer(f, quoting=csv.QUOTE_ALL, encoding='utf-8') - writer.writerow(['EMPLOYEE_ID', 'EMAIL', 'ACTIVE', 'WORK_TELEPHONE', 'LICENCE_TYPE']) + writer = csv.writer(f, quoting=csv.QUOTE_ALL, encoding="utf-8") + writer.writerow(["EMPLOYEE_ID", "EMAIL", "ACTIVE", "WORK_TELEPHONE", "LICENCE_TYPE"]) for user in users: - writer.writerow([ - user.employee_id, - user.email.lower(), - user.active, - user.telephone, - user.get_licence(), - ]) + writer.writerow( + [ + user.employee_id, + user.email.lower(), + user.active, + user.telephone, + user.get_licence(), + ] + ) f.seek(0) return f diff --git a/poetry.lock b/poetry.lock index 58cd0244..0014e6be 100644 --- a/poetry.lock +++ b/poetry.lock @@ -515,13 +515,13 @@ typing_extensions = ">=3.10.0.0" [[package]] name = "django" -version = "4.2.15" +version = "4.2.16" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.15-py3-none-any.whl", hash = "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30"}, - {file = "Django-4.2.15.tar.gz", hash = "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a"}, + {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, + {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, ] [package.dependencies] @@ -918,89 +918,100 @@ wcwidth = "*" [[package]] name = "psycopg" -version = "3.2.1" +version = "3.2.2" description = "PostgreSQL database adapter for Python" optional = false python-versions = ">=3.8" files = [ - {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, - {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, + {file = "psycopg-3.2.2-py3-none-any.whl", hash = "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2"}, + {file = "psycopg-3.2.2.tar.gz", hash = "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0"}, ] [package.dependencies] -psycopg-binary = {version = "3.2.1", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +psycopg-binary = {version = "3.2.2", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} -typing-extensions = ">=4.4" +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] -binary = ["psycopg-binary (==3.2.1)"] -c = ["psycopg-c (==3.2.1)"] -dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.6)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +binary = ["psycopg-binary (==3.2.2)"] +c = ["psycopg-c (==3.2.2)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] pool = ["psycopg-pool"] -test = ["anyio (>=4.0)", "mypy (>=1.6)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] +test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] [[package]] name = "psycopg-binary" -version = "3.2.1" +version = "3.2.2" description = "PostgreSQL database adapter for Python -- C optimisation distribution" optional = false python-versions = ">=3.8" files = [ - {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:cad2de17804c4cfee8640ae2b279d616bb9e4734ac3c17c13db5e40982bd710d"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:592b27d6c46a40f9eeaaeea7c1fef6f3c60b02c634365eb649b2d880669f149f"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a997efbaadb5e1a294fb5760e2f5643d7b8e4e3fe6cb6f09e6d605fd28e0291"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1d2b6438fb83376f43ebb798bf0ad5e57bc56c03c9c29c85bc15405c8c0ac5a"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f087bd84bdcac78bf9f024ebdbfacd07fc0a23ec8191448a50679e2ac4a19e"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415c3b72ea32119163255c6504085f374e47ae7345f14bc3f0ef1f6e0976a879"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f092114f10f81fb6bae544a0ec027eb720e2d9c74a4fcdaa9dd3899873136935"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06a7aae34edfe179ddc04da005e083ff6c6b0020000399a2cbf0a7121a8a22ea"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b018631e5c80ce9bc210b71ea885932f9cca6db131e4df505653d7e3873a938"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8a509aeaac364fa965454e80cd110fe6d48ba2c80f56c9b8563423f0b5c3cfd"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:413977d18412ff83486eeb5875eb00b185a9391c57febac45b8993bf9c0ff489"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:62b1b7b07e00ee490afb39c0a47d8282a9c2822c7cfed9553a04b0058adf7e7f"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8afb07114ea9b924a4a0305ceb15354ccf0ef3c0e14d54b8dbeb03e50182dd7"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40bb515d042f6a345714ec0403df68ccf13f73b05e567837d80c886c7c9d3805"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6418712ba63cebb0c88c050b3997185b0ef54173b36568522d5634ac06153040"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:101472468d59c74bb8565fab603e032803fd533d16be4b2d13da1bab8deb32a3"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc314a47d44fe1a8069b075a64abffad347a3a1d8652fed1bab5d3baea37acb2"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc304a46be1e291031148d9d95c12451ffe783ff0cc72f18e2cc7ec43cdb8c68"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f9e13600647087df5928875559f0eb8f496f53e6278b7da9511b4b3d0aff960"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b140182830c76c74d17eba27df3755a46442ce8d4fb299e7f1cf2f74a87c877b"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:3c838806eeb99af39f934b7999e35f947a8e577997cc892c12b5053a97a9057f"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7066d3dca196ed0dc6172f9777b2d62e4f138705886be656cccff2d555234d60"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:28ada5f610468c57d8a4a055a8ea915d0085a43d794266c4f3b9d02f4288f4db"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e8213bf50af073b1aa8dc3cff123bfeedac86332a16c1b7274910bc88a847c7"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74d623261655a169bc84a9669890975c229f2fa6e19a7f2d10a77675dcf1a707"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42781ba94e8842ee98bca5a7d0c44cc9d067500fedca2d6a90fa3609b6d16b42"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6669091d09f8ba36e10ce678a6d9916e110446236a9b92346464a3565635e"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b09e8a576a2ac69d695032ee76f31e03b30781828b5dd6d18c6a009e5a3d1c35"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8f28ff0cb9f1defdc4a6f8c958bf6787274247e7dfeca811f6e2f56602695fb1"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c84fcac8a3a3479ac14673095cc4e1fdba2935499f72c436785ac679bec0d1a"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:950fd666ec9e9fe6a8eeb2b5a8f17301790e518953730ad44d715b59ffdbc67f"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:334046a937bb086c36e2c6889fe327f9f29bfc085d678f70fac0b0618949f674"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:1d6833f607f3fc7b22226a9e121235d3b84c0eda1d3caab174673ef698f63788"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d353e028b8f848b9784450fc2abf149d53a738d451eab3ee4c85703438128b9"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34e369891f77d0738e5d25727c307d06d5344948771e5379ea29c76c6d84555"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ab58213cc976a1666f66bc1cb2e602315cd753b7981a8e17237ac2a185bd4a1"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0104a72a17aa84b3b7dcab6c84826c595355bf54bb6ea6d284dcb06d99c6801"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:059cbd4e6da2337e17707178fe49464ed01de867dc86c677b30751755ec1dc51"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:73f9c9b984be9c322b5ec1515b12df1ee5896029f5e72d46160eb6517438659c"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:af0469c00f24c4bec18c3d2ede124bf62688d88d1b8a5f3c3edc2f61046fe0d7"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:463d55345f73ff391df8177a185ad57b552915ad33f5cc2b31b930500c068b22"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:302b86f92c0d76e99fe1b5c22c492ae519ce8b98b88d37ef74fda4c9e24c6b46"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0879b5d76b7d48678d31278242aaf951bc2d69ca4e4d7cef117e4bbf7bfefda9"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f99e59f8a5f4dcd9cbdec445f3d8ac950a492fc0e211032384d6992ed3c17eb7"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84837e99353d16c6980603b362d0f03302d4b06c71672a6651f38df8a482923d"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ce965caf618061817f66c0906f0452aef966c293ae0933d4fa5a16ea6eaf5bb"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78c2007caf3c90f08685c5378e3ceb142bafd5636be7495f7d86ec8a977eaeef"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7a84b5eb194a258116154b2a4ff2962ea60ea52de089508db23a51d3d6b1c7d1"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4a42b8f9ab39affcd5249b45cac763ac3cf12df962b67e23fd15a2ee2932afe5"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:788ffc43d7517c13e624c83e0e553b7b8823c9655e18296566d36a829bfb373f"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:21927f41c4d722ae8eb30d62a6ce732c398eac230509af5ba1749a337f8a63e2"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8eacbf58d4f8d7bc82e0a60476afa2622b5a58f639a3cc2710e3e37b72aff3cb"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:d07e62476ee8c54853b2b8cfdf3858a574218103b4cd213211f64326c7812437"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c22e615ee0ecfc6687bb8a39a4ed9d6bac030b5e72ac15e7324fd6e48979af71"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec29c7ec136263628e3f09a53e51d0a4b1ad765a6e45135707bfa848b39113f9"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:035753f80cbbf6aceca6386f53e139df70c7aca057b0592711047b5a8cfef8bb"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ee99336151ff7c30682f2ef9cb1174d235bc1471322faabba97f9db1398167"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a60674dff4a4194e88312b463fb84ac80924c2b9e25d0e0460f3176bf1af4a6b"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3c701507a49340de422d77a6ce95918a0019990bbf27daec35aa40050c6eadb6"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b3c5a04eaf8866e399315cff2e810260cce10b797437a9f49fd71b5f4b94d0a"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ad9c09de4c262f516ae6891d042a4325649b18efa39dd82bbe0f7bc95c37bfb"}, + {file = "psycopg_binary-3.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:bf1d3582185cb43ecc27403bee2f5405b7a45ccaab46c8508d9a9327341574fc"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:554d208757129d34fa47b7c890f9ef922f754e99c6b089cb3a209aa0fe282682"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:71dc3cc10d1fd7d26a3079d0a5b4a8e8ad0d7b89a702ceb7605a52e4395be122"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86f578d63f2e1fdf87c9adaed4ff23d7919bda8791cf1380fa4cf3a857ccb8b"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a4eb737682c02a602a12aa85a492608066f77793dab681b1c4e885fedc160b1"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e120a576e74e4e612c48f4b021e322e320ca102534d78a0ca4db2ffd058ae8d"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849d518e7d4c6186e1e48ea2ac2671912edf7e732fffe6f01dfed61cf0245de4"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ee2b19152bcec8f356f989c31768702be5f139b4d51094273c4a9ddc8c55380"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:00273dd011892e8216fcef76b42f775ddaa6348664a7fffae2a27c9557f45bfa"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4bcb489615d7e56d1de42937e6a0fc13f766505729afdb54c2947a52db295220"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06963f88916a177df95aaed27101af0989ba206654743b1a0e050b9d8e734686"}, + {file = "psycopg_binary-3.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed1ad836a0c21890c7f84e73c7ef1ed0950e0e4b0d8e49b609b6fd9c13f2ca21"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0dd314229885a81f9497875295d8788e651b78945627540f1e78ed71595e614a"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:989acbe2f552769cdb780346cea32d86e7c117044238d5172ac10b025fe47194"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566b1c530898590f0ac9d949cf94351c08d73c89f8800c74c0a63ffd89a383c8"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68d03efab7e2830a0df3aa4c29a708930e3f6b9fd98774ff9c4fd1f33deafecc"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e1f013bfb744023df23750fde51edcb606def8328473361db3c192c392c6060"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a06136aab55a2de7dd4e2555badae276846827cfb023e6ba1b22f7a7b88e3f1b"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:020c5154be144a1440cf87eae012b9004fb414ae4b9e7b1b9fb808fe39e96e83"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef341c556aeaa43a2729b07b04e20bfffdcf3d96c4a96e728ca94fe4ce632d8c"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66de2dd7d37bf66eb234ca9d907f5cd8caca43ff8d8a50dd5c15844d1cf0390c"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2eb6f8f410dbbb71b8c633f283b8588b63bee0a7321f00ab76e9c800c593f732"}, + {file = "psycopg_binary-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b45553c6b614d02e1486585980afdfd18f0000aac668e2e87c6e32da1adb051a"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:1ee891287c2da57e7fee31fbe2fbcdf57125768133d811b02e9523d5a052eb28"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5e95e4a8076ac7611e571623e1113fa84fd48c0459601969ffbf534d7aa236e7"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6269d79a3d7d76b6fcf0fafae8444da00e83777a6c68c43851351a571ad37155"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6dd5d21a298c3c53af20ced8da4ae4cd038c6fe88c80842a8888fa3660b2094"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cf64e41e238620f05aad862f06bc8424f8f320d8075f1499bd85a225d18bd57"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c482c3236ded54add31136a91d5223b233ec301f297fa2db79747404222dca6"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0718be095cefdad712542169d16fa58b3bd9200a3de1b0217ae761cdec1cf569"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fb303b03c243a9041e1873b596e246f7caaf01710b312fafa65b1db5cd77dd6f"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:705da5bc4364bd7529473225fca02b795653bc5bd824dbe43e1df0b1a40fe691"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:05406b96139912574571b1c56bb023839a9146cf4b57c4548f36251dd5909fa1"}, + {file = "psycopg_binary-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:7c357cf87e8d7612cfe781225be7669f35038a765d1b53ec9605f6c5aef9ee85"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:059aa5e8fa119de328b4cb02ee80775443763b25682a02dd7d026b8d4f565834"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05a50f94e1e4fa37a0074b09263b83b0aa038c3c72068a61f1ad61ea449ef9d5"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:951507b3d77a64c907afe893e01e09b41051fd7e27e9462f450fb8bb64bc22b0"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ec4986c4ac2503e865acd3943d179531c3bbfa5a1c8ee81fcfccb551dad645f"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b32b0e838841d5b109d32fc706b8bc64e50c161fee3f1371ccf696e5598bc49"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fdc74a83348477b28bea9e7b391c9fc189b480fe3cd0e46bb989514410b64d60"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9efe0ca78be4a573b4b81226904c711cfadc4783d64bfdf58a3394da7c1a1354"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:51f56ae2898acaa33623adad96ddc5acbb5e2f72f2fc020065c8be05c0e01dce"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:43b209be0424e8abece428a884cb711f504e3526dfbcb0bf51529907a55eda15"}, + {file = "psycopg_binary-3.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:d3c147eea9f3950a34133dc187e8d3534e54ff4a178a4ebd8993b2c97e123200"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:6c7b6a8d4e1b77cdb50192b61235b33fc2f1d28c67627fc93a1d43e9130dd479"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e234edc4bb746d8ac3daae8753ee38eaa7af2ee333a1d35ce6b02a02874aed18"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f12640ba92c538b3b64a199a918d3bb0cc0d7f7123c6ba93cb065e1a2d049f0"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8937dc548621b336b0d8383a3470fb7192b42a108c760a152282909867bf5b26"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4afbb97d64cd8078edec859b07859a18ef3de7261a3a873ba52f32548373ae92"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c432710bdf8ccfdd75b0bc9cdf1fd21ff394363e4daec099c667f3c5f1721e2b"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:366cc4e194f7feb4e3038d6775fd4b69835e7d923972aee5baec986de972abd6"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b286ed65a891928bd457ffa0cd5fec09b9b5208bfd096d087e45369f07c5cb85"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fee41c99312002e5d1f7462b1954aefed44c6efe5f021c3eac311640c16f6b7"}, + {file = "psycopg_binary-3.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:87cceaf07760a04023596f9ca1d4e929d38ae8d778161cb3e8d27a0f990dd264"}, ] [[package]] @@ -1488,4 +1499,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "64f3ef62c7be1ba9f60079c57dff431c88c1b1a9ecf54222418d101dc5721838" +content-hash = "b3495ae02f34d5f3954198c7166382fbc1b737841f631d52e29b031cb6ece5fc" diff --git a/pyproject.toml b/pyproject.toml index 582ff22c..4b34a94b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "itassets" -version = "2.4.29" +version = "2.4.30" description = "DBCA IT assets (both physical and knowledge-based) management system" authors = ["DBCA OIM "] license = "Apache-2.0" @@ -8,8 +8,8 @@ package-mode = false [tool.poetry.dependencies] python = "^3.12" -django = "4.2.15" -psycopg = {version = "3.2.1", extras = ["binary", "pool"]} +django = "4.2.16" +psycopg = { version = "3.2.2", extras = ["binary", "pool"] } dbca-utils = "2.0.2" django-extensions = "3.2.3" python-dotenv = "1.0.1" @@ -20,12 +20,12 @@ python-dateutil = "2.8.2" webtemplate-dbca = "1.7.1" mixer = "7.2.2" msal = "1.31.0" -whitenoise = {version = "6.7.0", extras = ["brotli"]} +whitenoise = { version = "6.7.0", extras = ["brotli"] } pysftp = "0.2.9" azure-storage-blob = "12.22.0" -django-storages = {version = "1.14.4", extras = ["azure"]} +django-storages = { version = "1.14.4", extras = ["azure"] } xlsxwriter = "3.2.0" -sentry-sdk = {version = "2.14.0", extras = ["django"]} +sentry-sdk = { version = "2.14.0", extras = ["django"] } redis = "5.0.8" [tool.poetry.group.dev.dependencies] @@ -33,9 +33,19 @@ ipython = "^8.27.0" ipdb = "^0.13.13" pre-commit = "^3.8.0" +# Reference: https://docs.astral.sh/ruff/configuration/ [tool.ruff] line-length = 120 -target-version = "py311" + +[tool.ruff.lint] +ignore = [ + "E501", # Line too long + "E722", # Bare except +] + +# Reference: https://www.djlint.com/docs/configuration/ +[tool.djlint] +profile = "django" [build-system] requires = ["poetry-core"]