Ctrl K
ComunidadGuías…

server/commands/accountProvisioner.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import Router from "@koa/router";
import { faker } from "@faker-js/faker";
import { randomUUID } from "node:crypto";
import { errToString } from "@shared/utils/error";
import { Event, TeamDomain } from "@server/models";
import Collection from "@server/models/Collection";
import UserAuthentication from "@server/models/UserAuthentication";
import AuthenticationHelper from "@server/models/helpers/AuthenticationHelper";
import SyncUserGroupsTask from "@server/queues/tasks/SyncUserGroupsTask";
import {
  buildUser,
  buildTeam,
  buildAdmin,
  buildInvite,
  buildSubdomain,
} from "@server/test/factories";
import { mockTaskSchedule, setSelfHosted } from "@server/test/support";
import { Hook } from "@server/utils/PluginManager";
import accountProvisioner from "./accountProvisioner";
import { createContext } from "@server/context";

describe("accountProvisioner", () => {
  const ip = faker.internet.ip();
  const ctx = createContext({ ip });
  const schedule = mockTaskSchedule();

  describe("invites", () => {
    it("should record invite_accepted against the invited user and team", async () => {
      const team = await buildTeam();
      const invite = await buildInvite({ teamId: team.id });
      const providers = await team.$get("authenticationProviders");

      const result = await accountProvisioner(ctx, {
        user: {
          name: invite.name,
          email: invite.email!,
          avatarUrl: invite.avatarUrl,
          emailVerified: true,
        },
        team: {
          teamId: team.id,
          name: team.name,
          avatarUrl: team.avatarUrl,
          subdomain: faker.internet.domainWord(),
        },
        authenticationProvider: {
          name: providers[0].name,
          providerId: providers[0].providerId,
        },
        authentication: {
          providerId: randomUUID(),
          accessToken: "123",
          scopes: ["read"],
        },
      });

      expect(result.user.id).toEqual(invite.id);
      expect(result.isNewUser).toEqual(true);

      const event = await Event.findLatest({
        name: "users.invite_accepted",
        userId: invite.id,
      });
      expect(event?.teamId).toEqual(team.id);
      expect(event?.actorId).toEqual(invite.id);
      expect(event?.ip).toEqual(ip);
    });

    it("should not attribute invite_accepted to the user of an existing session", async () => {
      const team = await buildTeam();
      const invite = await buildInvite({ teamId: team.id });
      const providers = await team.$get("authenticationProviders");
      const sessionUser = await buildUser();

      await accountProvisioner(createContext({ ip, user: sessionUser }), {
        user: {
          name: invite.name,
          email: invite.email!,
          avatarUrl: invite.avatarUrl,
          emailVerified: true,
        },
        team: {
          teamId: team.id,
          name: team.name,
          avatarUrl: team.avatarUrl,
          subdomain: faker.internet.domainWord(),
        },
        authenticationProvider: {
          name: providers[0].name,
          providerId: providers[0].providerId,
        },
        authentication: {
          providerId: randomUUID(),
          accessToken: "123",
          scopes: ["read"],
        },
      });

      const event = await Event.findLatest({
        name: "users.invite_accepted",
        userId: invite.id,
      });
      expect(event?.teamId).toEqual(team.id);
      expect(event?.actorId).toEqual(invite.id);
    });
  });

  it("should not record invite_accepted for a user that was not invited", async () => {
    const team = await buildTeam();
    const providers = await team.$get("authenticationProviders");

    const result = await accountProvisioner(ctx, {
      user: {
        name: faker.person.fullName(),
        email: faker.internet.email().toLowerCase(),
        emailVerified: true,
      },
      team: {
        teamId: team.id,
        name: team.name,
        avatarUrl: team.avatarUrl,
        subdomain: faker.internet.domainWord(),
      },
      authenticationProvider: {
        name: providers[0].name,
        providerId: providers[0].providerId,
      },
      authentication: {
        providerId: randomUUID(),
        accessToken: "123",
        scopes: ["read"],
      },
    });

    expect(result.isNewUser).toEqual(true);
    await expect(
      Event.findLatest({
        name: "users.invite_accepted",
        userId: result.user.id,
      })
    ).resolves.toBeNull();
  });

  describe("group sync", () => {
    async function provisionExistingUser({
      groupSyncEnabled,
    }: {
      groupSyncEnabled: boolean;
    }) {
      const existingTeam = await buildTeam();
      const providers = await existingTeam.$get("authenticationProviders");
      const authenticationProvider = providers[0];
      await authenticationProvider.update({
        settings: { groupSyncEnabled },
      });
      const existing = await buildUser({ teamId: existingTeam.id });
      const authentications = await existing.$get("authentications");
      const authentication = authentications[0];

      await accountProvisioner(ctx, {
        user: {
          name: existing.name,
          email: existing.email!,
          avatarUrl: existing.avatarUrl,
        },
        team: {
          name: existingTeam.name,
          avatarUrl: existingTeam.avatarUrl,
          subdomain: faker.internet.domainWord(),
        },
        authenticationProvider: {
          name: authenticationProvider.name,
          providerId: authenticationProvider.providerId,
        },
        authentication: {
          providerId: authentication.providerId,
          accessToken: "123",
          scopes: ["read"],
        },
      });

      return authentication;
    }

    it("should schedule a group sync when enabled for the provider", async () => {
      const authentication = await provisionExistingUser({
        groupSyncEnabled: true,
      });

      const calls = schedule.mock.calls.filter(
        (_, index) =>
          schedule.mock.instances[index] instanceof SyncUserGroupsTask
      );
      expect(calls).toEqual([
        [
          { userAuthenticationId: authentication.id },
          { jobId: `sync-user-groups:${authentication.id}` },
        ],
      ]);
    });

    it("should not schedule a group sync when disabled for the provider", async () => {
      await provisionExistingUser({ groupSyncEnabled: false });

      const scheduled = schedule.mock.instances.some(
        (instance) => instance instanceof SyncUserGroupsTask
      );
      expect(scheduled).toEqual(false);
    });
  });

  describe("hosted", () => {
    it("should create a new user and team", async () => {
      const email = faker.internet.email();
      const { user, team, isNewTeam, isNewUser } = await accountProvisioner(
        ctx,
        {
          user: {
            name: "Jenny Tester",
            email,
            avatarUrl: faker.image.avatar(),
          },
          team: {
            name: "New workspace",
            avatarUrl: faker.image.avatar(),
            subdomain: buildSubdomain(),
          },
          authenticationProvider: {
            name: "google",
            providerId: faker.internet.domainName(),
          },
          authentication: {
            providerId: randomUUID(),
            accessToken: "123",
            scopes: ["read"],
          },
        }
      );
      const authentications = await user.$get("authentications");
      const auth = authentications[0];
      expect(auth.accessToken).toEqual("123");
      expect(auth.scopes.length).toEqual(1);
      expect(auth.scopes[0]).toEqual("read");
      expect(team.name).toEqual("New workspace");
      expect(user.email).toEqual(email);
      expect(isNewUser).toEqual(true);
      expect(isNewTeam).toEqual(true);
      const collectionCount = await Collection.count({
        where: {
          teamId: team.id,
        },
      });
      expect(collectionCount).toEqual(1);
    });

    it("should record the request ip on onboarding events", async () => {
      const { team } = await accountProvisioner(ctx, {
        user: {
          name: "Jenny Tester",
          email: faker.internet.email(),
          avatarUrl: faker.image.avatar(),
        },
        team: {
          name: "New workspace",
          avatarUrl: faker.image.avatar(),
          subdomain: buildSubdomain(),
        },
        authenticationProvider: {
          name: "google",
          providerId: faker.internet.domainName(),
        },
        authentication: {
          providerId: randomUUID(),
          accessToken: "123",
          scopes: ["read"],
        },
      });

      const events = await Event.findAll({
        where: {
          teamId: team.id,
          name: ["collections.create", "documents.create"],
        },
      });
      expect(events.length).toBeGreaterThan(0);
      events.forEach((event) => {
        expect(event.ip).toEqual(ip);
      });
    });

    it("should update existing user and authentication", async () => {
      const existingTeam = await buildTeam();
      const providers = await existingTeam.$get("authenticationProviders");
      const authenticationProvider = providers[0];
      const existing = await buildUser({
        teamId: existingTeam.id,
      });
      const authentications = await existing.$get("authentications");
      const authentication = authentications[0];
      const newEmail = faker.internet.email();
      const { user, isNewUser, isNewTeam } = await accountProvisioner(ctx, {
        user: {
          name: existing.name,
          email: newEmail,
          emailVerified: true,
          avatarUrl: existing.avatarUrl,
        },
        team: {
          name: existingTeam.name,
          avatarUrl: existingTeam.avatarUrl,
          subdomain: buildSubdomain(),
        },
        authenticationProvider: {
          name: authenticationProvider.name,
          providerId: authenticationProvider.providerId,
        },
        authentication: {
          providerId: authentication.providerId,
          accessToken: "123",
          scopes: ["read"],
        },
      });
      const auth = await UserAuthentication.findByPk(authentication.id);
      expect(auth?.accessToken).toEqual("123");
      expect(auth?.scopes.length).toEqual(1);
      expect(auth?.scopes[0]).toEqual("read");
      expect(user.email).toEqual(newEmail);
      expect(isNewTeam).toEqual(false);
      expect(isNewUser).toEqual(false);
    });

    it("should allow authentication by email matching", async () => {
      const subdomain = buildSubdomain();
      const existingTeam = await buildTeam({
        subdomain,
      });

      const providers = await existingTeam.$get("authenticationProviders");
      const authenticationProvider = providers[0];
      const email = faker.internet.email();
      const userWithoutAuth = await buildUser({
        email,
        teamId: existingTeam.id,
        authentications: [],
      });

      const { user, isNewUser, isNewTeam } = await accountProvisioner(ctx, {
        user: {
          name: userWithoutAuth.name,
          email,
          emailVerified: true,
          avatarUrl: userWithoutAuth.avatarUrl,
        },
        team: {
          teamId: existingTeam.id,
          name: existingTeam.name,
          avatarUrl: existingTeam.avatarUrl,
          subdomain,
        },
        authenticationProvider: {
          name: authenticationProvider.name,
          providerId: authenticationProvider.providerId,
        },
        authentication: {
          providerId: randomUUID(),
          accessToken: "123",
          scopes: ["read"],
        },
      });
      expect(user.id).toEqual(userWithoutAuth.id);
      expect(isNewTeam).toEqual(false);
      expect(isNewUser).toEqual(false);
    });

    it("should not allow authentication by email matching when email is unverified", async () => {
      const subdomain = buildSubdomain();
      const existingTeam = await buildTeam({
        subdomain,
      });

      const providers = await existingTeam.$get("authenticationProviders");
      const authenticationProvider = providers[0];
      const email = faker.internet.email();
      const userWithoutAuth = await buildUser({
        email,
        teamId: existingTeam.id,
        authentications: [],
      });

      let error;
      try {
        await accountProvisioner(ctx, {
          user: {
            name: userWithoutAuth.name,
            email,
            emailVerified: false,
            avatarUrl: userWithoutAuth.avatarUrl,
          },
          team: {
            teamId: existingTeam.id,
            name: existingTeam.name,
            avatarUrl: existingTeam.avatarUrl,
            subdomain,
          },
          authenticationProvider: {
            name: authenticationProvider.name,
            providerId: authenticationProvider.providerId,
          },
          authentication: {
            providerId: randomUUID(),
            accessToken: "123",
            scopes: ["read"],
          },
        });
      } catch (err) {
        error = err;
      }

      expect(error).toBeTruthy();
      expect(
        error instanceof Error && "id" in error ? error.id : undefined
      ).toEqual("invalid_authentication");
    });

    it("should allow authentication by email matching from another tenant when the provider allows it", async () => {
      const spy = vi
        .spyOn(AuthenticationHelper, "providers", "get")
        .mockReturnValue([
          {
            type: Hook.AuthProvider,
            value: {
              router: new Router(),
              id: "google",
              allowEmailMatch: true,
            },
          },
        ]);
      const subdomain = buildSubdomain();
      const existingTeam = await buildTeam({
        subdomain,
        authenticationProviders: [
          {
            name: "google",
            providerId: faker.internet.domainName(),
          },
        ],
      });
      const email = faker.internet.email();
      const existingUser = await buildUser({
        email,
        teamId: existingTeam.id,
      });

      try {
        const { user } = await accountProvisioner(ctx, {
          user: {
            name: existingUser.name,
            email,
            emailVerified: true,
          },
          team: {
            teamId: existingTeam.id,
            subdomain,
          },
          authenticationProvider: {
            name: "google",
            providerId: faker.internet.domainName(),
          },
          authentication: {
            providerId: randomUUID(),
            accessToken: "123",
            scopes: ["read"],
          },
        });
        expect(user.id).toEqual(existingUser.id);
      } finally {
        spy.mockRestore();
      }
    });

    it("should not allow authentication by email matching from another tenant when the provider does not allow it", async () => {
      const subdomain = buildSubdomain();
      const existingTeam = await buildTeam({
        subdomain,
      });
      const email = faker.internet.email();
      const existingUser = await buildUser({
        email,
        teamId: existingTeam.id,
      });

      let error;
      try {
        await accountProvisioner(ctx, {
          user: {
            name: existingUser.name,
            email,
            emailVerified: true,
          },
          team: {
            teamId: existingTeam.id,
            subdomain,
          },
          authenticationProvider: {
            name: "slack",
            providerId: randomUUID(),
          },
          authentication: {
            providerId: randomUUID(),
            accessToken: "123",
            scopes: ["read"],
          },
        });
      } catch (err) {
        error = err;
      }

      expect(
        error instanceof Error && "id" in error ? error.id : undefined
      ).toEqual("invalid_authentication");
      expect(
        await UserAuthentication.count({ where: { userId: existingUser.id } })
      ).toEqual(1);
    });

    it("should throw an error when authentication provider is disabled", async () => {
      const existingTeam = await buildTeam();
      const providers = await existingTeam.$get("authenticationProviders");
      const authenticationProvider = providers[0];
      await authenticationProvider.update({
        enabled: false,
      });
      const existing = await buildUser({
        teamId: existingTeam.id,
      });
      const authentications = await existing.$get("authentications");
      const authentication = authentications[0];
      let error;

      try {
        await accountProvisioner(ctx, {
          user: {
            name: existing.name,
            email: existing.email!,
            avatarUrl: existing.avatarUrl,
          },
          team: {
            name: existingTeam.name,
            avatarUrl: existingTeam.avatarUrl,
            subdomain: buildSubdomain(),
          },
          authenticationProvider: {
            name: authenticationProvider.name,
            providerId: authenticationProvider.providerId,
          },
          authentication: {
            providerId: authentication.providerId,
            accessToken: "123",
            scopes: ["read"],
          },
        });
      } catch (err) {
        error = err;
      }

      expect(error).toBeTruthy();
    });

    it("should prioritize enabled authentication provider", async () => {
      const existingTeam = await buildTeam();
      const existingProviders = await existingTeam.$get(
        "authenticationProviders"
      );

      const team2 = await buildTeam();

      const providers = await team2.$get("authenticationProviders");
      const authenticationProvider = providers[0];
      await authenticationProvider.update({
        enabled: false,
        providerId: existingProviders[0].providerId,
      });

      const existing = await buildUser({
        teamId: existingTeam.id,
      });
      const authentications = await existing.$get("authentications");
      const authentication = authentications[0];
      const { isNewUser, isNewTeam } = await accountProvisioner(ctx, {
        user: {
          name: existing.name,
          email: existing.email!,
          avatarUrl: existing.avatarUrl,
        },
        team: {
          name: existingTeam.name,
          avatarUrl: existingTeam.avatarUrl,
          subdomain: buildSubdomain(),
        },
        authenticationProvider: {
          name: authenticationProvider.name,
          providerId: authenticationProvider.providerId,
        },
        authentication: {
          providerId: authentication.providerId,
          accessToken: "123",
          scopes: ["read"],
        },
      });
      const auth = await UserAuthentication.findByPk(authentication.id);
      expect(auth?.accessToken).toEqual("123");
      expect(auth?.scopes.length).toEqual(1);
      expect(auth?.scopes[0]).toEqual("read");
      expect(isNewTeam).toEqual(false);
      expect(isNewUser).toEqual(false);
    });

    it("should throw an error when the domain is not allowed", async () => {
      const existingTeam = await buildTeam();
      const admin = await buildAdmin({ teamId: existingTeam.id });
      const providers = await existingTeam.$get("authenticationProviders");
      const authenticationProvider = providers[0];
      const email = faker.internet.email();

      await TeamDomain.create({
        teamId: existingTeam.id,
        name: "other.com",
        createdById: admin.id,
      });

      let error;

      try {
        await accountProvisioner(ctx, {
          user: {
            name: "Jenny Tester",
            email,
            emailVerified: true,
            avatarUrl: faker.image.avatar(),
          },
          team: {
            avatarUrl: existingTeam.avatarUrl,
            subdomain: buildSubdomain(),
          },
          authenticationProvider: {
            name: authenticationProvider.name,
            providerId: authenticationProvider.providerId,
          },
          authentication: {
            providerId: randomUUID(),
            accessToken: "123",
            scopes: ["read"],
          },
        });
      } catch (err) {
        error = err;
      }

      expect(error).toBeTruthy();
    });

    it("should create a new user in an existing team when the domain is allowed", async () => {
      const team = await buildTeam();
      const admin = await buildAdmin({ teamId: team.id });
      const authenticationProviders = await team.$get(
        "authenticationProviders"
      );
      const authenticationProvider = authenticationProviders[0];
      const domain = faker.internet.domainName();
      await TeamDomain.create({
        teamId: team.id,
        name: domain,
        createdById: admin.id,
      });
      const email = faker.internet.email({ provider: domain });
      const { user, isNewUser } = await accountProvisioner(ctx, {
        user: {
          name: "Jenny Tester",
          email,
          emailVerified: true,
          avatarUrl: faker.image.avatar(),
        },
        team: {
          avatarUrl: team.avatarUrl,
          subdomain: buildSubdomain(),
        },
        authenticationProvider: {
          name: authenticationProvider.name,
          providerId: authenticationProvider.providerId,
        },
        authentication: {
          providerId: randomUUID(),
          accessToken: "123",
          scopes: ["read"],
        },
      });
      const authentications = await user.$get("authentications");
      const auth = authentications[0];
      expect(auth.accessToken).toEqual("123");
      expect(auth.scopes.length).toEqual(1);
      expect(auth.scopes[0]).toEqual("read");
      expect(user.email).toEqual(email);
      expect(isNewUser).toEqual(true);
      // should provision welcome collection
      const collectionCount = await Collection.count({
        where: {
          teamId: team.id,
        },
      });
      expect(collectionCount).toEqual(1);
    });

    it("should create a new user in an existing team", async () => {
      const team = await buildTeam();
      const authenticationProviders = await team.$get(
        "authenticationProviders"
      );
      const authenticationProvider = authenticationProviders[0];
      const email = faker.internet.email();
      const { user, isNewUser } = await accountProvisioner(ctx, {
        user: {
          name: "Jenny Tester",
          email,
          avatarUrl: faker.image.avatar(),
        },
        team: {
          name: team.name,
          avatarUrl: team.avatarUrl,
          subdomain: buildSubdomain(),
        },
        authenticationProvider: {
          name: authenticationProvider.name,
          providerId: authenticationProvider.providerId,
        },
        authentication: {
          providerId: randomUUID(),
          accessToken: "123",
          scopes: ["read"],
        },
      });
      const authentications = await user.$get("authentications");
      const auth = authentications[0];
      expect(auth.accessToken).toEqual("123");
      expect(auth.scopes.length).toEqual(1);
      expect(auth.scopes[0]).toEqual("read");
      expect(user.email).toEqual(email);
      expect(isNewUser).toEqual(true);
      // should provision welcome collection
      const collectionCount = await Collection.count({
        where: {
          teamId: team.id,
        },
      });
      expect(collectionCount).toEqual(1);
    });

    it("should handle emails with capital letters correctly", async () => {
      const email = "Jenny.Tester@EXAMPLE.COM";

      const params = {
        user: {
          name: "Jenny Tester",
          email,
          avatarUrl: faker.image.avatar(),
        },
        team: {
          name: "New workspace",
          avatarUrl: faker.image.avatar(),
          subdomain: buildSubdomain(),
        },
        authenticationProvider: {
          name: "google",
          providerId: faker.internet.domainName(),
        },
        authentication: {
          providerId: randomUUID(),
          accessToken: "123",
          scopes: ["read"],
        },
      };

      const { user, isNewTeam, isNewUser } = await accountProvisioner(
        ctx,
        params
      );

      expect(user.email).toEqual(email);
      expect(isNewUser).toEqual(true);
      expect(isNewTeam).toEqual(true);

      // Test that we can find the user again
      const existing = await accountProvisioner(ctx, params);

      expect(user.email).toEqual(email);
      expect(existing.isNewTeam).toEqual(false);
      expect(existing.isNewUser).toEqual(false);
      expect(existing.user.id).toEqual(user.id);
    });

    it("should allow connecting a new authentication provider while logged in", async () => {
      const admin = await buildAdmin();
      const team = admin.team;
      const ctxWithAdmin = createContext({ ip, user: admin });

      const providerId = faker.internet.domainName();
      const { user, isNewTeam, isNewUser } = await accountProvisioner(
        ctxWithAdmin,
        {
          user: {
            name: admin.name,
            email: admin.email!,
          },
          team: {
            teamId: team.id,
            subdomain: team.subdomain!,
          },
          authenticationProvider: {
            name: "google",
            providerId,
          },
          authentication: {
            providerId: randomUUID(),
            accessToken: "456",
            scopes: ["read"],
          },
        }
      );

      expect(user.id).toEqual(admin.id);
      expect(isNewUser).toEqual(false);
      expect(isNewTeam).toEqual(false);

      const providers = await team.$get("authenticationProviders");
      expect(providers.find((p) => p.name === "google")).toBeTruthy();
    });
  });

  describe("self hosted", () => {
    beforeEach(setSelfHosted);

    it("should fail if existing team and domain not in allowed list", async () => {
      let error;
      const team = await buildTeam();

      try {
        await accountProvisioner(ctx, {
          user: {
            name: "Jenny Tester",
            email: faker.internet.email(),
            avatarUrl: faker.image.avatar(),
          },
          team: {
            teamId: team.id,
            name: team.name,
            avatarUrl: team.avatarUrl,
            subdomain: buildSubdomain(),
          },
          authenticationProvider: {
            name: "google",
            providerId: faker.internet.domainName(),
          },
          authentication: {
            providerId: randomUUID(),
            accessToken: "123",
            scopes: ["read"],
          },
        });
      } catch (err) {
        error = err;
      }

      expect(errToString(error)).toEqual("Invalid authentication");
    });

    it("should always use existing team if self-hosted", async () => {
      const team = await buildTeam();
      const domain = faker.internet.domainName();
      const { user, isNewUser } = await accountProvisioner(ctx, {
        user: {
          name: "Jenny Tester",
          email: faker.internet.email(),
          avatarUrl: faker.image.avatar(),
        },
        team: {
          teamId: team.id,
          name: team.name,
          avatarUrl: team.avatarUrl,
          subdomain: buildSubdomain(),
          domain,
        },
        authenticationProvider: {
          name: "google",
          providerId: domain,
        },
        authentication: {
          providerId: randomUUID(),
          accessToken: "123",
          scopes: ["read"],
        },
      });

      expect(user.teamId).toEqual(team.id);
      expect(isNewUser).toEqual(true);

      const providers = await team.$get("authenticationProviders");
      expect(providers.length).toEqual(2);
    });
  });
});