feat: allow external SSO methods to log into teams as long as emails match (#3813)
* wip * wip * fix comments * better separation of conerns * fix up tests * fix semantics * fixup tsc * fix some tests * the old semantics were easier to use * add db:reset to scripts * explicitly throw for unauthorized external authorization * fix minor bug * add additional tests for user creator and team creator * yank the email matching logic out of teamcreator * renaming * fix type and test errors * adds test to ensure that accountProvisioner works with email matching * remove only * fix comments * recreate changes to allow self hosted to make teams
This commit is contained in:
418
server/commands/userProvisioner.test.ts
Normal file
418
server/commands/userProvisioner.test.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { TeamDomain } from "@server/models";
|
||||
import { buildUser, buildTeam, buildInvite } from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
import userProvisioner from "./userProvisioner";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userProvisioner", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should update existing user and authentication", async () => {
|
||||
const existing = await buildUser();
|
||||
const authentications = await existing.$get("authentications");
|
||||
const existingAuth = authentications[0];
|
||||
const newEmail = "test@example.com";
|
||||
const newUsername = "tname";
|
||||
const result = await userProvisioner({
|
||||
name: existing.name,
|
||||
email: newEmail,
|
||||
username: newUsername,
|
||||
avatarUrl: existing.avatarUrl,
|
||||
teamId: existing.teamId,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: existingAuth.authenticationProviderId,
|
||||
providerId: existingAuth.providerId,
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user, authentication, isNewUser } = result;
|
||||
expect(authentication).toBeDefined();
|
||||
expect(authentication?.accessToken).toEqual("123");
|
||||
expect(authentication?.scopes.length).toEqual(1);
|
||||
expect(authentication?.scopes[0]).toEqual("read");
|
||||
expect(user.email).toEqual(newEmail);
|
||||
expect(user.username).toEqual(newUsername);
|
||||
expect(isNewUser).toEqual(false);
|
||||
});
|
||||
|
||||
it("should add authentication provider to existing users", async () => {
|
||||
const team = await buildTeam({ inviteRequired: true });
|
||||
const teamAuthProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = teamAuthProviders[0];
|
||||
|
||||
const email = "mynam@email.com";
|
||||
const existing = await buildUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
authentications: [],
|
||||
});
|
||||
|
||||
const result = await userProvisioner({
|
||||
name: existing.name,
|
||||
email,
|
||||
username: "new-username",
|
||||
avatarUrl: existing.avatarUrl,
|
||||
teamId: existing.teamId,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: uuidv4(),
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user, authentication, isNewUser } = result;
|
||||
expect(authentication).toBeDefined();
|
||||
expect(authentication?.accessToken).toEqual("123");
|
||||
expect(authentication?.scopes.length).toEqual(1);
|
||||
expect(authentication?.scopes[0]).toEqual("read");
|
||||
|
||||
const authentications = await user.$get("authentications");
|
||||
expect(authentications.length).toEqual(1);
|
||||
expect(isNewUser).toEqual(false);
|
||||
});
|
||||
|
||||
it("should add authentication provider to invited users", async () => {
|
||||
const team = await buildTeam({ inviteRequired: true });
|
||||
const teamAuthProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = teamAuthProviders[0];
|
||||
|
||||
const email = "mynam@email.com";
|
||||
const existing = await buildInvite({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const result = await userProvisioner({
|
||||
name: existing.name,
|
||||
email,
|
||||
username: "new-username",
|
||||
avatarUrl: existing.avatarUrl,
|
||||
teamId: existing.teamId,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: uuidv4(),
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user, authentication, isNewUser } = result;
|
||||
expect(authentication).toBeDefined();
|
||||
expect(authentication?.accessToken).toEqual("123");
|
||||
expect(authentication?.scopes.length).toEqual(1);
|
||||
expect(authentication?.scopes[0]).toEqual("read");
|
||||
|
||||
const authentications = await user.$get("authentications");
|
||||
expect(authentications.length).toEqual(1);
|
||||
expect(isNewUser).toEqual(true);
|
||||
});
|
||||
|
||||
it("should create user with deleted user matching providerId", async () => {
|
||||
const existing = await buildUser();
|
||||
const authentications = await existing.$get("authentications");
|
||||
const existingAuth = authentications[0];
|
||||
const newEmail = "test@example.com";
|
||||
await existing.destroy();
|
||||
const result = await userProvisioner({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
teamId: existing.teamId,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: existingAuth.authenticationProviderId,
|
||||
providerId: existingAuth.providerId,
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user, authentication, isNewUser } = result;
|
||||
expect(authentication).toBeDefined();
|
||||
expect(authentication?.accessToken).toEqual("123");
|
||||
expect(authentication?.scopes.length).toEqual(1);
|
||||
expect(authentication?.scopes[0]).toEqual("read");
|
||||
expect(user.email).toEqual(newEmail);
|
||||
expect(isNewUser).toEqual(true);
|
||||
});
|
||||
|
||||
it("should handle duplicate providerId for different iDP", async () => {
|
||||
const existing = await buildUser();
|
||||
const authentications = await existing.$get("authentications");
|
||||
const existingAuth = authentications[0];
|
||||
let error;
|
||||
|
||||
try {
|
||||
await userProvisioner({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
teamId: existing.teamId,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: uuidv4(),
|
||||
providerId: existingAuth.providerId,
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error && error.toString()).toContain("already exists for");
|
||||
});
|
||||
|
||||
it("should create a new user", async () => {
|
||||
const team = await buildTeam();
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
const result = await userProvisioner({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
username: "tname",
|
||||
teamId: team.id,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user, authentication, isNewUser } = result;
|
||||
expect(authentication).toBeDefined();
|
||||
expect(authentication?.accessToken).toEqual("123");
|
||||
expect(authentication?.scopes.length).toEqual(1);
|
||||
expect(authentication?.scopes[0]).toEqual("read");
|
||||
expect(user.email).toEqual("test@example.com");
|
||||
expect(user.username).toEqual("tname");
|
||||
expect(user.isAdmin).toEqual(false);
|
||||
expect(user.isViewer).toEqual(false);
|
||||
expect(isNewUser).toEqual(true);
|
||||
});
|
||||
|
||||
it("should prefer isAdmin argument over defaultUserRole", async () => {
|
||||
const team = await buildTeam({
|
||||
defaultUserRole: "viewer",
|
||||
});
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
const result = await userProvisioner({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
username: "tname",
|
||||
teamId: team.id,
|
||||
isAdmin: true,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user } = result;
|
||||
expect(user.isAdmin).toEqual(true);
|
||||
});
|
||||
|
||||
it("should prefer defaultUserRole when isAdmin is undefined or false", async () => {
|
||||
const team = await buildTeam({
|
||||
defaultUserRole: "viewer",
|
||||
});
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
const result = await userProvisioner({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
username: "tname",
|
||||
teamId: team.id,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user: tname } = result;
|
||||
expect(tname.username).toEqual("tname");
|
||||
expect(tname.isAdmin).toEqual(false);
|
||||
expect(tname.isViewer).toEqual(true);
|
||||
const tname2Result = await userProvisioner({
|
||||
name: "Test2 Name",
|
||||
email: "tes2@example.com",
|
||||
username: "tname2",
|
||||
teamId: team.id,
|
||||
isAdmin: false,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user: tname2 } = tname2Result;
|
||||
expect(tname2.username).toEqual("tname2");
|
||||
expect(tname2.isAdmin).toEqual(false);
|
||||
expect(tname2.isViewer).toEqual(true);
|
||||
});
|
||||
|
||||
it("should create a user from an invited user", async () => {
|
||||
const team = await buildTeam({ inviteRequired: true });
|
||||
const invite = await buildInvite({
|
||||
teamId: team.id,
|
||||
email: "invite@example.com",
|
||||
});
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
const result = await userProvisioner({
|
||||
name: invite.name,
|
||||
email: "invite@ExamPle.com",
|
||||
teamId: invite.teamId,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user, authentication, isNewUser } = result;
|
||||
expect(authentication).toBeDefined();
|
||||
expect(authentication?.accessToken).toEqual("123");
|
||||
expect(authentication?.scopes.length).toEqual(1);
|
||||
expect(authentication?.scopes[0]).toEqual("read");
|
||||
expect(user.email).toEqual(invite.email);
|
||||
expect(isNewUser).toEqual(true);
|
||||
});
|
||||
|
||||
it("should create a user from an invited user using email match", async () => {
|
||||
const externalUser = await buildUser({
|
||||
email: "external@example.com",
|
||||
});
|
||||
|
||||
const team = await buildTeam({ inviteRequired: true });
|
||||
const invite = await buildInvite({
|
||||
teamId: team.id,
|
||||
email: externalUser.email,
|
||||
});
|
||||
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
const result = await userProvisioner({
|
||||
name: invite.name,
|
||||
email: "external@ExamPle.com", // ensure that email is case insensistive
|
||||
teamId: invite.teamId,
|
||||
emailMatchOnly: true,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "whatever",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user, authentication, isNewUser } = result;
|
||||
expect(authentication).toEqual(null);
|
||||
expect(user.id).toEqual(invite.id);
|
||||
expect(isNewUser).toEqual(true);
|
||||
});
|
||||
|
||||
it("should reject an uninvited user when invites are required", async () => {
|
||||
const team = await buildTeam({ inviteRequired: true });
|
||||
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
let error;
|
||||
|
||||
try {
|
||||
await userProvisioner({
|
||||
name: "Uninvited User",
|
||||
email: "invite@ExamPle.com",
|
||||
teamId: team.id,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error && error.toString()).toContain(
|
||||
"You need an invite to join this team"
|
||||
);
|
||||
});
|
||||
|
||||
it("should create a user from allowed Domain", async () => {
|
||||
const { admin, team } = await seed();
|
||||
await TeamDomain.create({
|
||||
teamId: team.id,
|
||||
name: "example-company.com",
|
||||
createdById: admin.id,
|
||||
});
|
||||
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
const result = await userProvisioner({
|
||||
name: "Test Name",
|
||||
email: "user@example-company.com",
|
||||
teamId: team.id,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
const { user, authentication, isNewUser } = result;
|
||||
expect(authentication).toBeDefined();
|
||||
expect(authentication?.accessToken).toEqual("123");
|
||||
expect(authentication?.scopes.length).toEqual(1);
|
||||
expect(authentication?.scopes[0]).toEqual("read");
|
||||
expect(user.email).toEqual("user@example-company.com");
|
||||
expect(isNewUser).toEqual(true);
|
||||
});
|
||||
|
||||
it("should reject an user when the domain is not allowed", async () => {
|
||||
const { admin, team } = await seed();
|
||||
await TeamDomain.create({
|
||||
teamId: team.id,
|
||||
name: "other.com",
|
||||
createdById: admin.id,
|
||||
});
|
||||
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
let error;
|
||||
|
||||
try {
|
||||
await userProvisioner({
|
||||
name: "Bad Domain User",
|
||||
email: "user@example.com",
|
||||
teamId: team.id,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error && error.toString()).toContain(
|
||||
"The domain is not allowed for this team"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user