export function updateOrgMemberGroup()

in controlplane/src/core/bufservices/user/updateOrgMemberGroup.ts [17:236]


export function updateOrgMemberGroup(
  opts: RouterOptions,
  req: UpdateOrgMemberGroupRequest,
  ctx: HandlerContext,
): Promise<PlainMessage<UpdateOrgMemberGroupResponse>> {
  let logger = getLogger(ctx, opts.logger);

  return handleError<PlainMessage<UpdateOrgMemberGroupResponse>>(ctx, logger, async () => {
    const authContext = await opts.authenticator.authenticate(ctx.requestHeader);
    logger = enrichLogger(ctx, logger, authContext);

    if (authContext.organizationDeactivated || !authContext.rbac.isOrganizationAdmin) {
      throw new UnauthorizedError();
    }

    return opts.db.transaction(async (tx) => {
      const orgRepo = new OrganizationRepository(logger, tx, opts.billingDefaultPlanId);
      const orgGroupRepo = new OrganizationGroupRepository(tx);
      const oidcRepo = new OidcRepository(tx);
      const auditLogRepo = new AuditLogRepository(tx);

      const org = await orgRepo.byId(authContext.organizationId);
      if (!org) {
        return {
          response: {
            code: EnumStatusCode.ERR_NOT_FOUND,
            details: `Organization not found`,
          },
        };
      }

      // Fetch the organization member from the database
      const orgMember = await orgRepo.getOrganizationMember({
        organizationID: authContext.organizationId,
        userID: req.orgMemberUserID,
      });

      if (!orgMember) {
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: 'User is not a part of this organization.',
          },
        };
      }

      // Ensure that the organization member has not signed in with SSO
      const provider = await oidcRepo.getOidcProvider({ organizationId: authContext.organizationId });
      if (provider) {
        // checking if the user has logged in using the sso
        const ssoUser = await opts.keycloakClient.client.users.find({
          realm: opts.keycloakRealm,
          email: orgMember.email,
          exact: true,
          idpAlias: provider.alias,
        });

        if (ssoUser.length > 0) {
          return {
            response: {
              code: EnumStatusCode.ERR,
              details: 'User has logged in using the OIDC provider. Please update the group using the provider.',
            },
          };
        }
      }

      // Retrieve the Keycloak user
      await opts.keycloakClient.authenticateClient();

      const users = await opts.keycloakClient.client.users.find({
        realm: opts.keycloakRealm,
        email: orgMember.email,
        exact: true,
      });

      if (users.length === 0) {
        // The user doesn't exist in Keycloak
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: 'User does not exist.',
          },
        };
      }

      // Load all the group the member should be part of from the database
      const groups: OrganizationGroupDTO[] = [];
      const uniqueGroupIds = [...new Set(req.groups)];
      groups.push(
        ...(await orgGroupRepo.byIds({
          organizationId: authContext.organizationId,
          groupIds: uniqueGroupIds,
        })),
      );

      if (groups.length !== uniqueGroupIds.length) {
        // One or more of the submitted groups
        return {
          response: {
            code: EnumStatusCode.ERR_NOT_FOUND,
            details: `One of the submitted groups is not part of this organization`,
          },
        };
      }

      for (const groupId of new Set(req.groups)) {
        const orgGroup = await orgGroupRepo.byId({ organizationId: authContext.organizationId, groupId });
        if (!orgGroup) {
          // The group doesn't exist for the organization
          return {
            response: {
              code: EnumStatusCode.ERR_NOT_FOUND,
              details: `One of the submitted groups is not part of this organization`,
            },
          };
        }

        groups.push(orgGroup);
      }

      // Figure out which groups we need to remove the user from and to add the user to
      const newGroups = new Set(groups.filter(Boolean).map((group) => group.groupId));

      if (newGroups.size === 0) {
        return {
          response: {
            code: EnumStatusCode.ERR,
            details: 'The organization member must have at least one group',
          },
        };
      }

      const existingGroups = new Set(orgMember.rbac.groups.map((group) => group.groupId));

      const groupsToAddTo = newGroups.difference(existingGroups);
      const groupsToRemoveFrom = existingGroups.difference(newGroups);

      if (groupsToAddTo.size === 0 && groupsToRemoveFrom.size === 0) {
        // We don't need to remove or add the group from/to any group
        return {
          response: {
            code: EnumStatusCode.OK,
          },
        };
      }

      // Remove the member from removed groups
      for (const groupId of groupsToRemoveFrom) {
        const group = orgMember.rbac.groups.find((g) => g.groupId === groupId);
        if (!group?.kcGroupId) {
          // The group hasn't been linked to Keycloak, skip
          continue;
        }

        await opts.keycloakClient.client.users.delFromGroup({
          id: users[0].id!,
          realm: opts.keycloakRealm,
          groupId: group.kcGroupId,
        });

        await auditLogRepo.addAuditLog({
          organizationId: authContext.organizationId,
          organizationSlug: authContext.organizationSlug,
          auditAction: 'member_group.removed',
          action: 'removed',
          actorId: authContext.userId,
          auditableDisplayName: group.name,
          auditableType: 'member_group',
          actorDisplayName: authContext.userDisplayName,
          apiKeyName: authContext.apiKeyName,
          targetId: orgMember.userID,
          targetType: 'user',
          targetDisplayName: orgMember.email,
          actorType: authContext.auth === 'api_key' ? 'api_key' : 'user',
        });
      }

      // Add the member to added groups
      for (const groupId of groupsToAddTo) {
        const group = groups.find((g) => g.groupId === groupId);
        if (!group?.kcGroupId) {
          // The group hasn't been linked to Keycloak, skip
          continue;
        }

        await opts.keycloakClient.client.users.addToGroup({
          id: users[0].id!,
          realm: opts.keycloakRealm,
          groupId: group.kcGroupId,
        });

        await auditLogRepo.addAuditLog({
          organizationId: authContext.organizationId,
          organizationSlug: authContext.organizationSlug,
          auditAction: 'member_group.added',
          action: 'added',
          actorId: authContext.userId,
          auditableDisplayName: group.name,
          auditableType: 'member_group',
          actorDisplayName: authContext.userDisplayName,
          apiKeyName: authContext.apiKeyName,
          targetId: orgMember.userID,
          targetType: 'user',
          targetDisplayName: orgMember.email,
          actorType: authContext.auth === 'api_key' ? 'api_key' : 'user',
        });
      }

      // Update the groups from the member
      await orgRepo.updateMemberGroups({ orgMemberID: orgMember.orgMemberID, groups: [...newGroups] });

      return {
        response: {
          code: EnumStatusCode.OK,
        },
      };
    });
  });
}