mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
feat(connectors): OneDrive checks follow the design's rows, copy and states
OneDrive follows the design:
- Names and order: the 9 checks are the design's 8 rows. The drive count
shares the "List users" row, which now validates the credential binding.
Dependencies follow the design's run order; "Find a OneDrive" also runs
after "Find selected users", so no user found stops it.
- Every failure gives a short line and a description that names the
grant the app needs. Passing checks give what they found ("Up to {n}
OneDrives", "Opened the OneDrive of {user}", "Read members of
{group}"...).
- Find selected users is a partial find: some missing is INCOMPLETE and
the drive checks walk the found users; none found is FAILED. An entry
that is not an email and a list of blanks have their rows.
- Checks that passed without testing anything are now INCOMPLETE: an empty
OneDrive for the permission read, and groups that all hide their
members. A group that went away is INCOMPLETE, and a user list that does
not end is INCOMPLETE, not an unexpected error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFMazQbuG8ui3jxBxm47sC
This commit is contained in:
3 files changed
+684
-170
No files matched your search
@@ -1,5 +1,18 @@
|
||||
"""Capability checks for the OneDrive connector.
|
||||
|
||||
Each check makes small probes (one user, one drive, one delta page, one
|
||||
group) with the gateway operations indexing and the syncs call. The drive
|
||||
checks walk candidate users: the first configured users Graph finds, else
|
||||
one user from each of the first pages of the tenant's user listing.
|
||||
|
||||
"Find selected users" is a partial find: when some are missing it is
|
||||
INCOMPLETE, and the drive checks after it walk the users it found.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from typing import TypeVar
|
||||
from typing import NoReturn, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from onyx.connectors.capability_checks.findings import bounded_count
|
||||
from onyx.connectors.capability_checks.form_state import FormState
|
||||
@@ -16,11 +29,15 @@ from onyx.connectors.capability_checks.models import (
|
||||
)
|
||||
from onyx.connectors.exceptions import (
|
||||
ConnectorValidationError,
|
||||
InconclusiveValidationError,
|
||||
UnexpectedValidationError,
|
||||
)
|
||||
from onyx.connectors.microsoft_utils.check_text import (
|
||||
ENTRA_GROUPS_DENIED_ROW,
|
||||
MicrosoftAccountLabels,
|
||||
MicrosoftRowText,
|
||||
MicrosoftSignIn,
|
||||
grant_line,
|
||||
)
|
||||
from onyx.connectors.microsoft_utils.drive_delta import (
|
||||
DRIVE_DELTA_SELECT_FIELDS,
|
||||
@@ -51,42 +68,84 @@ _CANDIDATE_PAGES = 20
|
||||
# A bound on the pages the drive count reads. Pages hold up to 999 users, so
|
||||
# the count reaches its cap long before this unless Graph sends empty pages.
|
||||
_COUNT_PAGES = 20
|
||||
# The longest row line the card shows.
|
||||
_MAX_LINE = 40
|
||||
_LOCKED_STATUS = 423
|
||||
|
||||
T = TypeVar("T")
|
||||
_HIDDEN_MEMBERSHIP_VISIBILITY = "HiddenMembership"
|
||||
_ACCOUNT_LABELS = MicrosoftAccountLabels.with_prefix(
|
||||
"OneDrive", private_key="OneDrive Certificate"
|
||||
)
|
||||
_USERS_GRANT_LINE = "Grant `User.Read.All` and admin-consent it."
|
||||
_RETRY_LINE = "Re-run the checks. If it repeats, send the error details to support."
|
||||
|
||||
_SIGN_IN_CHECK_ID = "onedrive_token_auth"
|
||||
# Every probe signs in first, so a failed sign-in is one row.
|
||||
_AFTER_SIGN_IN: tuple[str, ...] = (_SIGN_IN_CHECK_ID,)
|
||||
_USERS_CHECK_ID = "onedrive_users"
|
||||
_CONFIGURED_USERS_CHECK_ID = "onedrive_configured_users"
|
||||
_DRIVE_CHECK_ID = "onedrive_drive"
|
||||
_GROUPS_CHECK_ID = "onedrive_groups"
|
||||
# check_id to the checks it runs after, in run order.
|
||||
_DEPENDS_ON: dict[str, tuple[str, ...]] = {
|
||||
_USERS_CHECK_ID: (_SIGN_IN_CHECK_ID,),
|
||||
"onedrive_drive_count": (_SIGN_IN_CHECK_ID,),
|
||||
_CONFIGURED_USERS_CHECK_ID: (_USERS_CHECK_ID,),
|
||||
# The selected users too: with none found there is no OneDrive to open.
|
||||
_DRIVE_CHECK_ID: (_USERS_CHECK_ID, _CONFIGURED_USERS_CHECK_ID),
|
||||
"onedrive_delta": (_DRIVE_CHECK_ID,),
|
||||
"onedrive_item_permissions": (_DRIVE_CHECK_ID,),
|
||||
_GROUPS_CHECK_ID: (_SIGN_IN_CHECK_ID,),
|
||||
"onedrive_group_members": (_GROUPS_CHECK_ID,),
|
||||
}
|
||||
|
||||
_EMPTY_DRIVE_ADVISORY = CheckAdvisory(
|
||||
message=(
|
||||
"The check found no item in the OneDrives it sampled. "
|
||||
"It did not read item permissions. "
|
||||
f"The check samples the drives of the first {_CANDIDATE_PAGES} configured "
|
||||
f"users, or of the users on the first {_CANDIDATE_PAGES} pages of the "
|
||||
"user list. Add a file to one of those drives and run the check again "
|
||||
"to verify this access."
|
||||
)
|
||||
|
||||
def _line(prefix: str, name: str, suffix: str = "") -> str:
|
||||
"""``prefix``, ``name`` and ``suffix``, with the name cut to fit the row."""
|
||||
room: int = _MAX_LINE - len(prefix) - len(suffix)
|
||||
cut: str = name if len(name) <= room else name[: room - 1] + "…"
|
||||
return f"{prefix}{cut}{suffix}"
|
||||
|
||||
|
||||
def _row(summary: str, description: str) -> MicrosoftRowText:
|
||||
return MicrosoftRowText(summary=summary, description=description)
|
||||
|
||||
|
||||
_USERS_DENIED_ROW = _row(
|
||||
"No permission to read user directory",
|
||||
f"Every-user mode and user lookups would not work.\n{_USERS_GRANT_LINE}",
|
||||
)
|
||||
_NO_VISIBLE_GROUP_ADVISORY = CheckAdvisory(
|
||||
message=(
|
||||
"The check found no Entra group with visible membership "
|
||||
f"in the first {_CANDIDATE_PAGES} groups. "
|
||||
"It did not read transitive group members. "
|
||||
"The check skips groups with hidden membership."
|
||||
)
|
||||
_USER_LIST_UNENDING_ROW = _row(
|
||||
"User list did not end",
|
||||
"Microsoft kept returning short pages.\nRe-run the checks later.",
|
||||
)
|
||||
_BLANK_USERS_ROW = _row(
|
||||
"Users holds only blank entries",
|
||||
"Specific scope is chosen but no user address is filled in.\nAdd at least one "
|
||||
"address to Users, or choose General.",
|
||||
)
|
||||
_DRIVES_DENIED_ROW = _row(
|
||||
"No permission to read sampled OneDrives",
|
||||
f"The app is refused on every OneDrive it tried.\n{grant_line('Sites.Read.All')}",
|
||||
)
|
||||
_GROUP_MEMBERS_DENIED_ROW = _row(
|
||||
"No permission to read group members",
|
||||
"The app cannot read who is in an Entra group.\n"
|
||||
+ grant_line("GroupMember.Read.All"),
|
||||
)
|
||||
_NO_DRIVE_ROW = _row(
|
||||
"No OneDrives found to test",
|
||||
"None of the sampled users has a OneDrive the app could open.\nSelect users "
|
||||
"who have a OneDrive in Users, then run the checks again.",
|
||||
)
|
||||
|
||||
|
||||
class _NoCandidateFound(ConnectorValidationError):
|
||||
class _NoCandidateFound(InconclusiveValidationError):
|
||||
pass
|
||||
|
||||
|
||||
def _gateway(context: CapabilityCheckContext) -> OneDriveSourceOperations:
|
||||
assert isinstance(context.source_operations, OneDriveSourceOperations)
|
||||
if not isinstance(context.source_operations, OneDriveSourceOperations):
|
||||
raise TypeError("OneDrive checks need the OneDrive gateway.")
|
||||
return context.source_operations
|
||||
|
||||
|
||||
@@ -96,16 +155,49 @@ def _config(context: CapabilityCheckContext) -> OneDriveConnectorConfig:
|
||||
return form_config(context, OneDriveConnectorConfig)
|
||||
|
||||
|
||||
def _raise_row(message: str, row: MicrosoftRowText) -> NoReturn:
|
||||
raise ConnectorValidationError(
|
||||
message, summary=row.summary, description=row.description
|
||||
)
|
||||
|
||||
|
||||
def _is_invalid_entry(user: str) -> bool:
|
||||
if not user.strip():
|
||||
return False
|
||||
try:
|
||||
normalize_configured_users([user])
|
||||
except ConnectorValidationError:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _configured_users(context: CapabilityCheckContext) -> list[str]:
|
||||
return normalize_configured_users(_config(context).users)
|
||||
"""The Users entries, normalized as indexing reads them. An entry that is
|
||||
not an email, or a list of blanks, fails with its row."""
|
||||
users: list[str] = _config(context).users or []
|
||||
try:
|
||||
return normalize_configured_users(users)
|
||||
except ConnectorValidationError as error:
|
||||
if not any(user.strip() for user in users):
|
||||
_raise_row(error.message, _BLANK_USERS_ROW)
|
||||
entry: str = next(
|
||||
(user.strip() for user in users if _is_invalid_entry(user)), ""
|
||||
)
|
||||
_raise_row(
|
||||
error.message,
|
||||
_row(
|
||||
_line("Not an email address: ", entry),
|
||||
"Each entry in Users must be a user principal name or primary "
|
||||
"email address.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _candidate_users(
|
||||
context: CapabilityCheckContext,
|
||||
) -> Generator[OneDriveUser, None, None]:
|
||||
gateway = _gateway(context)
|
||||
config = _config(context)
|
||||
configured = normalize_configured_users(config.users)
|
||||
configured = _configured_users(context)
|
||||
if configured:
|
||||
for identifier in configured[:_CANDIDATE_PAGES]:
|
||||
try:
|
||||
@@ -135,12 +227,21 @@ def _candidate_users(
|
||||
return
|
||||
|
||||
|
||||
class _Opened(BaseModel):
|
||||
"""The user whose OneDrive answered a probe."""
|
||||
|
||||
user: OneDriveUser
|
||||
drive: OneDriveDrive
|
||||
|
||||
|
||||
def _first_drive_that(
|
||||
context: CapabilityCheckContext,
|
||||
opens: Callable[[OneDriveDrive], T | None],
|
||||
denied_message: str,
|
||||
nothing_to_probe: str,
|
||||
) -> tuple[OneDriveDrive, T]:
|
||||
nothing_to_probe: MicrosoftRowText,
|
||||
) -> tuple[_Opened, T]:
|
||||
"""The first candidate's OneDrive on which ``opens`` returns something.
|
||||
A 403 is remembered: when nothing opens it is the likelier cause."""
|
||||
gateway = _gateway(context)
|
||||
denied: OneDriveGraphError | None = None
|
||||
for user in _candidate_users(context):
|
||||
@@ -163,10 +264,18 @@ def _first_drive_that(
|
||||
denied = error
|
||||
continue
|
||||
if opened is not None:
|
||||
return drive, opened
|
||||
return _Opened(user=user, drive=drive), opened
|
||||
if denied is not None:
|
||||
raise_for_graph_error(denied, denied_message)
|
||||
raise _NoCandidateFound(nothing_to_probe)
|
||||
raise_for_graph_error(denied, denied_message, denied_row=_DRIVES_DENIED_ROW)
|
||||
raise _NoCandidateFound(
|
||||
nothing_to_probe.description.split("\n", 1)[0],
|
||||
summary=nothing_to_probe.summary,
|
||||
description=nothing_to_probe.description,
|
||||
)
|
||||
|
||||
|
||||
def _user_name(user: OneDriveUser) -> str:
|
||||
return user.display_name or user.user_principal_name
|
||||
|
||||
|
||||
def _first_delta_item(
|
||||
@@ -176,7 +285,8 @@ def _first_delta_item(
|
||||
) -> DriveDeltaItem | None:
|
||||
cursor: str | None = start_url
|
||||
for _ in range(_CANDIDATE_PAGES):
|
||||
assert cursor is not None
|
||||
if cursor is None:
|
||||
return None
|
||||
result = gateway.get_delta_page(
|
||||
drive_id=drive.id,
|
||||
page_url=cursor,
|
||||
@@ -190,10 +300,12 @@ def _first_delta_item(
|
||||
if item is not None:
|
||||
return item
|
||||
cursor = result.next_checkpoint_url
|
||||
if cursor is None:
|
||||
return None
|
||||
raise ConnectorValidationError(
|
||||
f"No readable OneDrive item was found in {_CANDIDATE_PAGES} delta pages."
|
||||
raise InconclusiveValidationError(
|
||||
f"No readable OneDrive item was found in {_CANDIDATE_PAGES} delta pages.",
|
||||
summary="No files found to test",
|
||||
description=(
|
||||
"The change feed of a OneDrive held only deleted items in its first pages."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -243,15 +355,46 @@ def _group_membership_probe_group(
|
||||
return None
|
||||
|
||||
|
||||
class _TokenCheck(CapabilityCheck):
|
||||
class _OneDriveCheck(CapabilityCheck[OneDriveConnectorConfig]):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
check_id: str,
|
||||
display_name: str,
|
||||
remediation: str,
|
||||
capability: CredentialCapability = CredentialCapability.INDEXING,
|
||||
requires_connector_config: bool = False,
|
||||
validates_binding: bool = False,
|
||||
fails_when_unreachable: bool = False,
|
||||
discovery: bool = False,
|
||||
row_id: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
capability=capability,
|
||||
check_id=check_id,
|
||||
display_name=display_name,
|
||||
requires_connector_instance=False,
|
||||
requires_connector_config=requires_connector_config,
|
||||
remediation=remediation,
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_DEPENDS_ON.get(check_id, ()),
|
||||
validates_binding=validates_binding,
|
||||
fails_when_unreachable=fails_when_unreachable,
|
||||
discovery=discovery,
|
||||
row_id=row_id,
|
||||
)
|
||||
|
||||
|
||||
class _ConfigReadingCheck(_OneDriveCheck):
|
||||
config_class = OneDriveConnectorConfig
|
||||
|
||||
|
||||
class _TokenCheck(_OneDriveCheck):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.INDEXING,
|
||||
check_id=_SIGN_IN_CHECK_ID,
|
||||
display_name="Sign in to Microsoft",
|
||||
requires_connector_instance=False,
|
||||
remediation="Check the app id, tenant id, and client credential.",
|
||||
docs_link=_DOCS_LINK,
|
||||
validates_binding=True,
|
||||
fails_when_unreachable=True,
|
||||
)
|
||||
@@ -267,23 +410,26 @@ class _TokenCheck(CapabilityCheck):
|
||||
)
|
||||
|
||||
|
||||
class _UsersCheck(CapabilityCheck):
|
||||
class _UsersCheck(_OneDriveCheck):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.INDEXING,
|
||||
check_id="onedrive_users",
|
||||
display_name="Tenant users can be listed",
|
||||
requires_connector_instance=False,
|
||||
check_id=_USERS_CHECK_ID,
|
||||
display_name="List users",
|
||||
remediation="Grant and admin-consent `User.Read.All`.",
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_AFTER_SIGN_IN,
|
||||
validates_binding=True,
|
||||
)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> None:
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput:
|
||||
try:
|
||||
_gateway(context).list_users(page_size=_PROBE_PAGE_SIZE)
|
||||
except OneDriveGraphError as error:
|
||||
raise_for_graph_error(error, "The app cannot list tenant users.")
|
||||
raise_for_graph_error(
|
||||
error,
|
||||
"The app cannot list tenant users.",
|
||||
denied_row=_USERS_DENIED_ROW,
|
||||
request="the user listing",
|
||||
)
|
||||
return CheckOutput(summary="Tenant user list is readable")
|
||||
|
||||
|
||||
def _tenant_users(
|
||||
@@ -297,30 +443,27 @@ def _tenant_users(
|
||||
next_link = page.next_link
|
||||
if next_link is None:
|
||||
return
|
||||
raise UnexpectedValidationError(
|
||||
f"The tenant's user listing did not end within {_COUNT_PAGES} pages."
|
||||
raise InconclusiveValidationError(
|
||||
f"The tenant's user listing did not end within {_COUNT_PAGES} pages.",
|
||||
summary=_USER_LIST_UNENDING_ROW.summary,
|
||||
description=_USER_LIST_UNENDING_ROW.description,
|
||||
)
|
||||
|
||||
|
||||
class _DriveCountCheck(CapabilityCheck[OneDriveConnectorConfig]):
|
||||
class _DriveCountCheck(_ConfigReadingCheck):
|
||||
"""Counts the enabled member users: the owners of the OneDrives indexing
|
||||
walks in every-user mode. It does not open each drive, so a user without a
|
||||
OneDrive still counts. Separate from ``onedrive_users``, which reads one
|
||||
user to validate. A config that lists users walks only those, so the
|
||||
tenant count does not apply to it."""
|
||||
|
||||
config_class = OneDriveConnectorConfig
|
||||
OneDrive still counts. It shares the "List users" row, whose probe reads
|
||||
one user. A config that lists users walks only those, so the tenant count
|
||||
does not apply to it."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.INDEXING,
|
||||
check_id="onedrive_drive_count",
|
||||
display_name="Counts the tenant's OneDrives",
|
||||
requires_connector_instance=False,
|
||||
display_name="List users",
|
||||
remediation="Grant and admin-consent `User.Read.All`.",
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_AFTER_SIGN_IN,
|
||||
discovery=True,
|
||||
row_id=_USERS_CHECK_ID,
|
||||
)
|
||||
|
||||
def applies(self, form_state: FormState[OneDriveConnectorConfig]) -> bool:
|
||||
@@ -334,83 +477,169 @@ class _DriveCountCheck(CapabilityCheck[OneDriveConnectorConfig]):
|
||||
cap=MAX_FINDING_OPTIONS,
|
||||
)
|
||||
except OneDriveGraphError as error:
|
||||
raise_for_graph_error(error, "The app cannot list tenant users.")
|
||||
return CheckOutput(findings=[finding])
|
||||
raise_for_graph_error(
|
||||
error,
|
||||
"The app cannot list tenant users.",
|
||||
denied_row=_USERS_DENIED_ROW,
|
||||
request="the user listing",
|
||||
)
|
||||
count: str = f"{finding.count}+" if finding.at_least else str(finding.count)
|
||||
return CheckOutput(
|
||||
summary=f"Up to {count} {'OneDrive' if count == '1' else 'OneDrives'}",
|
||||
findings=[finding],
|
||||
)
|
||||
|
||||
|
||||
class _ConfiguredUsersCheck(CapabilityCheck[OneDriveConnectorConfig]):
|
||||
config_class = OneDriveConnectorConfig
|
||||
class _UserProblem(BaseModel):
|
||||
identifier: str
|
||||
message: str
|
||||
locked: bool
|
||||
|
||||
|
||||
def _user_problem_row(problem: _UserProblem) -> MicrosoftRowText:
|
||||
if problem.locked:
|
||||
return _row(
|
||||
_line("User is locked: ", problem.identifier),
|
||||
f"Microsoft refused this user's lookup.\n{_RETRY_LINE}",
|
||||
)
|
||||
return _row(
|
||||
_line("User not found: ", problem.identifier),
|
||||
"No enabled member user has this address.\nCorrect or remove "
|
||||
f"{problem.identifier} in Users.",
|
||||
)
|
||||
|
||||
|
||||
def _users_output(identifiers: list[str], problems: list[_UserProblem]) -> CheckOutput:
|
||||
if not problems:
|
||||
return CheckOutput(
|
||||
summary=(
|
||||
"1 selected user found"
|
||||
if len(identifiers) == 1
|
||||
else f"All {len(identifiers)} selected users found"
|
||||
)
|
||||
)
|
||||
message: str = " ".join(problem.message for problem in problems)
|
||||
first: MicrosoftRowText = _user_problem_row(problems[0])
|
||||
names: str = ", ".join(problem.identifier for problem in problems)
|
||||
if len(problems) == len(identifiers):
|
||||
if all(problem.locked for problem in problems):
|
||||
raise UnexpectedValidationError(
|
||||
message, summary=first.summary, description=first.description
|
||||
)
|
||||
_raise_row(
|
||||
message,
|
||||
first
|
||||
if len(problems) == 1
|
||||
else _row(
|
||||
"No selected user found",
|
||||
"None of the selected users is visible to this account.\nNot "
|
||||
f"found: {names}.",
|
||||
),
|
||||
)
|
||||
if len(problems) == 1:
|
||||
return CheckOutput(
|
||||
summary=first.summary, description=first.description, partial=True
|
||||
)
|
||||
return CheckOutput(
|
||||
summary=f"{len(problems)} of {len(identifiers)} users not found",
|
||||
description=f"Not found: {names}.\nCorrect or remove them in Users.",
|
||||
partial=True,
|
||||
)
|
||||
|
||||
|
||||
class _ConfiguredUsersCheck(_ConfigReadingCheck):
|
||||
"""Every Users entry names an enabled member user. Some missing is
|
||||
INCOMPLETE, and the drive checks walk the found ones; none found
|
||||
FAILS."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.INDEXING,
|
||||
check_id="onedrive_configured_users",
|
||||
display_name="Configured users resolve",
|
||||
requires_connector_instance=False,
|
||||
requires_connector_config=True,
|
||||
check_id=_CONFIGURED_USERS_CHECK_ID,
|
||||
display_name="Find selected users",
|
||||
remediation="Use enabled member user principal names.",
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_AFTER_SIGN_IN,
|
||||
requires_connector_config=True,
|
||||
)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> None:
|
||||
def applies(self, form_state: FormState[OneDriveConnectorConfig]) -> bool:
|
||||
return bool(form_state.config.users)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput:
|
||||
gateway = _gateway(context)
|
||||
for identifier in _configured_users(context):
|
||||
identifiers: list[str] = _configured_users(context)
|
||||
problems: list[_UserProblem] = []
|
||||
for identifier in identifiers:
|
||||
try:
|
||||
user = gateway.get_user(identifier=identifier)
|
||||
except OneDriveGraphError as error:
|
||||
raise_for_graph_error(error, f"The app cannot resolve `{identifier}`.")
|
||||
if error.status != _LOCKED_STATUS:
|
||||
raise_for_graph_error(
|
||||
error,
|
||||
f"The app cannot resolve `{identifier}`.",
|
||||
denied_row=_USERS_DENIED_ROW,
|
||||
request="the user lookup",
|
||||
)
|
||||
problems.append(
|
||||
_UserProblem(
|
||||
identifier=identifier,
|
||||
message=f"The app cannot resolve `{identifier}` ({error.code}).",
|
||||
locked=True,
|
||||
)
|
||||
)
|
||||
continue
|
||||
if user is None:
|
||||
raise ConnectorValidationError(f"No user matches `{identifier}`.")
|
||||
problems.append(
|
||||
_UserProblem(
|
||||
identifier=identifier,
|
||||
message=f"No user matches `{identifier}`.",
|
||||
locked=False,
|
||||
)
|
||||
)
|
||||
return _users_output(identifiers, problems)
|
||||
|
||||
|
||||
class _DriveCheck(CapabilityCheck[OneDriveConnectorConfig]):
|
||||
config_class = OneDriveConnectorConfig
|
||||
|
||||
class _DriveCheck(_ConfigReadingCheck):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.INDEXING,
|
||||
check_id="onedrive_drive",
|
||||
display_name="A OneDrive is readable",
|
||||
requires_connector_instance=False,
|
||||
requires_connector_config=True,
|
||||
check_id=_DRIVE_CHECK_ID,
|
||||
display_name="Find a OneDrive",
|
||||
remediation="Grant `Sites.Read.All` or a selected personal-site read grant.",
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_AFTER_SIGN_IN,
|
||||
requires_connector_config=True,
|
||||
)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> None:
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput:
|
||||
try:
|
||||
_first_drive_that(
|
||||
opened, _ = _first_drive_that(
|
||||
context,
|
||||
lambda drive: drive,
|
||||
"The app cannot read the tenant's first OneDrives.",
|
||||
"No readable OneDrive was found among the first users.",
|
||||
_NO_DRIVE_ROW,
|
||||
)
|
||||
except OneDriveGraphError as error:
|
||||
raise_for_graph_error(error, "The app cannot read this user's OneDrive.")
|
||||
|
||||
|
||||
class _DeltaCheck(CapabilityCheck[OneDriveConnectorConfig]):
|
||||
config_class = OneDriveConnectorConfig
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.INDEXING,
|
||||
check_id="onedrive_delta",
|
||||
display_name="OneDrive changes are readable",
|
||||
requires_connector_instance=False,
|
||||
requires_connector_config=True,
|
||||
remediation="Grant `Sites.Read.All` or a selected personal-site read grant.",
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_AFTER_SIGN_IN,
|
||||
raise_for_graph_error(
|
||||
error,
|
||||
"The app cannot read this user's OneDrive.",
|
||||
denied_row=_DRIVES_DENIED_ROW,
|
||||
request="the OneDrive read",
|
||||
)
|
||||
return CheckOutput(
|
||||
summary=_line("Opened the OneDrive of ", _user_name(opened.user))
|
||||
)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> None:
|
||||
|
||||
class _DeltaCheck(_ConfigReadingCheck):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
check_id="onedrive_delta",
|
||||
display_name="Read OneDrive changes",
|
||||
remediation="Grant `Sites.Read.All` or a selected personal-site read grant.",
|
||||
requires_connector_config=True,
|
||||
)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput:
|
||||
gateway = _gateway(context)
|
||||
try:
|
||||
host = _config(context).graph_api_host.rstrip("/")
|
||||
_first_drive_that(
|
||||
opened, _ = _first_drive_that(
|
||||
context,
|
||||
lambda drive: gateway.get_delta_page(
|
||||
drive_id=drive.id,
|
||||
@@ -424,100 +653,126 @@ class _DeltaCheck(CapabilityCheck[OneDriveConnectorConfig]):
|
||||
allow_full_resync=True,
|
||||
),
|
||||
"The app cannot read changes in the tenant's first OneDrives.",
|
||||
"No readable OneDrive delta was found among the first users.",
|
||||
_row(
|
||||
"OneDrive changes could not be read",
|
||||
"The OneDrives opened, but none answered the change feed in a "
|
||||
"usable way.\nRe-run the checks.",
|
||||
),
|
||||
)
|
||||
except OneDriveGraphError as error:
|
||||
raise_for_graph_error(error, "The app cannot read OneDrive changes.")
|
||||
raise_for_graph_error(
|
||||
error,
|
||||
"The app cannot read OneDrive changes.",
|
||||
denied_row=_DRIVES_DENIED_ROW,
|
||||
request="the change feed read",
|
||||
)
|
||||
return CheckOutput(
|
||||
summary=_line("Read changes in ", _user_name(opened.user), "'s OneDrive")
|
||||
)
|
||||
|
||||
|
||||
class _PermissionCheck(CapabilityCheck[OneDriveConnectorConfig]):
|
||||
config_class = OneDriveConnectorConfig
|
||||
|
||||
class _PermissionCheck(_ConfigReadingCheck):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.DOC_PERMISSION_SYNC,
|
||||
check_id="onedrive_item_permissions",
|
||||
display_name="OneDrive item permissions are readable",
|
||||
requires_connector_instance=False,
|
||||
requires_connector_config=True,
|
||||
display_name="Read file permissions",
|
||||
remediation="Grant `Sites.Read.All` or a selected personal-site read grant.",
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_AFTER_SIGN_IN,
|
||||
requires_connector_config=True,
|
||||
)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput | None:
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput:
|
||||
gateway = _gateway(context)
|
||||
host = _config(context).graph_api_host.rstrip("/")
|
||||
found_empty_drive = False
|
||||
|
||||
def probe_permissions(drive: OneDriveDrive) -> bool | None:
|
||||
nonlocal found_empty_drive
|
||||
result = _probe_item_permissions(
|
||||
gateway,
|
||||
drive,
|
||||
f"{host}/{GRAPH_API_VERSION}",
|
||||
)
|
||||
found_empty_drive = result is None
|
||||
return result
|
||||
|
||||
try:
|
||||
_first_drive_that(
|
||||
context,
|
||||
probe_permissions,
|
||||
lambda drive: _probe_item_permissions(
|
||||
gateway, drive, f"{host}/{GRAPH_API_VERSION}"
|
||||
),
|
||||
"The app cannot read permissions in the tenant's first OneDrives.",
|
||||
"No readable OneDrive was found among the first users.",
|
||||
_row(
|
||||
"No files found to test",
|
||||
"The OneDrives the check sampled hold no file.\nAdd a file to "
|
||||
"one of them, then run the checks again.",
|
||||
),
|
||||
)
|
||||
except _NoCandidateFound:
|
||||
if not found_empty_drive:
|
||||
raise
|
||||
return CheckOutput(advisories=[_EMPTY_DRIVE_ADVISORY])
|
||||
except OneDriveGraphError as error:
|
||||
raise_for_graph_error(error, "The app cannot read OneDrive permissions.")
|
||||
return None
|
||||
raise_for_graph_error(
|
||||
error,
|
||||
"The app cannot read OneDrive permissions.",
|
||||
denied_row=_DRIVES_DENIED_ROW,
|
||||
request="the permission read",
|
||||
)
|
||||
return CheckOutput(summary="Read permissions of a file in a OneDrive")
|
||||
|
||||
|
||||
class _GroupListCheck(CapabilityCheck):
|
||||
class _GroupListCheck(_OneDriveCheck):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.EXTERNAL_GROUP_SYNC,
|
||||
check_id="onedrive_groups",
|
||||
display_name="Entra groups are readable",
|
||||
requires_connector_instance=False,
|
||||
check_id=_GROUPS_CHECK_ID,
|
||||
display_name="List groups",
|
||||
remediation="Grant and admin-consent `GroupMember.ReadBasic.All`.",
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_AFTER_SIGN_IN,
|
||||
)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> None:
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput:
|
||||
try:
|
||||
_gateway(context).list_groups(page_size=_PROBE_PAGE_SIZE)
|
||||
except OneDriveGraphError as error:
|
||||
raise_for_graph_error(error, "The app cannot list Entra groups.")
|
||||
raise_for_graph_error(
|
||||
error,
|
||||
"The app cannot list Entra groups.",
|
||||
denied_row=ENTRA_GROUPS_DENIED_ROW,
|
||||
request="the group listing",
|
||||
)
|
||||
return CheckOutput(summary="Entra group list is readable")
|
||||
|
||||
|
||||
class _GroupMembershipCheck(CapabilityCheck):
|
||||
class _GroupMembershipCheck(_OneDriveCheck):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
capability=CredentialCapability.EXTERNAL_GROUP_SYNC,
|
||||
check_id="onedrive_group_members",
|
||||
display_name="Entra transitive group members are readable",
|
||||
requires_connector_instance=False,
|
||||
display_name="Read group members",
|
||||
remediation="Grant `GroupMember.ReadBasic.All`.",
|
||||
docs_link=_DOCS_LINK,
|
||||
depends_on=_AFTER_SIGN_IN,
|
||||
)
|
||||
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput | None:
|
||||
def run(self, context: CapabilityCheckContext) -> CheckOutput:
|
||||
gateway = _gateway(context)
|
||||
denied = "The app cannot expand transitive Entra group members."
|
||||
try:
|
||||
group = _group_membership_probe_group(gateway)
|
||||
if group is None:
|
||||
return CheckOutput(advisories=[_NO_VISIBLE_GROUP_ADVISORY])
|
||||
raise InconclusiveValidationError(
|
||||
"The check found no Entra group with visible membership in "
|
||||
f"the first {_CANDIDATE_PAGES} groups, so it read no members.",
|
||||
summary="No readable groups found",
|
||||
description=(
|
||||
"The first groups all hide their members, or the tenant "
|
||||
"has none."
|
||||
),
|
||||
)
|
||||
gateway.list_transitive_group_members(group_id=group.id)
|
||||
except OneDriveGraphError as error:
|
||||
denied = "The app cannot expand transitive Entra group members."
|
||||
raise_for_graph_error(error, denied)
|
||||
return None
|
||||
if error.status == 404:
|
||||
raise InconclusiveValidationError(
|
||||
denied,
|
||||
summary="Sample group disappeared",
|
||||
description=(
|
||||
"The group was deleted between the listing and the member "
|
||||
"read.\nRe-run the checks."
|
||||
),
|
||||
) from error
|
||||
raise_for_graph_error(
|
||||
error,
|
||||
denied,
|
||||
denied_row=_GROUP_MEMBERS_DENIED_ROW,
|
||||
request="the group member read",
|
||||
)
|
||||
return CheckOutput(
|
||||
summary=_line("Read members of ", group.display_name or group.id)
|
||||
)
|
||||
|
||||
|
||||
def build_onedrive_indexing_checks() -> list[CapabilityCheck]:
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
"""The OneDrive checks' rows: the design's names, short lines and
|
||||
descriptions, the run order, the partial find of selected users, and what a
|
||||
check that tested nothing says."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, create_autospec
|
||||
|
||||
import pytest
|
||||
|
||||
from onyx.configs.constants import DocumentSource
|
||||
from onyx.connectors.capability_checks.models import (
|
||||
CapabilityCheck,
|
||||
CapabilityCheckContext,
|
||||
CapabilityCheckResult,
|
||||
CapabilityCheckStatus,
|
||||
CheckErrorKind,
|
||||
)
|
||||
from onyx.connectors.capability_checks.runner import run_capability_checks
|
||||
from onyx.connectors.microsoft_utils.drive_delta import (
|
||||
DriveDeltaFetchResult,
|
||||
DriveDeltaPage,
|
||||
)
|
||||
from onyx.connectors.microsoft_utils.entra import (
|
||||
EntraDirectoryObject,
|
||||
EntraGroup,
|
||||
EntraPage,
|
||||
)
|
||||
from onyx.connectors.microsoft_utils.graph_errors import MicrosoftGraphError
|
||||
from onyx.connectors.microsoft_utils.models import MicrosoftTokenInfo
|
||||
from onyx.connectors.onedrive.capability_checks import (
|
||||
build_onedrive_doc_permission_sync_checks,
|
||||
build_onedrive_group_sync_checks,
|
||||
build_onedrive_indexing_checks,
|
||||
)
|
||||
from onyx.connectors.onedrive.models import (
|
||||
OneDriveDrive,
|
||||
OneDriveUser,
|
||||
OneDriveUserPage,
|
||||
)
|
||||
from onyx.connectors.onedrive.source_operations import OneDriveSourceOperations
|
||||
|
||||
_CHECKS: dict[str, CapabilityCheck] = {
|
||||
check.check_id: check
|
||||
for check in build_onedrive_indexing_checks()
|
||||
+ build_onedrive_doc_permission_sync_checks()
|
||||
+ build_onedrive_group_sync_checks()
|
||||
}
|
||||
_OWNER = OneDriveUser(
|
||||
id="owner-id", user_principal_name="owner@example.com", display_name="Owner"
|
||||
)
|
||||
|
||||
|
||||
def _gateway() -> MagicMock:
|
||||
gateway: MagicMock = create_autospec(OneDriveSourceOperations, instance=True)
|
||||
gateway.check_token.return_value = MicrosoftTokenInfo(app_name="Onyx")
|
||||
gateway.list_users.return_value = OneDriveUserPage(users=[_OWNER])
|
||||
gateway.get_user.return_value = _OWNER
|
||||
gateway.get_default_drive.return_value = OneDriveDrive(id="d", name="Documents")
|
||||
gateway.get_delta_page.return_value = DriveDeltaFetchResult(
|
||||
page=DriveDeltaPage.model_validate(
|
||||
{"value": [{"id": "item", "name": "a.pdf", "file": {}}]}
|
||||
)
|
||||
)
|
||||
gateway.list_groups.return_value = EntraPage[EntraGroup](
|
||||
items=[EntraGroup(id="g", displayName="Engineering")]
|
||||
)
|
||||
gateway.list_transitive_group_members.return_value = EntraPage[
|
||||
EntraDirectoryObject
|
||||
](items=[])
|
||||
return gateway
|
||||
|
||||
|
||||
def _run_all(
|
||||
check_ids: list[str], gateway: MagicMock, config: dict[str, Any] | None = None
|
||||
) -> dict[str, CapabilityCheckResult]:
|
||||
context = CapabilityCheckContext(
|
||||
source=DocumentSource.ONEDRIVE,
|
||||
credential_json={},
|
||||
connector_specific_config=config if config is not None else {},
|
||||
source_operations=gateway,
|
||||
)
|
||||
return {
|
||||
result.check_id: result
|
||||
for result in run_capability_checks(
|
||||
[_CHECKS[check_id] for check_id in check_ids], context
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _run(
|
||||
check_id: str, gateway: MagicMock, config: dict[str, Any] | None = None
|
||||
) -> CapabilityCheckResult:
|
||||
return _run_all([check_id], gateway, config)[check_id]
|
||||
|
||||
|
||||
def test_the_rows_have_the_design_names() -> None:
|
||||
assert {check_id: check.display_name for check_id, check in _CHECKS.items()} == {
|
||||
"onedrive_token_auth": "Sign in to Microsoft",
|
||||
"onedrive_users": "List users",
|
||||
"onedrive_drive_count": "List users",
|
||||
"onedrive_configured_users": "Find selected users",
|
||||
"onedrive_drive": "Find a OneDrive",
|
||||
"onedrive_delta": "Read OneDrive changes",
|
||||
"onedrive_item_permissions": "Read file permissions",
|
||||
"onedrive_groups": "List groups",
|
||||
"onedrive_group_members": "Read group members",
|
||||
}
|
||||
assert _CHECKS["onedrive_drive_count"].row_id == "onedrive_users"
|
||||
assert _CHECKS["onedrive_users"].validates_binding
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("check_id", "summary"),
|
||||
[
|
||||
("onedrive_token_auth", "Signed in as Onyx"),
|
||||
("onedrive_users", "Tenant user list is readable"),
|
||||
("onedrive_drive_count", "Up to 1 OneDrive"),
|
||||
("onedrive_drive", "Opened the OneDrive of Owner"),
|
||||
("onedrive_delta", "Read changes in Owner's OneDrive"),
|
||||
("onedrive_item_permissions", "Read permissions of a file in a OneDrive"),
|
||||
("onedrive_groups", "Entra group list is readable"),
|
||||
("onedrive_group_members", "Read members of Engineering"),
|
||||
],
|
||||
)
|
||||
def test_a_passing_check_says_what_it_found(check_id: str, summary: str) -> None:
|
||||
result = _run(check_id, _gateway())
|
||||
|
||||
assert result.status == CapabilityCheckStatus.PASSED, result.message
|
||||
assert result.summary == summary
|
||||
|
||||
|
||||
def test_a_refused_user_listing_names_the_grant() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.list_users.side_effect = MicrosoftGraphError(403, "Authorization", "")
|
||||
|
||||
result = _run("onedrive_users", gateway)
|
||||
|
||||
assert result.status == CapabilityCheckStatus.FAILED
|
||||
assert result.summary == "No permission to read user directory"
|
||||
|
||||
|
||||
# --- Find selected users ---
|
||||
|
||||
|
||||
def test_all_selected_users_found() -> None:
|
||||
result = _run(
|
||||
"onedrive_configured_users",
|
||||
_gateway(),
|
||||
{"users": ["owner@example.com", "b@example.com"]},
|
||||
)
|
||||
|
||||
assert result.summary == "All 2 selected users found"
|
||||
|
||||
|
||||
def test_an_entry_that_is_not_an_email_names_it() -> None:
|
||||
result = _run("onedrive_configured_users", _gateway(), {"users": ["not-an-email"]})
|
||||
|
||||
assert result.status == CapabilityCheckStatus.FAILED
|
||||
assert result.summary == "Not an email address: not-an-email"
|
||||
|
||||
|
||||
def test_blank_entries_say_so() -> None:
|
||||
result = _run("onedrive_configured_users", _gateway(), {"users": [" "]})
|
||||
|
||||
assert result.summary == "Users holds only blank entries"
|
||||
|
||||
|
||||
def test_some_missing_users_are_a_partial_find() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.get_user.side_effect = lambda identifier: (
|
||||
_OWNER if identifier == "owner@example.com" else None
|
||||
)
|
||||
|
||||
results = _run_all(
|
||||
["onedrive_users", "onedrive_configured_users", "onedrive_drive"],
|
||||
gateway,
|
||||
{"users": ["owner@example.com", "gone@example.com"]},
|
||||
)
|
||||
|
||||
users = results["onedrive_configured_users"]
|
||||
assert users.status == CapabilityCheckStatus.INCOMPLETE
|
||||
assert users.partial
|
||||
assert users.summary == "User not found: gone@example.com"
|
||||
assert results["onedrive_drive"].status == CapabilityCheckStatus.PASSED
|
||||
|
||||
|
||||
def test_no_selected_user_found_fails_and_stops_the_drive_check() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.get_user.return_value = None
|
||||
|
||||
results = _run_all(
|
||||
["onedrive_configured_users", "onedrive_drive"],
|
||||
gateway,
|
||||
{"users": ["a@example.com", "b@example.com"]},
|
||||
)
|
||||
|
||||
assert results["onedrive_configured_users"].summary == "No selected user found"
|
||||
assert results["onedrive_drive"].blocked_by == "onedrive_configured_users"
|
||||
|
||||
|
||||
# --- Samples that test nothing ---
|
||||
|
||||
|
||||
def test_no_drive_to_open_is_incomplete() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.get_default_drive.return_value = None
|
||||
|
||||
result = _run("onedrive_drive", gateway)
|
||||
|
||||
assert result.status == CapabilityCheckStatus.INCOMPLETE
|
||||
assert result.summary == "No OneDrives found to test"
|
||||
|
||||
|
||||
def test_every_drive_refused_names_the_grant() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.get_default_drive.side_effect = MicrosoftGraphError(403, "accessDenied", "")
|
||||
|
||||
result = _run("onedrive_drive", gateway)
|
||||
|
||||
assert result.status == CapabilityCheckStatus.FAILED
|
||||
assert result.summary == "No permission to read sampled OneDrives"
|
||||
|
||||
|
||||
def test_an_empty_drive_tests_no_permission_read() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.get_delta_page.return_value = DriveDeltaFetchResult(page=DriveDeltaPage())
|
||||
|
||||
result = _run("onedrive_item_permissions", gateway)
|
||||
|
||||
assert result.status == CapabilityCheckStatus.INCOMPLETE
|
||||
assert result.summary == "No files found to test"
|
||||
|
||||
|
||||
def test_a_group_that_went_away_is_incomplete() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.list_transitive_group_members.side_effect = MicrosoftGraphError(
|
||||
404, "Request_ResourceNotFound", ""
|
||||
)
|
||||
|
||||
result = _run("onedrive_group_members", gateway)
|
||||
|
||||
assert result.status == CapabilityCheckStatus.INCOMPLETE
|
||||
assert result.summary == "Sample group disappeared"
|
||||
|
||||
|
||||
def test_a_refused_group_listing_stops_the_member_check() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.list_groups.side_effect = MicrosoftGraphError(403, "Authorization", "")
|
||||
|
||||
results = _run_all(["onedrive_groups", "onedrive_group_members"], gateway)
|
||||
|
||||
assert results["onedrive_groups"].summary == "No permission to read Entra groups"
|
||||
assert results["onedrive_group_members"].blocked_by == "onedrive_groups"
|
||||
|
||||
|
||||
def test_a_throttled_listing_is_rate_limited() -> None:
|
||||
gateway = _gateway()
|
||||
gateway.list_users.side_effect = MicrosoftGraphError(429, "TooManyRequests", "")
|
||||
|
||||
result = _run("onedrive_users", gateway)
|
||||
|
||||
assert result.error_kind == CheckErrorKind.RATE_LIMITED
|
||||
assert result.summary == "Microsoft is throttling requests"
|
||||
@@ -27,6 +27,7 @@ from onyx.connectors.exceptions import (
|
||||
ConnectorValidationError,
|
||||
CredentialExpiredError,
|
||||
CredentialInvalidError,
|
||||
InconclusiveValidationError,
|
||||
InsufficientPermissionsError,
|
||||
UnexpectedValidationError,
|
||||
)
|
||||
@@ -1208,7 +1209,7 @@ def test_onedrive_capability_user_probe_is_bounded_for_mock_pages() -> None:
|
||||
if check.check_id == "onedrive_drive"
|
||||
)
|
||||
|
||||
with pytest.raises(ConnectorValidationError, match="No readable OneDrive"):
|
||||
with pytest.raises(InconclusiveValidationError, match="has a OneDrive"):
|
||||
check.run(context)
|
||||
assert gateway.list_users.call_count == 20
|
||||
|
||||
@@ -1435,11 +1436,8 @@ def test_onedrive_permission_check_accepts_empty_drive_at_end_cursor() -> None:
|
||||
build_onedrive_doc_permission_sync_checks(), context
|
||||
)
|
||||
|
||||
assert result.status == CapabilityCheckStatus.PASSED
|
||||
(advisory,) = result.advisories
|
||||
assert "did not read item permissions" in advisory.message
|
||||
# The admin must know which drives the check samples.
|
||||
assert "first 20 configured users" in advisory.message
|
||||
assert result.status == CapabilityCheckStatus.INCOMPLETE
|
||||
assert result.summary == "No files found to test"
|
||||
gateway.get_delta_page.assert_called_once()
|
||||
gateway.list_permissions.assert_not_called()
|
||||
|
||||
@@ -1493,7 +1491,7 @@ def test_onedrive_permission_check_bounds_empty_delta_pages() -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ConnectorValidationError, match="No readable OneDrive item was found"
|
||||
InconclusiveValidationError, match="No readable OneDrive item was found"
|
||||
):
|
||||
build_onedrive_doc_permission_sync_checks()[0].run(context)
|
||||
|
||||
@@ -1517,9 +1515,8 @@ def test_onedrive_hidden_group_check_does_not_require_optional_scope() -> None:
|
||||
)
|
||||
(result,) = run_capability_checks([_group_members_check()], context)
|
||||
|
||||
assert result.status == CapabilityCheckStatus.PASSED
|
||||
(advisory,) = result.advisories
|
||||
assert "did not read transitive group members" in advisory.message
|
||||
assert result.status == CapabilityCheckStatus.INCOMPLETE
|
||||
assert result.summary == "No readable groups found"
|
||||
gateway.list_transitive_group_members.assert_not_called()
|
||||
|
||||
|
||||
@@ -1534,9 +1531,8 @@ def test_onedrive_group_check_reports_tenant_without_groups() -> None:
|
||||
|
||||
(result,) = run_capability_checks([_group_members_check()], context)
|
||||
|
||||
assert result.status == CapabilityCheckStatus.PASSED
|
||||
(advisory,) = result.advisories
|
||||
assert "did not read transitive group members" in advisory.message
|
||||
assert result.status == CapabilityCheckStatus.INCOMPLETE
|
||||
assert result.summary == "No readable groups found"
|
||||
gateway.list_groups.assert_called_once()
|
||||
gateway.list_transitive_group_members.assert_not_called()
|
||||
|
||||
@@ -1631,9 +1627,9 @@ def test_onedrive_group_check_bounds_hidden_group_discovery() -> None:
|
||||
if check.check_id == "onedrive_group_members"
|
||||
)
|
||||
|
||||
output = check.run(context)
|
||||
with pytest.raises(InconclusiveValidationError, match="first 20 groups"):
|
||||
check.run(context)
|
||||
|
||||
assert len(_advisories(output)) == 1
|
||||
assert gateway.list_groups.call_count == 20
|
||||
gateway.list_transitive_group_members.assert_not_called()
|
||||
|
||||
|
||||
Reference in new issue
Block a user