Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -514,11 +514,16 @@ def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_aut
def nsf_access_list(
vault: vault_online.VaultOnline,
folder_uids: List[str],
record_uids: List[str]) -> None:
record_uids: List[str],
show_inherited: bool = False,
show_denied: bool = False) -> None:
"""
Print access details for a caller-supplied list of NSF folder and
record UIDs.

Inherited and denied entries are omitted unless ``show_inherited`` /
``show_denied`` is True (the SDK default is to hide them).

Access details are not pre-populated in the NSF in-memory cache, so
this fetches them for exactly the requested UIDs via
``get_nsf_folder_access()`` / ``get_nsf_record_accesses()``, caches
Expand All @@ -535,7 +540,8 @@ def nsf_access_list(
if not folder_uids:
print("No NSF folder UIDs requested.")
else:
access = nsf_management.get_nsf_folder_access(vault, folder_uids)
access = nsf_management.get_nsf_folder_access(
vault, folder_uids, show_inherited=show_inherited, show_denied=show_denied)
for fr in access.get("results") or []:
fuid = fr.get("folder_uid")
if fuid:
Expand All @@ -560,6 +566,7 @@ def nsf_access_list(
f" | access_type={access.get('access_type')}"
f" | role={access.get('role')}"
f" | inherited={access.get('inherited')}"
f" | denied={access.get('denied_access')}"
f" | hidden={access.get('hidden')}"
f" | owner={access.get('owner', False)}"
)
Expand All @@ -574,7 +581,8 @@ def nsf_access_list(
if not record_uids:
print("No NSF record UIDs requested.")
else:
access = nsf_management.get_nsf_record_accesses(vault, record_uids)
access = nsf_management.get_nsf_record_accesses(
vault, record_uids, show_inherited=show_inherited, show_denied=show_denied)
by_uid: Dict[str, List] = {}
for ao in access.get("record_accesses") or []:
by_uid.setdefault(ao["record_uid"], []).append(ao)
Expand Down Expand Up @@ -619,9 +627,13 @@ def nsf_access_list_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None:
folder_uids = ["<FOLDER_UID_1>", "<FOLDER_UID_2>"]
record_uids = ["<RECORD_UID_1>", "<RECORD_UID_2>"]

# Set to True to include inherited / denied entries (hidden by default).
show_inherited = False
show_denied = False

vault = open_vault(keeper_auth_context)
try:
nsf_access_list(vault, folder_uids, record_uids)
nsf_access_list(vault, folder_uids, record_uids, show_inherited, show_denied)
except Exception as e:
print(f"Error: {e}")
finally:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -515,16 +515,14 @@ def nsf_access_list(
vault: vault_online.VaultOnline,
folder_uids: List[str],
record_uids: List[str],
show_inherited: bool = False,
show_denied: bool = False
) -> None:
load_folder = bool(folder_uids)
load_record = bool(record_uids)
view = vault.nsf_data
if view is None:
print("NSF storage is not available on this vault.")
return
nsf_management.load_nsf_access_details(vault, load_folder=load_folder, load_record=load_record, show_inherited=show_inherited, show_denied=show_denied)
nsf_management.load_nsf_access_details(vault, load_folder=load_folder, load_record=load_record)

for folder_uid in folder_uids:
folder = view.get_folder(folder_uid)
Expand Down Expand Up @@ -584,12 +582,9 @@ def nsf_access_list_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None:
folder_uids = ["<FOLDER_UID_1>", "<FOLDER_UID_2>"]
record_uids = ["<RECORD_UID_1>", "<RECORD_UID_2>"]

show_inherited = True
show_denied = True

vault = open_vault(keeper_auth_context)
try:
nsf_access_list(vault, folder_uids, record_uids, show_inherited, show_denied)
nsf_access_list(vault, folder_uids, record_uids)
except Exception as e:
print(f"Error: {e}")
finally:
Expand Down
6 changes: 5 additions & 1 deletion examples/sdk_examples/nested_shared_folders/nsf_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,10 +515,14 @@ def nsf_get(vault: vault_online.VaultOnline) -> None:
"""Get NSF record or folder details by UID or title (nsf-get).

