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
8 changes: 8 additions & 0 deletions KeeperSdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,9 +580,17 @@ export {
transferNestedShareRecords,
formatTransferNestedShareRecordResults,
NSF_RECORD_PERMISSION_ROLES,
NSFShareRoleName,
NsfShareCommandName,
NSF_SHARE_EXPIRATION_NEVER,
NsfFolderShareAction,
NsfFolderShareActionTaken,
NsfRecordShareAction,
NsfRecordShareActionTaken,
NsfRecordPermissionAction,
NsfResultStatus,
NsfTransferApiStatus,
NsfPermissionFailureCode,
collectNsfRecordUidsInFolder,
updateNestedShareRecordPermissions,
buildNsfRecordPermissionPlan,
Expand Down
8 changes: 8 additions & 0 deletions KeeperSdk/src/nestedShareFolders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ export {
NsfRemoveFolderOperation,
GetNsfRecordDetailsFormat,
NsfFolderShareAction,
NsfFolderShareActionTaken,
NsfRecordShareAction,
NsfRecordShareActionTaken,
NsfRecordPermissionAction,
NsfResultStatus,
NsfTransferApiStatus,
NsfPermissionFailureCode,
NSF_ACCESS_ROLE_LABELS,
resolveRecordPermissionRole,
toNsfAccessRoleLabel,
Expand Down Expand Up @@ -170,7 +175,10 @@ export {
NSF_FOLDER_COLORS,
NSF_MAX_RECORD_BATCH,
MIN_SHARE_EXPIRATION_MS,
NSF_SHARE_EXPIRATION_NEVER,
TeamGetKeysResponseKeyType,
NSFShareRoleName,
NsfShareCommandName,
} from './nsfConstants'
export type { NsfFolderColor } from './nsfConstants'

Expand Down
61 changes: 41 additions & 20 deletions KeeperSdk/src/nestedShareFolders/nsfConstants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Folder } from '@keeper-security/keeperapi'
import { KeeperSdkError, ResultCodes } from '../utils'

export const NSF_LEGACY_RECORD_MSG =
"Record '{0}' is a legacy vault record. Nested Share Folder commands operate only on Nested Share Records."
Expand Down Expand Up @@ -86,41 +87,58 @@ export const NSF_STRUCTURED_SUBKEYS: Record<string, ReadonlySet<string>> = {
phone: new Set(['number', 'region', 'type', 'ext']),
}

export enum NSFShareRoleName {
Contributor = 'contributor',
Viewer = 'viewer',
ShareManager = 'share-manager',
ContentManager = 'content-manager',
ContentShareManager = 'content-share-manager',
FullManager = 'full-manager',
Unresolved = 'unresolved',
}

export const NSF_RECORD_PERMISSION_ROLES = [
'viewer',
'share-manager',
'content-manager',
'content-share-manager',
'full-manager',
NSFShareRoleName.Viewer,
NSFShareRoleName.ShareManager,
NSFShareRoleName.ContentManager,
NSFShareRoleName.ContentShareManager,
NSFShareRoleName.FullManager,
] as const
export type NsfRecordPermissionRole = (typeof NSF_RECORD_PERMISSION_ROLES)[number]

export const NSF_RECORD_PERMISSION_ROLE_LABELS: Record<number, string> = {
[Folder.AccessRoleType.NAVIGATOR]: 'contributor',
[Folder.AccessRoleType.REQUESTOR]: 'contributor',
[Folder.AccessRoleType.VIEWER]: 'viewer',
[Folder.AccessRoleType.SHARED_MANAGER]: 'share-manager',
[Folder.AccessRoleType.CONTENT_MANAGER]: 'content-manager',
[Folder.AccessRoleType.CONTENT_SHARE_MANAGER]: 'content-share-manager',
[Folder.AccessRoleType.MANAGER]: 'full-manager',
[Folder.AccessRoleType.UNRESOLVED]: 'unresolved',
[Folder.AccessRoleType.NAVIGATOR]: NSFShareRoleName.Contributor,
[Folder.AccessRoleType.REQUESTOR]: NSFShareRoleName.Contributor,
[Folder.AccessRoleType.VIEWER]: NSFShareRoleName.Viewer,
[Folder.AccessRoleType.SHARED_MANAGER]: NSFShareRoleName.ShareManager,
[Folder.AccessRoleType.CONTENT_MANAGER]: NSFShareRoleName.ContentManager,
[Folder.AccessRoleType.CONTENT_SHARE_MANAGER]: NSFShareRoleName.ContentShareManager,
[Folder.AccessRoleType.MANAGER]: NSFShareRoleName.FullManager,
[Folder.AccessRoleType.UNRESOLVED]: NSFShareRoleName.Unresolved,
}

export const NSF_RECORD_PERMISSION_ROLE_MAP: Record<string, Folder.AccessRoleType> = {
contributor: Folder.AccessRoleType.REQUESTOR,
[NSFShareRoleName.Contributor]: Folder.AccessRoleType.REQUESTOR,
requestor: Folder.AccessRoleType.REQUESTOR,
viewer: Folder.AccessRoleType.VIEWER,
'share-manager': Folder.AccessRoleType.SHARED_MANAGER,
[NSFShareRoleName.Viewer]: Folder.AccessRoleType.VIEWER,
[NSFShareRoleName.ShareManager]: Folder.AccessRoleType.SHARED_MANAGER,
share_manager: Folder.AccessRoleType.SHARED_MANAGER,
shared_manager: Folder.AccessRoleType.SHARED_MANAGER,
'content-manager': Folder.AccessRoleType.CONTENT_MANAGER,
[NSFShareRoleName.ContentManager]: Folder.AccessRoleType.CONTENT_MANAGER,
content_manager: Folder.AccessRoleType.CONTENT_MANAGER,
'content-share-manager': Folder.AccessRoleType.CONTENT_SHARE_MANAGER,
[NSFShareRoleName.ContentShareManager]: Folder.AccessRoleType.CONTENT_SHARE_MANAGER,
content_share_manager: Folder.AccessRoleType.CONTENT_SHARE_MANAGER,
'full-manager': Folder.AccessRoleType.MANAGER,
[NSFShareRoleName.FullManager]: Folder.AccessRoleType.MANAGER,
full_manager: Folder.AccessRoleType.MANAGER,
}

export const NSF_SHARE_EXPIRATION_NEVER = 'never'

export enum NsfShareCommandName {
FolderShare = 'nsf-share-folder',
RecordShare = 'nsf-share-record',
}

export const NSF_SHARE_BATCH_SIZE = 200

export enum TeamGetKeysResponseKeyType {
Expand Down Expand Up @@ -236,7 +254,10 @@ const NSF_FOLDER_ROLE_PERMISSIONS: Record<number, FolderRolePermissionFlags> = {
export function getFolderPermissionsForRole(roleType: Folder.AccessRoleType): Folder.IFolderPermissions {
const flags = NSF_FOLDER_ROLE_PERMISSIONS[roleType]
if (!flags) {
throw new Error(`Unknown folder access role type: ${roleType}`)
throw new KeeperSdkError(
`Unknown folder access role type: ${roleType}`,
ResultCodes.NSF_SHARE_FAILED
)
}
return Folder.FolderPermissions.create(flags)
}
77 changes: 70 additions & 7 deletions KeeperSdk/src/nestedShareFolders/nsfHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
import {
Folder,
Records,
getFolderAccessMessage,
getRecordAccessMessage,
getShareObjectsMessage,
normal64Bytes,
Expand All @@ -36,6 +37,8 @@ import {
NSF_TLA_EXPIRATION_TOLERANCE_MS,
NSF_SHARE_EXPIRATION_RE,
NSF_SHARE_EXPIRATION_UNIT_MS,
NSF_SHARE_EXPIRATION_NEVER,
NsfShareCommandName,
} from './nsfConstants'
import { NsfAccessRoleLabel, NSF_ACCESS_ROLE_LABELS, type ParseShareExpirationInput } from './nsfTypes'

Expand Down Expand Up @@ -485,6 +488,7 @@ type FolderPermissionFlag =
| 'canDelete'
| 'canUpdateAccess'
| 'canEditRecords'
| 'canUpdateSetting'
| 'canChangeOwnership'

function hasFolderPermission(
Expand Down Expand Up @@ -807,7 +811,6 @@ export function resolveAccessUsername(
return accessTypeUid
}


export async function loadShareUserMap(auth: Auth, storage: InMemoryStorage): Promise<Map<string, string>> {
const map = new Map<string, string>()

Expand Down Expand Up @@ -837,7 +840,6 @@ export async function loadShareUserMap(auth: Auth, storage: InMemoryStorage): Pr
return map
}


export function isFolderOwnerAccessor(
folder: DKdFolder,
entry: DKdFolderAccess,
Expand Down Expand Up @@ -944,7 +946,7 @@ export function checkFolderEditPermission(
username: string,
accountUid: Uint8Array
): void {
if (hasFolderPermission(storage, folderUid, username, accountUid, 'canEditRecords')) return
if (hasFolderPermission(storage, folderUid, username, accountUid, 'canUpdateSetting')) return
throw new KeeperSdkError(
'You do not have permission to edit this folder.',
ResultCodes.NSF_PERMISSION_DENIED
Expand Down Expand Up @@ -1001,6 +1003,67 @@ export function findFolderAccessEntry(
)
}

function toFolderAccessEntry(folderUid: string, accessor: Folder.IFolderAccessData): DKdFolderAccess {
return {
kind: 'keeper_drive_folder_access',
accessUid: `${folderUid}:${webSafe64FromBytes(accessor.accessTypeUid!)}`,
folderUid,
accessTypeUid: webSafe64FromBytes(accessor.accessTypeUid!),
accessType: accessor.accessType!,
accessRoleType: accessor.accessRoleType!,
permission: accessor.permissions ?? {},
inherited: accessor.inherited ?? undefined,
hidden: accessor.hidden ?? undefined,
}
}

export async function fetchLiveFolderAccessEntries(
auth: Auth,
folderUid: string
): Promise<DKdFolderAccess[]> {
try {
const response = await auth.executeRest(
getFolderAccessMessage({ folderUid: [normal64Bytes(folderUid)] })
)
const result = response.folderAccessResults?.find(
(entry) => entry.folderUid?.length && webSafe64FromBytes(entry.folderUid) === folderUid
)
return (result?.accessors ?? [])
.filter(
(accessor) =>
accessor.accessTypeUid?.length &&
accessor.accessType != null &&
accessor.accessRoleType != null
)
.map((accessor) => toFolderAccessEntry(folderUid, accessor))
} catch (err) {
throw new KeeperSdkError(
`Failed to fetch folder permissions for ${folderUid}: ${extractErrorMessage(err)}`,
ResultCodes.NSF_DETAILS_FAILED
)
}
}

export async function findFolderAccessEntryOrLive(
storage: InMemoryStorage,
auth: Auth,
folderUid: string,
accessTypeUid: string,
accessType: Folder.AccessType
): Promise<DKdFolderAccess | undefined> {
const fromStorage = findFolderAccessEntry(storage, folderUid, accessTypeUid, accessType)
if (fromStorage) return fromStorage

try {
const liveEntries = await fetchLiveFolderAccessEntries(auth, folderUid)
return liveEntries.find(
(entry) => entry.accessTypeUid === accessTypeUid && entry.accessType === accessType
)
} catch {
return undefined
}
}

export function collectExistingFolderShareTargets(
storage: InMemoryStorage,
folderUid: string,
Expand Down Expand Up @@ -1043,7 +1106,7 @@ export type NsfRecordAccessFlags = {
}

export function getNsfAccessRoleLabel(access: NsfRecordAccessFlags): string {
if (access.owner) return 'owner'
if (access.owner) return NsfAccessRoleLabel.Owner
return getNsfRecordPermissionRoleLabel(access.accessRoleType)
}

Expand Down Expand Up @@ -1211,7 +1274,7 @@ export function validateShareExpirationTimestamp(
}

export function parseShareExpiration(input: ParseShareExpirationInput): number | undefined {
const { expireAt, expireIn, expirationTimestamp, cmdName = 'nsf-share' } = input
const { expireAt, expireIn, expirationTimestamp, cmdName = NsfShareCommandName.FolderShare } = input
const expireAtValue = expireAt?.trim()
const expireInValue = expireIn?.trim()

Expand All @@ -1237,7 +1300,7 @@ export function parseShareExpiration(input: ParseShareExpirationInput): number |
const raw = expireAtValue || expireInValue
if (!raw) return undefined

if (raw.toLowerCase() === 'never') return -1
if (raw.toLowerCase() === NSF_SHARE_EXPIRATION_NEVER) return -1

if (expireAtValue) {
const normalized = raw.replace('Z', '+00:00')
Expand Down Expand Up @@ -1277,7 +1340,7 @@ export function parseShareExpiration(input: ParseShareExpirationInput): number |

export function parseShareExpirationValue(
value: string,
cmdName: string = 'nsf-share'
cmdName: string = NsfShareCommandName.FolderShare
): number | undefined {
const raw = value.trim()
if (!raw) return undefined
Expand Down
13 changes: 7 additions & 6 deletions KeeperSdk/src/nestedShareFolders/nsfRecordPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import type {
UpdateNsfRecordPermissionInput,
UpdateNsfRecordPermissionResult,
} from './nsfTypes'
import { NsfRecordPermissionAction } from './nsfTypes'
import { NsfPermissionFailureCode, NsfRecordPermissionAction } from './nsfTypes'

type NsfRecordAccessEntry = NsfLiveRecordAccessEntry

Expand Down Expand Up @@ -371,9 +371,10 @@ function computePermissionChanges(

const forbidden = new Set(forbiddenRecords)
const ownerFlags = new Map<string, boolean>()
const currentUserLower = currentUser.toLowerCase()

for (const access of accesses) {
if (access.accessorName === currentUser) {
if (access.accessorName.toLowerCase() === currentUserLower) {
ownerFlags.set(access.recordUid, access.owner || access.canUpdateAccess)
}
}
Expand All @@ -394,7 +395,7 @@ function computePermissionChanges(
if (!recordUids.has(recordUid) || access.owner) continue

const email = access.accessorName
if (!email || email === currentUser) continue
if (!email || email.toLowerCase() === currentUserLower) continue

const curRole = getNsfAccessRoleLabel(access)
const isInherited = access.inherited
Expand Down Expand Up @@ -550,7 +551,7 @@ export function formatNsfRecordPermissionPlan(plan: NsfRecordPermissionPlan): st
])
)
)
lines.push('ALERT!!!')
lines.push('WARNING: Review the planned changes carefully before confirming.')
lines.push('')
}

Expand All @@ -568,7 +569,7 @@ export function formatNsfRecordPermissionPlan(plan: NsfRecordPermissionPlan): st
])
)
)
lines.push('ALERT!!!')
lines.push('WARNING: Review the planned changes carefully before confirming.')
lines.push('')
}

Expand All @@ -584,7 +585,7 @@ function mapFailures(
.map((outcome) => ({
recordUid: outcome.recordUid,
email: outcome.email,
code: outcome.skipped ? 'skipped' : defaultCode,
code: outcome.skipped ? NsfPermissionFailureCode.Skipped : defaultCode,
message: outcome.message || 'Unknown error',
}))
}
Expand Down
Loading
Loading