fix: Self-hosted should show signin options for all configured authentication methods (#2986)

This commit is contained in:
Tom Moor
2022-06-04 10:46:03 -07:00
committed by GitHub
parent 4eb3b61c7a
commit 28439d315d
6 changed files with 205 additions and 110 deletions

View File

@@ -1,4 +1,5 @@
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import env from "@server/env";
import { TeamDomain } from "@server/models";
import Collection from "@server/models/Collection";
import UserAuthentication from "@server/models/UserAuthentication";
@@ -14,6 +15,8 @@ describe("accountProvisioner", () => {
const ip = "127.0.0.1";
it("should create a new user and team", async () => {
env.DEPLOYMENT = "hosted";
const spy = jest.spyOn(WelcomeEmail, "schedule");
const { user, team, isNewTeam, isNewUser } = await accountProvisioner({
ip,
@@ -286,4 +289,81 @@ describe("accountProvisioner", () => {
spy.mockRestore();
});
describe("self hosted", () => {
it("should fail if existing team and domain not in allowed list", async () => {
env.DEPLOYMENT = undefined;
let error;
const team = await buildTeam();
try {
await accountProvisioner({
ip,
user: {
name: "Jenny Tester",
email: "jenny@example.com",
avatarUrl: "https://example.com/avatar.png",
username: "jtester",
},
team: {
name: team.name,
avatarUrl: team.avatarUrl,
subdomain: "example",
},
authenticationProvider: {
name: "google",
providerId: "example.com",
},
authentication: {
providerId: "123456789",
accessToken: "123",
scopes: ["read"],
},
});
} catch (err) {
error = err;
}
expect(error.message).toEqual(
"The maximum number of teams has been reached"
);
});
it("should always use existing team if self-hosted", async () => {
env.DEPLOYMENT = undefined;
const team = await buildTeam();
const { user, isNewUser } = await accountProvisioner({
ip,
user: {
name: "Jenny Tester",
email: "jenny@example.com",
avatarUrl: "https://example.com/avatar.png",
username: "jtester",
},
team: {
name: team.name,
avatarUrl: team.avatarUrl,
subdomain: "example",
domain: "allowed-domain.com",
},
authenticationProvider: {
name: "google",
providerId: "allowed-domain.com",
},
authentication: {
providerId: "123456789",
accessToken: "123",
scopes: ["read"],
},
});
expect(user.teamId).toEqual(team.id);
expect(user.username).toEqual("jtester");
expect(isNewUser).toEqual(true);
const providers = await team.$get("authenticationProviders");
expect(providers.length).toEqual(2);
});
});
});

View File

@@ -8,6 +8,7 @@ beforeEach(() => flushdb());
describe("teamCreator", () => {
it("should create team and authentication provider", async () => {
env.DEPLOYMENT = "hosted";
const result = await teamCreator({
name: "Test team",
subdomain: "example",
@@ -25,6 +26,7 @@ describe("teamCreator", () => {
expect(isNewTeam).toEqual(true);
});
describe("self hosted", () => {
it("should not allow creating multiple teams in installation", async () => {
env.DEPLOYMENT = undefined;
await buildTeam();
@@ -58,7 +60,6 @@ describe("teamCreator", () => {
name: "allowed-domain.com",
createdById: user.id,
});
const result = await teamCreator({
name: "Updated name",
subdomain: "example",
@@ -79,12 +80,14 @@ describe("teamCreator", () => {
});
it("should error when NOT within allowed domains", async () => {
const user = await buildUser();
delete process.env.DEPLOYMENT;
env.DEPLOYMENT = undefined;
const existing = await buildTeam();
const user = await buildUser({
teamId: existing.id,
});
await TeamDomain.create({
teamId: existing.id,
name: "other-domain.com",
name: "allowed-domain.com",
createdById: user.id,
});
@@ -93,10 +96,10 @@ describe("teamCreator", () => {
await teamCreator({
name: "Updated name",
subdomain: "example",
domain: "allowed-domain.com",
domain: "other-domain.com",
authenticationProvider: {
name: "google",
providerId: "allowed-domain.com",
providerId: "other-domain.com",
},
});
} catch (err) {
@@ -127,4 +130,5 @@ describe("teamCreator", () => {
expect(team.subdomain).toEqual("example");
expect(isNewTeam).toEqual(false);
});
});
});

View File

@@ -1,4 +1,3 @@
import invariant from "invariant";
import env from "@server/env";
import { DomainNotAllowedError, MaximumTeamsError } from "@server/errors";
import Logger from "@server/logging/Logger";
@@ -55,15 +54,12 @@ async function teamCreator({
// to the multi-tenant version, we want to restrict to a single team that MAY
// have multiple authentication providers
if (env.DEPLOYMENT !== "hosted") {
const teamCount = await Team.count();
const team = await Team.findOne();
// If the self-hosted installation has a single team and the domain for the
// new team is allowed then assign the authentication provider to the
// existing team
if (teamCount === 1 && domain) {
const team = await Team.findOne();
invariant(team, "Team should exist");
if (team && domain) {
if (await team.isDomainAllowed(domain)) {
authP = await team.$create<AuthenticationProvider>(
"authenticationProvider",
@@ -79,10 +75,8 @@ async function teamCreator({
}
}
if (teamCount >= 1) {
throw MaximumTeamsError();
}
}
// If the service did not provide a logo/avatar then we attempt to generate
// one via ClearBit, or fallback to colored initials in worst case scenario

View File

@@ -1,4 +1,5 @@
import TestServer from "fetch-test-server";
import sharedEnv from "@shared/env";
import env from "@server/env";
import webService from "@server/services/web";
import { buildUser, buildTeam } from "@server/test/factories";
@@ -49,6 +50,8 @@ describe("#auth.info", () => {
describe("#auth.config", () => {
it("should return available SSO providers", async () => {
env.DEPLOYMENT = "hosted";
const res = await server.post("/api/auth.config");
const body = await res.json();
expect(res.status).toEqual(200);
@@ -58,7 +61,10 @@ describe("#auth.config", () => {
});
it("should return available providers for team subdomain", async () => {
env.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "http://localoutline.com";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
env.DEPLOYMENT = "hosted";
await buildTeam({
guestSignin: false,
subdomain: "example",
@@ -81,6 +87,8 @@ describe("#auth.config", () => {
});
it("should return available providers for team custom domain", async () => {
env.DEPLOYMENT = "hosted";
await buildTeam({
guestSignin: false,
domain: "docs.mycompany.com",
@@ -103,7 +111,9 @@ describe("#auth.config", () => {
});
it("should return email provider for team when guest signin enabled", async () => {
env.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "http://localoutline.com";
env.DEPLOYMENT = "hosted";
await buildTeam({
guestSignin: true,
subdomain: "example",
@@ -127,7 +137,9 @@ describe("#auth.config", () => {
});
it("should not return provider when disabled", async () => {
env.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "http://localoutline.com";
env.DEPLOYMENT = "hosted";
await buildTeam({
guestSignin: false,
subdomain: "example",
@@ -148,8 +160,9 @@ describe("#auth.config", () => {
expect(res.status).toEqual(200);
expect(body.data.providers.length).toBe(0);
});
describe("self hosted", () => {
it("should return available providers for team", async () => {
it("should return all configured providers but respect email setting", async () => {
env.DEPLOYMENT = "";
await buildTeam({
guestSignin: false,
@@ -163,9 +176,11 @@ describe("#auth.config", () => {
const res = await server.post("/api/auth.config");
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.providers.length).toBe(1);
expect(body.data.providers[0].name).toBe("Slack");
expect(body.data.providers.length).toBe(2);
expect(body.data.providers[0].name).toBe("Google");
expect(body.data.providers[1].name).toBe("Slack");
});
it("should return email provider for team when guest signin enabled", async () => {
env.DEPLOYMENT = "";
await buildTeam({
@@ -180,9 +195,10 @@ describe("#auth.config", () => {
const res = await server.post("/api/auth.config");
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.providers.length).toBe(2);
expect(body.data.providers.length).toBe(3);
expect(body.data.providers[0].name).toBe("Slack");
expect(body.data.providers[1].name).toBe("Email");
expect(body.data.providers[1].name).toBe("Google");
expect(body.data.providers[2].name).toBe("Email");
});
});
});

View File

@@ -22,6 +22,7 @@ function filterProviders(team?: Team) {
return (
!team ||
env.DEPLOYMENT !== "hosted" ||
find(team.authenticationProviders, {
name: provider.id,
enabled: true,
@@ -40,10 +41,9 @@ router.post("auth.config", async (ctx) => {
// brand for the knowledge base and it's guest signin option is used for the
// root login page.
if (env.DEPLOYMENT !== "hosted") {
const teams = await Team.scope("withAuthenticationProviders").findAll();
const team = await Team.scope("withAuthenticationProviders").findOne();
if (teams.length === 1) {
const team = teams[0];
if (team) {
ctx.body = {
data: {
name: team.name,

View File

@@ -1,4 +1,5 @@
import Router from "koa-router";
import { sortBy } from "lodash";
import { signin } from "@shared/utils/urlHelpers";
import { requireDirectory } from "@server/utils/fs";
@@ -43,4 +44,4 @@ requireDirectory(__dirname).forEach(([module, id]) => {
}
});
export default providers;
export default sortBy(providers, "id");