For folders, the response includes parent_uid and parent_name (when available).
Inherited and denied access entries are omitted unless requested.
"""
ITEM_UID_OR_TITLE = "<record_uid_or_title>" # Record/folder UID or title
SHOW_INHERITED = False # include inherited permission entries
SHOW_DENIED = False # include denied-access permission entries

detail = nsf_management.get_nsf_item(vault, ITEM_UID_OR_TITLE)
detail = nsf_management.get_nsf_item(
vault, ITEM_UID_OR_TITLE, show_inherited=SHOW_INHERITED, show_denied=SHOW_DENIED)
print(json.dumps(detail, indent=2, default=str))


Expand Down
2 changes: 1 addition & 1 deletion keepercli-package/src/keepercli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
# Contact: commander@keepersecurity.com
#

__version__ = '1.2.8'
__version__ = '1.2.9'

2 changes: 1 addition & 1 deletion keepersdk-package/src/keepersdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
#
from . import background

__version__ = '1.2.8'
__version__ = '1.2.9'

background.init()
35 changes: 19 additions & 16 deletions keepersdk-package/src/keepersdk/vault/nsf_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,15 +681,6 @@ def folder_inherits_parent_permissions(vault: VaultOnline, folder_uid: str) -> b
return row.inherit_user_permissions != int(folder_pb2.BOOLEAN_FALSE)


def ensure_folder_direct_permissions(
vault: VaultOnline,
folder_uid: str) -> bool:
"""Check whether folder permission inheritance is enabled."""
if not folder_inherits_parent_permissions(vault, folder_uid):
return False
return True


def access_role_label(access: Dict[str, Any]) -> str:
if access.get('owner'):
return 'owner'
Expand Down Expand Up @@ -759,8 +750,11 @@ def plan_folder_access_change(state: str, action: str) -> List[Dict[str, Any]]:

``action`` is ``'grant'`` (set/change a role), ``'deny'`` or ``'remove'``.
Each step is ``{'request': <list name>, 'include_folder_key': bool,
'denied_access': bool}``. Steps are sent sequentially and a later step is
sent only when the previous one succeeded.
'denied_access': bool, 'rollback': Optional[str]}``. Steps are sent
sequentially and a later step is sent only when the previous one
succeeded. ``rollback`` names the compensating action to take for a
completed step when a later step fails (``'deny'`` re-applies a denial
that was removed, so a failed grant never leaves inherited access open).

* inherited -> grant : folderAccessAdds + recipient-encrypted folderKey
* denied -> grant : folderAccessRemoves (denial), then
Expand All @@ -774,15 +768,19 @@ def plan_folder_access_change(state: str, action: str) -> List[Dict[str, Any]]:

Raises ValueError for transitions that are not allowed.
"""
add_with_key = {'request': FOLDER_ACCESS_ADD, 'include_folder_key': True, 'denied_access': False}
update = {'request': FOLDER_ACCESS_UPDATE, 'include_folder_key': False, 'denied_access': False}
remove = {'request': FOLDER_ACCESS_REMOVE, 'include_folder_key': False, 'denied_access': False}
add_with_key = {'request': FOLDER_ACCESS_ADD, 'include_folder_key': True,
'denied_access': False, 'rollback': None}
update = {'request': FOLDER_ACCESS_UPDATE, 'include_folder_key': False,
'denied_access': False, 'rollback': None}
remove = {'request': FOLDER_ACCESS_REMOVE, 'include_folder_key': False,
'denied_access': False, 'rollback': None}

if action == 'grant':
if state == FOLDER_ACCESS_DIRECT:
return [update]
if state == FOLDER_ACCESS_DENIED:
return [remove, add_with_key]
# If the add fails, the removed denial must be restored.
return [dict(remove, rollback='deny'), add_with_key]
return [add_with_key]

if action == 'deny':
Expand All @@ -803,8 +801,13 @@ def plan_folder_access_change(state: str, action: str) -> List[Dict[str, Any]]:


