From 5d5013363fc3a94c2dd611b019bd9a03a31ae92c Mon Sep 17 00:00:00 2001 From: Elahd Bar-Shai <466460+elahd@users.noreply.github.com> Date: Fri, 28 Jul 2023 17:24:19 +0000 Subject: [PATCH] Fix OTP issues and upgrade to Python 11. --- .devcontainer.json | 4 +- .pre-commit-config.yaml | 2 +- pyalarmdotcomajax/__init__.py | 71 +++++++++++++++++++++------ pyalarmdotcomajax/devices/__init__.py | 2 +- pyproject.toml | 6 +-- 5 files changed, 63 insertions(+), 22 deletions(-) diff --git a/.devcontainer.json b/.devcontainer.json index 1bb5f5f..4bdaa05 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,5 +1,5 @@ { - "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10-bullseye", + "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye", "name": "pyalarmdotcomajax", "postCreateCommand": "scripts/setup.sh", "customizations": { @@ -44,4 +44,4 @@ "features": { "github-cli": "latest" } -} \ No newline at end of file +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f7e3c4..b03799e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.10 + python: python3.11 repos: - repo: https://github.com/charliermarsh/ruff-pre-commit diff --git a/pyalarmdotcomajax/__init__.py b/pyalarmdotcomajax/__init__.py index 9fee7b5..09e4ac4 100644 --- a/pyalarmdotcomajax/__init__.py +++ b/pyalarmdotcomajax/__init__.py @@ -48,7 +48,7 @@ ExtendedProperties, ) -__version__ = "0.4.14" +__version__ = "0.4.15" log = logging.getLogger(__name__) @@ -81,10 +81,10 @@ class OtpType(Enum): # https://www.alarm.com/web/system/assets/customer-ember/enums/TwoFactorAuthenticationType.js - DISABLED = 0 - APP = 1 - SMS = 2 - EMAIL = 4 + disabled = 0 + app = 1 + sms = 2 + email = 4 class AlarmController: @@ -107,7 +107,7 @@ class AlarmController: "{}web/api/engines/twoFactorAuthentication/twoFactorAuthentications/{}" ) LOGIN_2FA_TRUST_URL_TEMPLATE = "{}web/api/engines/twoFactorAuthentication/twoFactorAuthentications/{}/trustTwoFactorDevice" - LOGIN_2FA_REQUEST_OTP_SMS_URL_TEMPLATE = "{}web/api/engines/twoFactorAuthentication/twoFactorAuthentications/{}/sendTwoFactorAuthenticationCode" + LOGIN_2FA_REQUEST_OTP_SMS_URL_TEMPLATE = "{}web/api/engines/twoFactorAuthentication/twoFactorAuthentications/{}/sendTwoFactorAuthenticationCodeViaSms" LOGIN_2FA_REQUEST_OTP_EMAIL_URL_TEMPLATE = "{}web/api/engines/twoFactorAuthentication/twoFactorAuthentications/{}/sendTwoFactorAuthenticationCodeViaEmail" VIEWSTATE_FIELD = "__VIEWSTATE" @@ -224,8 +224,8 @@ async def async_login(self, request_otp: bool = True) -> AuthResult: log.debug("Two factor authentication code or cookie required.") if request_otp and self._two_factor_method in [ - OtpType.SMS, - OtpType.EMAIL, + OtpType.sms, + OtpType.email, ]: await self.async_request_otp() @@ -248,7 +248,7 @@ async def async_request_otp(self) -> str | None: request_url = ( self.LOGIN_2FA_REQUEST_OTP_EMAIL_URL_TEMPLATE - if self._two_factor_method == OtpType.EMAIL + if self._two_factor_method == OtpType.email else self.LOGIN_2FA_REQUEST_OTP_SMS_URL_TEMPLATE ) @@ -863,18 +863,59 @@ async def _async_requires_2fa(self) -> bool | None: self._update_antiforgery_token(resp) json_rsp = await resp.json() + log.debug(json_rsp) + if isinstance( factor_id := json_rsp.get("data", {}).get("id"), int ): self._factor_type_id = factor_id - self._two_factor_method = OtpType( - json_rsp.get("data", {}) - .get("attributes", {}) - .get("twoFactorType") + + if not ( + attribs := json_rsp.get("data", {}).get("attributes") + ): + raise UnexpectedDataStructure( + "Could not find expected data in two-factor" + " authentication details." + ) + + if attribs.get("showSuggestedSetup") is True: + raise NagScreen + + enabled_otp_types_bitmask = attribs.get( + "enabledTwoFactorTypes" ) - log.debug( - "Requires 2FA. Using method %s", self._two_factor_method + + enabled_2fa_methods = [ + otp_type + for otp_type in OtpType + if bool(enabled_otp_types_bitmask & otp_type.value) + ] + + if ( + (OtpType.disabled in enabled_2fa_methods) + or (attribs.get("isCurrentDeviceTrusted") is True) + or not enabled_otp_types_bitmask + or not enabled_2fa_methods + ): + # 2FA is disabled, we can skip 2FA altogether. + return False + + log.info( + "Requires two-factor authentication. Enabled methods" + f" are {enabled_2fa_methods}" ) + + # We have proper 2FA type selection in the 5.x releases. Since this 4.x release is a hotfix, we're going to be lazy here and will select an OTP method for the user. + supported_types = [ + int(otp_type.value) + for otp_type in OtpType + if bool(enabled_otp_types_bitmask & otp_type.value) + ] + + self._two_factor_method = OtpType(min(supported_types)) + + log.debug("Using method %s", self._two_factor_method) + return True log.debug("Does not require 2FA.") diff --git a/pyalarmdotcomajax/devices/__init__.py b/pyalarmdotcomajax/devices/__init__.py index 95ed70d..e02f6e3 100644 --- a/pyalarmdotcomajax/devices/__init__.py +++ b/pyalarmdotcomajax/devices/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations import asyncio +import logging from collections.abc import Callable from dataclasses import dataclass from enum import Enum -import logging from typing import Any, Protocol, TypedDict, final import aiohttp diff --git a/pyproject.toml b/pyproject.toml index 2fa776c..7403f49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,12 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 -target-version = ["py310"] +target-version = ["py311"] exclude = 'generated' preview = "True" [tool.mypy] -python_version = "3.10" +python_version = "3.11" show_error_codes = true # follow_imports = "silent" ignore_missing_imports = true @@ -40,7 +40,7 @@ norecursedirs = [".git", "testing_config"] asyncio_mode = "auto" [tool.ruff] -target-version = "py310" +target-version = "py311" exclude = ["examples", "tests", "pylint"] select = [ "B007", # Loop control variable {name} not used within loop body