mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
fix(m365): correct premium license insight parsing and semantics
- Parse correct Microsoft Graph keys (entitledP1/P2LicenseCount, p1FeatureUtilizations.conditionalAccess[GuestUsers].userCount) - Sum P1 + P2 entitlements and count regular plus guest CA users - Return MANUAL when license insight is unavailable instead of FAIL - Downgrade missingLicense log to warning with explanatory message - Add parser unit tests that mock the raw Graph response
This commit is contained in:
+5
-5
@@ -9,8 +9,8 @@
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "Verifies that the number of entitled Azure AD Premium **P1 licenses** is sufficient to cover all users actively utilizing **Conditional Access**. Unlicensed Conditional Access usage may violate Microsoft licensing terms and indicates gaps in license governance.",
|
||||
"Risk": "When more users consume Conditional Access than the organization has P1 licenses for, it creates a **licensing compliance gap**. Microsoft may flag the tenant during audits, leading to retroactive charges or service restrictions. Additionally, unlicensed users may lose Conditional Access protection if Microsoft enforces entitlements.",
|
||||
"Description": "Verifies that the total number of entitled Microsoft Entra ID Premium licenses (**P1 plus P2**, since P2 includes P1) is sufficient to cover all users actively utilizing **Conditional Access** (regular plus guest users). Unlicensed Conditional Access usage may violate Microsoft licensing terms and indicates gaps in license governance.",
|
||||
"Risk": "When more users consume Conditional Access than the organization has premium licenses for, it creates a **licensing compliance gap**. Microsoft may flag the tenant during audits, leading to retroactive charges or service restrictions. Additionally, unlicensed users may lose Conditional Access protection if Microsoft enforces entitlements.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview",
|
||||
@@ -20,11 +20,11 @@
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to the **Microsoft Entra admin center** (https://entra.microsoft.com).\n2. Go to **Identity** > **Billing** > **Licenses** > **Licensed features**.\n3. Review the **License utilization** report for P1 features.\n4. Purchase additional P1 licenses or remove Conditional Access policy assignments for unlicensed users to bring entitlements in line with utilization.",
|
||||
"Other": "1. Navigate to the **Microsoft Entra admin center** (https://entra.microsoft.com).\n2. Go to **Identity** > **Billing** > **Licenses** > **Licensed features**.\n3. Review the **License utilization** report for P1 and P2 features.\n4. Purchase additional P1 or P2 licenses, or remove Conditional Access policy assignments for unlicensed users to bring entitlements in line with utilization.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Review the Azure AD Premium license utilization report and ensure that P1 license entitlements match or exceed the number of users utilizing Conditional Access. Purchase additional licenses or adjust policy scope as needed.",
|
||||
"Text": "Review the Microsoft Entra ID Premium license utilization report and ensure that combined P1 and P2 license entitlements match or exceed the number of users utilizing Conditional Access (regular and guest). Purchase additional licenses or adjust policy scope as needed.",
|
||||
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_p1_license_utilization"
|
||||
}
|
||||
},
|
||||
@@ -34,5 +34,5 @@
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "This check uses the Microsoft Graph beta API endpoint reports/azureADPremiumLicenseInsight. The Reports.Read.All permission is required."
|
||||
"Notes": "Uses Microsoft Graph beta reports/azureADPremiumLicenseInsight. Requires Reports.Read.All. Returns MANUAL when the tenant has no Microsoft Entra ID P1 or P2 license (Graph returns 403 missingLicense). The check sums P1 + P2 entitlements against regular plus guest Conditional Access users."
|
||||
}
|
||||
|
||||
+23
-17
@@ -3,20 +3,24 @@ from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
|
||||
|
||||
class entra_conditional_access_policy_p1_license_utilization(Check):
|
||||
"""Check if P1 license entitlements cover Conditional Access utilization.
|
||||
"""Check if Premium license entitlements cover Conditional Access utilization.
|
||||
|
||||
Validates that the number of entitled Azure AD Premium P1 licenses
|
||||
is sufficient to cover all users actively utilizing Conditional Access.
|
||||
Compares the total entitled Microsoft Entra ID Premium licenses (P1 + P2,
|
||||
since P2 includes P1) against the number of users actively utilising
|
||||
Conditional Access (regular plus guest users).
|
||||
|
||||
- PASS: P1 license entitlements cover all Conditional Access users.
|
||||
- FAIL: More users utilize Conditional Access than entitled P1 licenses.
|
||||
- PASS: total premium license entitlements cover all Conditional Access users.
|
||||
- FAIL: more users utilize Conditional Access than entitled premium licenses.
|
||||
- MANUAL: license insight unavailable, typically because the tenant has no
|
||||
P1/P2 license (Microsoft Graph returns 403 ``missingLicense``) or the
|
||||
``Reports.Read.All`` permission was not granted.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""Execute the P1 license utilization check.
|
||||
"""Execute the premium license utilization check.
|
||||
|
||||
Returns:
|
||||
A list of reports containing the result of the check.
|
||||
A list with a single report describing the licensing coverage.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
@@ -24,29 +28,31 @@ class entra_conditional_access_policy_p1_license_utilization(Check):
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=insight if insight else {},
|
||||
resource=insight or {},
|
||||
resource_name="Premium License Insight",
|
||||
resource_id="azureADPremiumLicenseInsight",
|
||||
)
|
||||
|
||||
if insight is None:
|
||||
report.status = "FAIL"
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
"Could not retrieve Azure AD Premium license insight data. "
|
||||
"Verify that the tenant has a P1 or P2 license and that "
|
||||
"the required permissions are granted."
|
||||
"Could not retrieve Azure AD Premium license insight. "
|
||||
"Verify the tenant has at least one Microsoft Entra ID P1 or P2 "
|
||||
"license and that Reports.Read.All permission is granted."
|
||||
)
|
||||
elif insight.p1_license_count >= insight.conditional_access_users_count:
|
||||
elif insight.total_license_count >= insight.conditional_access_users_count:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"P1 license entitlements ({insight.p1_license_count}) cover "
|
||||
f"all Conditional Access users ({insight.conditional_access_users_count})."
|
||||
f"Premium license entitlements ({insight.total_license_count}) "
|
||||
f"cover all Conditional Access users "
|
||||
f"({insight.conditional_access_users_count})."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"P1 license entitlements ({insight.p1_license_count}) do not cover "
|
||||
f"all Conditional Access users ({insight.conditional_access_users_count})."
|
||||
f"Premium license entitlements ({insight.total_license_count}) "
|
||||
f"do not cover all Conditional Access users "
|
||||
f"({insight.conditional_access_users_count})."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
@@ -1027,9 +1027,14 @@ OAuthAppInfo
|
||||
async def _get_premium_license_insight(self) -> Optional["PremiumLicenseInsight"]:
|
||||
"""Retrieve Azure AD Premium license insight from the Microsoft Graph beta API.
|
||||
|
||||
Fetches the premium license utilization report which includes the total
|
||||
number of entitled P1 licenses and the number of users actively utilizing
|
||||
Conditional Access (a P1 feature).
|
||||
Fetches the premium license utilization report which exposes entitled
|
||||
P1/P2 license counts and per-feature utilization (Conditional Access
|
||||
regular and guest users).
|
||||
|
||||
Tenants without any P1/P2 license receive HTTP 403 with the
|
||||
``missingLicense`` error code; this is surfaced as ``None`` so the
|
||||
consuming check can distinguish "not applicable" from a real
|
||||
coverage gap.
|
||||
|
||||
Returns:
|
||||
Optional[PremiumLicenseInsight]: The premium license insight data,
|
||||
@@ -1050,14 +1055,36 @@ OAuthAppInfo
|
||||
)
|
||||
if response:
|
||||
data = json.loads(response)
|
||||
p1 = int(data.get("entitledP1LicenseCount", 0) or 0)
|
||||
p2 = int(data.get("entitledP2LicenseCount", 0) or 0)
|
||||
p1_features = data.get("p1FeatureUtilizations") or {}
|
||||
ca_users = int(
|
||||
(p1_features.get("conditionalAccess") or {}).get("userCount", 0)
|
||||
or 0
|
||||
)
|
||||
ca_guest_users = int(
|
||||
(p1_features.get("conditionalAccessGuestUsers") or {}).get(
|
||||
"userCount", 0
|
||||
)
|
||||
or 0
|
||||
)
|
||||
total_licenses = int(
|
||||
data.get("entitledTotalLicenseCount", p1 + p2) or (p1 + p2)
|
||||
)
|
||||
return PremiumLicenseInsight(
|
||||
p1_license_count=data.get("p1LicenseCount", 0),
|
||||
conditional_access_users_count=data.get(
|
||||
"conditionalAccessUsersCount", 0
|
||||
),
|
||||
entitled_p1_license_count=p1,
|
||||
entitled_p2_license_count=p2,
|
||||
conditional_access_user_count=ca_users,
|
||||
conditional_access_guest_user_count=ca_guest_users,
|
||||
total_license_count=total_licenses,
|
||||
conditional_access_users_count=ca_users + ca_guest_users,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
# 403 missingLicense is expected for tenants without P1/P2.
|
||||
logger.warning(
|
||||
f"Entra - Could not retrieve Azure AD Premium license insight. "
|
||||
f"Requires Reports.Read.All and a tenant with at least one "
|
||||
f"Microsoft Entra ID P1 or P2 license. "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
@@ -1529,13 +1556,23 @@ class OAuthApp(BaseModel):
|
||||
class PremiumLicenseInsight(BaseModel):
|
||||
"""Model representing Azure AD Premium license utilization insight.
|
||||
|
||||
Contains the total number of entitled P1 licenses and the number
|
||||
of users actively utilizing Conditional Access, a P1-gated feature.
|
||||
Mirrors the four counters returned by Microsoft Graph beta
|
||||
``reports/azureADPremiumLicenseInsight`` plus two derived totals
|
||||
consumed by the licence-utilization check (P2 entitlements include P1,
|
||||
and guest Conditional Access users still consume premium licences).
|
||||
|
||||
Attributes:
|
||||
p1_license_count: Total number of entitled P1 (or P2) licenses.
|
||||
conditional_access_users_count: Number of users utilizing Conditional Access.
|
||||
entitled_p1_license_count: Tenant-wide entitled Microsoft Entra ID P1 licenses.
|
||||
entitled_p2_license_count: Tenant-wide entitled Microsoft Entra ID P2 licenses.
|
||||
conditional_access_user_count: Users actively utilising Conditional Access.
|
||||
conditional_access_guest_user_count: Guest users actively utilising Conditional Access.
|
||||
total_license_count: Total premium licenses entitled (P1 + P2).
|
||||
conditional_access_users_count: Total Conditional Access utilization (regular + guest).
|
||||
"""
|
||||
|
||||
p1_license_count: int = 0
|
||||
entitled_p1_license_count: int = 0
|
||||
entitled_p2_license_count: int = 0
|
||||
conditional_access_user_count: int = 0
|
||||
conditional_access_guest_user_count: int = 0
|
||||
total_license_count: int = 0
|
||||
conditional_access_users_count: int = 0
|
||||
|
||||
+84
-33
@@ -7,7 +7,7 @@ CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_acc
|
||||
|
||||
class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
def test_no_premium_license_insight(self):
|
||||
"""FAIL when premium license insight data is unavailable (None)."""
|
||||
"""MANUAL when premium license insight data is unavailable (None)."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
@@ -35,15 +35,17 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "Could not retrieve" in result[0].status_extended
|
||||
assert "P1 or P2" in result[0].status_extended
|
||||
assert "Reports.Read.All" in result[0].status_extended
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_licenses_cover_utilization(self):
|
||||
"""PASS when P1 license count >= conditional access users count."""
|
||||
def test_p1_only_covers_utilization(self):
|
||||
"""PASS when P1 entitlements alone cover all CA users (regular + guest)."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
@@ -69,7 +71,60 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
p1_license_count=100,
|
||||
entitled_p1_license_count=100,
|
||||
entitled_p2_license_count=0,
|
||||
conditional_access_user_count=60,
|
||||
conditional_access_guest_user_count=10,
|
||||
total_license_count=100,
|
||||
conditional_access_users_count=70,
|
||||
)
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Premium license entitlements (100) cover all Conditional Access users (70)."
|
||||
)
|
||||
assert result[0].resource == entra_client.premium_license_insight.dict()
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_p1_plus_p2_covers_utilization(self):
|
||||
"""PASS when combined P1 + P2 entitlements cover all CA users."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_p1_license_utilization.entra_conditional_access_policy_p1_license_utilization import (
|
||||
entra_conditional_access_policy_p1_license_utilization,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
PremiumLicenseInsight,
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
entitled_p1_license_count=50,
|
||||
entitled_p2_license_count=50,
|
||||
conditional_access_user_count=60,
|
||||
conditional_access_guest_user_count=20,
|
||||
total_license_count=100,
|
||||
conditional_access_users_count=80,
|
||||
)
|
||||
|
||||
@@ -80,18 +135,15 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "P1 license entitlements (100) cover all Conditional Access users (80)."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.premium_license_insight.dict()
|
||||
== "Premium license entitlements (100) cover all Conditional Access users (80)."
|
||||
)
|
||||
assert result[0].resource == entra_client.premium_license_insight.dict()
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_licenses_equal_utilization(self):
|
||||
"""PASS when P1 license count exactly equals conditional access users count."""
|
||||
def test_p2_only_covers_utilization(self):
|
||||
"""PASS when only P2 licenses are entitled (P2 includes P1)."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
@@ -117,8 +169,12 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
p1_license_count=50,
|
||||
conditional_access_users_count=50,
|
||||
entitled_p1_license_count=0,
|
||||
entitled_p2_license_count=150,
|
||||
conditional_access_user_count=100,
|
||||
conditional_access_guest_user_count=20,
|
||||
total_license_count=150,
|
||||
conditional_access_users_count=120,
|
||||
)
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
@@ -128,18 +184,15 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "P1 license entitlements (50) cover all Conditional Access users (50)."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.premium_license_insight.dict()
|
||||
== "Premium license entitlements (150) cover all Conditional Access users (120)."
|
||||
)
|
||||
assert result[0].resource == entra_client.premium_license_insight.dict()
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_licenses_insufficient(self):
|
||||
"""FAIL when P1 license count < conditional access users count."""
|
||||
def test_licenses_insufficient_with_guests(self):
|
||||
"""FAIL when guest CA users push utilization above the entitled count."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
@@ -165,8 +218,12 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
p1_license_count=30,
|
||||
conditional_access_users_count=80,
|
||||
entitled_p1_license_count=30,
|
||||
entitled_p2_license_count=0,
|
||||
conditional_access_user_count=25,
|
||||
conditional_access_guest_user_count=10,
|
||||
total_license_count=30,
|
||||
conditional_access_users_count=35,
|
||||
)
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
@@ -176,12 +233,9 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "P1 license entitlements (30) do not cover all Conditional Access users (80)."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.premium_license_insight.dict()
|
||||
== "Premium license entitlements (30) do not cover all Conditional Access users (35)."
|
||||
)
|
||||
assert result[0].resource == entra_client.premium_license_insight.dict()
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
@@ -212,10 +266,7 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
PremiumLicenseInsight,
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
p1_license_count=0,
|
||||
conditional_access_users_count=0,
|
||||
)
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight()
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
result = check.execute()
|
||||
@@ -224,7 +275,7 @@ class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "P1 license entitlements (0) cover all Conditional Access users (0)."
|
||||
== "Premium license entitlements (0) cover all Conditional Access users (0)."
|
||||
)
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -24,6 +25,7 @@ from prowler.providers.m365.services.entra.entra_service import (
|
||||
InvitationsFrom,
|
||||
Organization,
|
||||
PersistentBrowser,
|
||||
PremiumLicenseInsight,
|
||||
SessionControls,
|
||||
SignInFrequency,
|
||||
SignInFrequencyInterval,
|
||||
@@ -593,3 +595,93 @@ class Test_Entra_Service:
|
||||
registration_builder.get.assert_awaited()
|
||||
registration_builder.with_url.assert_called_once_with("next-link")
|
||||
registration_builder_next.get.assert_awaited()
|
||||
|
||||
|
||||
class Test_Entra_get_premium_license_insight:
|
||||
"""Validate the raw JSON parsing of the Microsoft Graph beta endpoint
|
||||
`reports/azureADPremiumLicenseInsight`. These tests bypass full Entra
|
||||
initialisation and exercise the parser directly via a stand-alone
|
||||
instance with a mocked `request_adapter`.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _build_entra_with_adapter(adapter):
|
||||
entra = Entra.__new__(Entra)
|
||||
entra.client = MagicMock(request_adapter=adapter)
|
||||
return entra
|
||||
|
||||
def test_parses_realistic_response(self):
|
||||
"""Parses the canonical Microsoft Graph response and exposes derived totals."""
|
||||
payload = {
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"entitledP1LicenseCount": 100,
|
||||
"entitledP2LicenseCount": 50,
|
||||
"entitledTotalLicenseCount": 150,
|
||||
"p1FeatureUtilizations": {
|
||||
"conditionalAccess": {"userCount": 85},
|
||||
"conditionalAccessGuestUsers": {"userCount": 12},
|
||||
},
|
||||
"p2FeatureUtilizations": {
|
||||
"riskBasedConditionalAccess": {"userCount": 30},
|
||||
"riskBasedConditionalAccessGuestUsers": {"userCount": 5},
|
||||
},
|
||||
}
|
||||
adapter = MagicMock()
|
||||
adapter.send_primitive_async = AsyncMock(
|
||||
return_value=json.dumps(payload).encode("utf-8")
|
||||
)
|
||||
entra = self._build_entra_with_adapter(adapter)
|
||||
|
||||
insight = asyncio.run(entra._get_premium_license_insight())
|
||||
|
||||
assert isinstance(insight, PremiumLicenseInsight)
|
||||
assert insight.entitled_p1_license_count == 100
|
||||
assert insight.entitled_p2_license_count == 50
|
||||
assert insight.total_license_count == 150
|
||||
assert insight.conditional_access_user_count == 85
|
||||
assert insight.conditional_access_guest_user_count == 12
|
||||
assert insight.conditional_access_users_count == 97
|
||||
|
||||
def test_returns_none_on_403(self):
|
||||
"""Returns None when the API raises (e.g. 403 missingLicense)."""
|
||||
adapter = MagicMock()
|
||||
adapter.send_primitive_async = AsyncMock(
|
||||
side_effect=Exception("403 Forbidden: missingLicense")
|
||||
)
|
||||
entra = self._build_entra_with_adapter(adapter)
|
||||
|
||||
insight = asyncio.run(entra._get_premium_license_insight())
|
||||
|
||||
assert insight is None
|
||||
|
||||
def test_returns_none_on_empty_response(self):
|
||||
"""Returns None when the API returns empty bytes."""
|
||||
adapter = MagicMock()
|
||||
adapter.send_primitive_async = AsyncMock(return_value=b"")
|
||||
entra = self._build_entra_with_adapter(adapter)
|
||||
|
||||
insight = asyncio.run(entra._get_premium_license_insight())
|
||||
|
||||
assert insight is None
|
||||
|
||||
def test_handles_missing_feature_utilizations(self):
|
||||
"""Falls back to P1+P2 sum when entitledTotalLicenseCount is missing
|
||||
and tolerates a null p1FeatureUtilizations object."""
|
||||
payload = {
|
||||
"entitledP1LicenseCount": 10,
|
||||
"entitledP2LicenseCount": 0,
|
||||
"p1FeatureUtilizations": None,
|
||||
}
|
||||
adapter = MagicMock()
|
||||
adapter.send_primitive_async = AsyncMock(
|
||||
return_value=json.dumps(payload).encode("utf-8")
|
||||
)
|
||||
entra = self._build_entra_with_adapter(adapter)
|
||||
|
||||
insight = asyncio.run(entra._get_premium_license_insight())
|
||||
|
||||
assert insight is not None
|
||||
assert insight.total_license_count == 10
|
||||
assert insight.conditional_access_users_count == 0
|
||||
assert insight.conditional_access_user_count == 0
|
||||
assert insight.conditional_access_guest_user_count == 0
|
||||
|
||||
Reference in New Issue
Block a user