def current_folder_role_name(accessors: List[Dict[str, Any]]) -> Optional[str]:
"""AccessRoleType name of the most specific row (direct before inherited)."""
"""AccessRoleType name of the most specific row (direct before inherited).

Denied rows grant nothing, so they are ignored.
"""
for a in sorted(accessors or [], key=lambda r: bool(r.get('inherited'))):
if a.get('denied_access'):
continue
if a.get('role'):
return a['role']
return None
75 changes: 62 additions & 13 deletions keepersdk-package/src/keepersdk/vault/nsf_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,11 @@ def get_nsf_folder_detail(
include_access: bool = True,
show_inherited: bool = False,
show_denied: bool = False) -> Dict[str, Any]:
"""Folder detail payload for ``nsf-get`` (folder branch)."""
"""Folder detail payload for ``nsf-get`` (folder branch).

Inherited / denied accessors are omitted unless ``show_inherited`` /
``show_denied`` is True (see :func:`get_nsf_folder_access`).
"""
folder = _nsf_view(vault).get_folder(folder_uid)
if folder is None:
raise NsfError(f'NSF folder not found: {folder_uid}')
Expand Down Expand Up @@ -445,7 +449,11 @@ def get_nsf_record_detail(
include_access: bool = True,
show_inherited: bool = False,
show_denied: bool = False) -> Dict[str, Any]:
"""Record detail payload for ``nsf-get`` (record branch)."""
"""Record detail payload for ``nsf-get`` (record branch).

Inherited / denied accessors are omitted unless ``show_inherited`` /
``show_denied`` is True (see :func:`get_nsf_record_accesses`).
"""
meta = load_nsf_record_metadata(vault, record_uid)
entry = _nsf_view(vault).get_record(record_uid)
result: Dict[str, Any] = {
Expand Down Expand Up @@ -478,7 +486,11 @@ def get_nsf_item(
include_access: bool = True,
show_inherited: bool = False,
show_denied: bool = False) -> Dict[str, Any]:
"""Resolve and return folder or record detail."""
"""Resolve and return folder or record detail.

Inherited / denied accessors are omitted unless ``show_inherited`` /
``show_denied`` is True.
"""
folder_uid = resolve_nsf_folder_uid(vault, uid_or_title)
if folder_uid:
return {'item_type': 'folder', **get_nsf_folder_detail(
Expand Down Expand Up @@ -1047,7 +1059,14 @@ def get_nsf_record_accesses(
record_uids: Iterable[str],
show_inherited: bool = False,
show_denied: bool = False) -> Dict[str, Any]:
"""``vault/records/v3/details/access``, chunked at 100 UIDs per request."""
"""``vault/records/v3/details/access``, chunked at 100 UIDs per request.

By default only direct, non-denied rows are returned. Pass
``show_inherited=True`` and/or ``show_denied=True`` to include inherited /
denied rows. Code that reasons about effective access (permission checks,
sharing, revocation) must pass both as True, otherwise it sees an
incomplete picture.
"""
uids = [resolve_nsf_record_uid(vault, u) or u for u in record_uids]
uids = [u for u in uids if u]
if not uids:
Expand All @@ -1066,7 +1085,7 @@ def get_nsf_record_accesses(
for ra in rs.recordAccesses:
d = ra.data
ai = ra.accessorInfo
if (not show_inherited and d.inherited) or not (show_denied and d.deniedAccess):
if not _include_access_row(bool(d.inherited), bool(d.deniedAccess), show_inherited, show_denied):
continue
ao = {
'record_uid': utils.base64_url_encode(d.recordUid),
Expand Down Expand Up @@ -1113,15 +1132,29 @@ def _resolve_uid_to_username(vault: VaultOnline, uid_b64: str) -> Optional[str]:
"""Safety cap on continuation-token pages consumed per UID chunk."""


def _parse_folder_accessor(vault: VaultOnline, a: Any, show_inherited: bool = False, show_denied: bool = False) -> Dict[str, Any]:
def _include_access_row(inherited: bool, denied: bool, show_inherited: bool, show_denied: bool) -> bool:
"""Shared inherited/denied filter for folder and record access rows."""
if inherited and not show_inherited:
return False
if denied and not show_denied:
return False
return True


def _parse_folder_accessor(
vault: VaultOnline,
a: Any,
show_inherited: bool = False,
show_denied: bool = False) -> Optional[Dict[str, Any]]:
"""Parse one folder accessor row, or return None when it is filtered out."""
if not _include_access_row(bool(a.inherited), bool(a.deniedAccess), show_inherited, show_denied):
return None
auid = utils.base64_url_encode(a.accessTypeUid)
at = folder_pb2.AccessType.Name(a.accessType)
rt = folder_pb2.AccessRoleType.Name(a.accessRoleType)
username = None
if at == 'AT_USER':
username = _resolve_uid_to_username(vault, auid)
if (not show_inherited and a.inherited) or (not show_denied and a.deniedAccess):
return {}
ai = {
'accessor_uid': auid, 'access_type': at, 'role': rt,
'access_role_type': int(a.accessRoleType),
Expand Down Expand Up @@ -1154,7 +1187,10 @@ def _parse_folder_accessor(vault: VaultOnline, a: Any, show_inherited: bool = Fa


def _fetch_nsf_folder_access_chunk(
vault: VaultOnline, chunk: List[str], show_inherited: bool = False, show_denied: bool = False) -> Dict[str, Dict[str, Any]]:
vault: VaultOnline,
chunk: List[str],
show_inherited: bool = False,
show_denied: bool = False) -> Dict[str, Dict[str, Any]]:
"""Fetch access details for one <=100-UID chunk, fully draining continuation pages."""
accumulated: Dict[str, Dict[str, Any]] = {}
continuation_token = None
Expand All @@ -1179,7 +1215,11 @@ def _fetch_nsf_folder_access_chunk(
'message': err.message},
'success': False}
else:
accessors = [_parse_folder_accessor(vault, a, show_inherited, show_denied) for a in fr.accessors]
accessors = []
for a in fr.accessors:
parsed = _parse_folder_accessor(vault, a, show_inherited, show_denied)
if parsed is not None:
accessors.append(parsed)
entry = accumulated.setdefault(
fuid, {'folder_uid': fuid, 'accessors': [], 'success': True})
entry['accessors'].extend(accessors)
Expand All @@ -1197,6 +1237,12 @@ def get_nsf_folder_access(
show_denied: bool = False) -> Dict[str, Any]:
"""``vault/folders/v3/access``, chunked at 100 UIDs per request.

By default only direct, non-denied rows are returned. Pass
``show_inherited=True`` and/or ``show_denied=True`` to include inherited /
denied rows. Code that reasons about effective access (permission checks,
sharing, revocation) must pass both as True, otherwise it sees an
incomplete picture.

Fully drains continuation-token pagination within each chunk before
moving to the next, so the returned ``results`` are always complete —
there is no caller-facing ``has_more``/``continuation_token`` to track.
Expand All @@ -1217,7 +1263,7 @@ def get_nsf_folder_access(
return {'results': results}


def load_nsf_access_details(vault: VaultOnline, load_folder: bool = False, load_record: bool = False, show_inherited: bool = False, show_denied: bool = False) -> Dict[str, int]:
def load_nsf_access_details(vault: VaultOnline, load_folder: bool = False, load_record: bool = False) -> Dict[str, int]:
"""Fetch access details for every NSF folder/record and cache them on ``vault.nsf_data``.

Scans all folder and record UIDs currently known to the NSF cache
Expand All @@ -1226,6 +1272,9 @@ def load_nsf_access_details(vault: VaultOnline, load_folder: bool = False, load_
the results back onto ``vault.nsf_data`` — folders keyed by folder UID (the
full result entry returned by :func:`get_nsf_folder_access`), records keyed
by record UID (the list of accessor rows for that record).

The cache always holds the complete access state, including inherited and
denied rows; filter at read time if only direct access is wanted.
"""
view = _nsf_view(vault)
if not load_folder and not load_record:
Expand All @@ -1238,7 +1287,7 @@ def load_nsf_access_details(vault: VaultOnline, load_folder: bool = False, load_
loaded = {'folders': 0, 'records': 0}

if folder_uid_list:
access = get_nsf_folder_access(vault, folder_uid_list, show_inherited=show_inherited, show_denied=show_denied)
access = get_nsf_folder_access(vault, folder_uid_list, show_inherited=True, show_denied=True)
for fr in access.get('results') or []:
fuid = fr.get('folder_uid')
if not fuid:
Expand All @@ -1247,7 +1296,7 @@ def load_nsf_access_details(vault: VaultOnline, load_folder: bool = False, load_
loaded['folders'] += 1

if record_uid_list:
access = get_nsf_record_accesses(vault, record_uid_list, show_inherited=show_inherited, show_denied=show_denied)
access = get_nsf_record_accesses(vault, record_uid_list, show_inherited=True, show_denied=True)
by_uid: Dict[str, List[Dict[str, Any]]] = {}
for ao in access.get('record_accesses') or []:
by_uid.setdefault(ao['record_uid'], []).append(ao)
Expand Down
Loading
Loading