UNPKG

cumulocity-cypress

Version:
367 lines (366 loc) 15.2 kB
import { to_array } from "../util"; import { wrapFetchResponse } from "../c8yclient"; import { expectSuccessfulDelete, maxPageSize } from "./helper"; /** * Creates a user with the specified global roles and optionally assigns applications. * * This function: * 1. Creates the user in Cumulocity * 2. Assigns the user to the specified global role groups * 3. Optionally assigns applications to the user (by name or IApplication object) * * @param client - The Cumulocity client instance * @param user - The user object to create (must include userName, email, etc.) * @param globalRoles - Array of global role names to assign to the user * @param applications - Optional array of application names (strings) or IApplication objects to assign * @returns Promise resolving to the created user result * * @throws Error if user creation fails or if roles/applications cannot be assigned * * @example * const userResult = await createUser( * client, * { userName: 'john.doe', email: 'john@example.com', password: 'SecurePass123!' }, * ['business'], * ['cockpit', 'devicemanagement'] * ); */ export async function createUser(client, user, globalRoles, applications) { const userResponse = await client.user.create(user); for (const role of globalRoles) { const groupResponse = await wrapFetchResponse(await client.core.fetch("/user/" + client.core.tenant + "/groupByName/" + role)); const childId = userResponse?.data?.self; const groupId = groupResponse?.data?.id; if (!childId || !groupId) { throw `Failed to add user ${childId} to group ${childId}.`; } await client.userGroup.addUserToGroup(groupId, childId); } const userId = userResponse.data.id; // Handle applications if provided if (applications && applications.length > 0) { const allApps = []; for (const app of applications) { if (typeof app === "string") { // Fetch application by name const applicationResponse = await wrapFetchResponse(await client.core.fetch(`/application/applicationsByName/${app}`, { headers: { accept: "application/vnd.com.nsn.cumulocity.applicationcollection+json", }, })); const applicationsData = applicationResponse.data?.applications || applicationResponse.data; if (!applicationsData || !Array.isArray(applicationsData)) { throw new Error(`Application ${app} not found. No or empty response.`); } const apps = applicationsData .map((a) => { if (typeof a === "string") { return { type: "HOSTED", id: a }; } else if (typeof a === "object" && a.id) { return { id: a.id, type: a.type || "HOSTED" }; } return undefined; }) .filter((a) => a !== undefined); allApps.push(...apps); } else if (typeof app === "object" && app.id) { allApps.push({ id: app.id, type: app.type || "HOSTED", }); } else { throw new Error("Invalid application format. Expected string (name) or IApplication object with id."); } } // Get user details and merge applications if (userId && allApps.length > 0) { const userDetailResponse = await client.user.detail(userId); const existingApps = userDetailResponse.data?.applications || []; // Merge with existing applications, avoiding duplicates by id const mergedApps = [...existingApps]; for (const app of allApps) { if (!mergedApps.find((existing) => existing.id === app.id)) { mergedApps.push(app); } } await client.user.update({ id: userId, applications: mergedApps }); } } return userResponse; } function isIdentifiedObject(user) { return (typeof user === "object" && (user.id != null || user.userName != null || user.displayName != null || user.self != null || user.email != null)); } function needsAllUsersFetch(users) { const userArray = to_array(users) ?? []; return (userArray.filter((u) => typeof u === "string" || typeof u === "function" || typeof u === "string" || (typeof u === "object" && u.userName == null && u.id == null)).length > 0); } /** * Deletes one or more users from Cumulocity. * * Supports multiple input formats: * - Single username string * - Single IUser object (matched by id, userName, displayName, self, or email) * - Array of usernames or IUser objects * - Filter function to select users to delete * * When an IUser object is provided, the function matches it against existing users using * any available identifying properties (id, userName, displayName, self, email). This allows * for flexible matching even with partial user objects. * * @param client - The Cumulocity client instance * @param user - Username(s), IUser object(s), or filter function to identify users to delete * @param options - Optional configuration * @param options.ignoreNotFound - If true (default), ignores 404 errors when user is not found * @returns Promise that resolves when all users are deleted * * @throws Error if user is missing required properties or if deletion fails (unless ignoreNotFound is true) * * @example * // Delete single user by username * await deleteUser(client, 'john.doe'); * * @example * // Delete multiple users * await deleteUser(client, ['user1', 'user2', 'user3']); * * @example * // Delete users matching a filter * await deleteUser(client, (user) => user.email?.includes('@example.com')); * * @example * // Delete user by partial IUser object * await deleteUser(client, { displayName: 'John Doe', email: 'john@example.com' }); */ export async function deleteUser(client, user, options) { if (!user) { throw new Error("Missing user argument. deleteUser() requires IUser object or username string."); } const userArray = to_array(user) ?? []; const ignoreNotFound = options?.ignoreNotFound ?? true; let allUsersResponse = undefined; if (needsAllUsersFetch(user)) { try { allUsersResponse = await client.user.list({ pageSize: maxPageSize, }); } catch (error) { throw new Error(`Failed to fetch list of users for list of usernames or filter function: ${error}`); } } let allUsers = undefined; if (typeof user === "function") { const fn = user; allUsers = allUsersResponse?.data.filter((userItem) => fn(userItem)); } else { allUsers = userArray.reduce((acc, u) => { if (typeof u === "string") { const lowerU = u.toLowerCase(); const foundUser = allUsersResponse?.data.find((userItem) => userItem.userName?.toLowerCase() === lowerU || userItem.id?.toLowerCase() === lowerU) ?? false; if (!foundUser) { if (ignoreNotFound) { return acc; } throw new Error(`User with username '${u}' not found.`); } acc.push(foundUser); } else if (typeof u === "object") { if (!isIdentifiedObject(u)) { throw new Error("IUser object must have at least one identifying property (id, userName, displayName, self, or email)."); } if (u.id != null || u.userName != null) { acc.push(u); return acc; } // If u is IUser object, match using fields available in u // Properties used for matching: id, userName, displayName, self, email const foundUser = allUsersResponse?.data.find((userItem) => { if (u.displayName && userItem.displayName !== u.displayName) return false; if (u.self && userItem.self !== u.self) return false; if (u.email && userItem.email?.toLowerCase() !== u.email.toLowerCase()) return false; return true; }) ?? false; if (!foundUser) { if (ignoreNotFound) { return acc; } const identifier = u.userName || u.email || u.displayName || u.id || "unknown"; throw new Error(`User with identifier '${identifier}' not found.`); } acc.push(foundUser); } return acc; }, []); } for (const user of allUsers ?? []) { try { const response = await client.user.delete(user.id ?? user.userName); expectSuccessfulDelete(response.res?.status || 204); } catch (error) { if (error?.res?.status && error?.res?.status !== 404) throw error; } } } /** * Assigns one or more global roles to a user. * * This function adds the user to the specified global role groups, granting them * the permissions associated with those roles. * * @param client - The Cumulocity client instance * @param username - Username string or IUser object (must have userName property) * @param roles - Array of global role names to assign to the user * @returns Promise that resolves when all roles are assigned * * @throws Error if username is missing, roles array is empty, or if role assignment fails * * @example * await assignUserRoles(client, 'john.doe', ['business', 'admins']); * * @example * const user = await client.user.detail('john.doe'); * await assignUserRoles(client, user.data, ['devicemanagement']); */ export async function assignUserRoles(client, username, roles) { const userIdentifier = typeof username === "object" && username.userName ? username.userName : username; if (!userIdentifier || (typeof username === "object" && !username.userName)) { throw new Error("Missing argument. Requiring IUser object with userName or username argument."); } if (!roles || roles.length === 0) { throw new Error("Missing argument. Requiring a string array with roles."); } const userResponse = await client.user.detail(userIdentifier); const childId = userResponse.data?.self; if (!childId) { throw new Error(`Failed to assign roles to user ${userIdentifier}. User data null or does not contain self linking.`); } for (const role of roles) { const groupResponse = await client.core.fetch(`/user/${client.core.tenant}/groupByName/${role}`); const groupId = groupResponse.data?.id; if (!childId || !groupId) { throw new Error(`Failed to add user ${childId} to group ${groupId}.`); } await client.userGroup.addUserToGroup(groupId, childId); } } /** * Removes all global roles currently assigned to a user. * * This function removes the user from all global role groups, effectively * revoking all role-based permissions. * * @param client - The Cumulocity client instance * @param username - Username string or IUser object (must have userName property) * @returns Promise that resolves when all roles are removed * * @throws Error if username is missing or if role removal fails * * @example * await clearUserRoles(client, 'john.doe'); * * @example * const user = await client.user.detail('john.doe'); * await clearUserRoles(client, user.data); */ export async function clearUserRoles(client, username) { const userIdentifier = typeof username === "object" && username.userName ? username.userName : username; if (!userIdentifier || (typeof username === "object" && !username.userName)) { throw new Error("Missing argument. Requiring IUser object with userName or username argument."); } const response = await client.user.detail(userIdentifier); const assignedRoles = response.data.groups?.references; if (!assignedRoles || assignedRoles.length === 0) { return; } for (const assignedRole of assignedRoles) { await client.userGroup.removeUserFromGroup(assignedRole.group.id, userIdentifier); } } /** * Generates a secure random password with mixed case letters, numbers, and special characters. * * The password includes: * - Uppercase and lowercase letters (50% chance for each letter) * - Numbers (from timestamp) * - Special characters (!@#$%^&*()) * * @param length - The desired length of the password (default: 28, minimum: 8) * @returns A randomly generated password string * * @example * const password = generatePassword(); * // Returns something like: "2Kl9j8Gh!4m2@x7n#5p3q8r9" * * @example * const shortPassword = generatePassword(12); * // Returns a 12-character password */ export function generatePassword(length = 28) { const minLength = 8; const targetLength = Math.max(length, minLength); const timestamp = Date.now().toString(36); const random1 = Math.random().toString(36).substring(2); const random2 = Math.random().toString(36).substring(2); // Build password ensuring minimum length before randomization const base = `${timestamp}-${random1}-${random2}`.substring(0, targetLength); const specialChars = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"]; return randomizePassword(base, specialChars); } /** * Converts letters to uppercase by 50% chance and replaces hyphens with special characters. * Ensures at least one uppercase character is included. * * @param text - The base text to randomize * @param replaceOptions - Array of special characters to use for replacing hyphens * @returns The randomized password string * * @internal */ function randomizePassword(text, replaceOptions) { let randomizedString = ""; // make sure that at least one char is uppercase by using isFirst flag let isFirst = true; for (let i = 0; i < text.length; i++) { const character = text.charAt(i); if (/^[a-zA-Z]$/.test(character)) { const transformedCharacter = isFirst || Math.random() < 0.5 ? character.toUpperCase() : character.toLowerCase(); randomizedString += transformedCharacter; isFirst = false; continue; } if (character === "-") { const index = Math.floor(Math.random() * replaceOptions.length); randomizedString += replaceOptions[index]; continue; } randomizedString += character; } return randomizedString; }