fix: Do not show actively disabled auth providers in self-hosted install (#4794)
* fix: Do not show actively disabled auth providers in self-hosted installation * self review * Refactor for easier mocking
This commit is contained in:
@@ -72,7 +72,7 @@ async function teamProvisioner({
|
|||||||
};
|
};
|
||||||
} else if (teamId) {
|
} else if (teamId) {
|
||||||
// The user is attempting to log into a team with an unfamiliar SSO provider
|
// The user is attempting to log into a team with an unfamiliar SSO provider
|
||||||
if (env.DEPLOYMENT === "hosted") {
|
if (env.isCloudHosted()) {
|
||||||
throw InvalidAuthenticationError();
|
throw InvalidAuthenticationError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Oy from "oy-vey";
|
|||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import Logger from "@server/logging/Logger";
|
import Logger from "@server/logging/Logger";
|
||||||
import { trace } from "@server/logging/tracing";
|
import { trace } from "@server/logging/tracing";
|
||||||
import isCloudHosted from "@server/utils/isCloudHosted";
|
|
||||||
import { baseStyles } from "./templates/components/EmailLayout";
|
import { baseStyles } from "./templates/components/EmailLayout";
|
||||||
|
|
||||||
const useTestEmailService =
|
const useTestEmailService =
|
||||||
@@ -79,7 +78,7 @@ export class Mailer {
|
|||||||
subject: data.subject,
|
subject: data.subject,
|
||||||
html,
|
html,
|
||||||
text: data.text,
|
text: data.text,
|
||||||
attachments: isCloudHosted
|
attachments: env.isCloudHosted()
|
||||||
? undefined
|
? undefined
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Table, TBody, TR, TD } from "oy-vey";
|
import { Table, TBody, TR, TD } from "oy-vey";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import isCloudHosted from "@server/utils/isCloudHosted";
|
|
||||||
import EmptySpace from "./EmptySpace";
|
import EmptySpace from "./EmptySpace";
|
||||||
|
|
||||||
const url = env.CDN_URL ?? env.URL;
|
const url = env.CDN_URL ?? env.URL;
|
||||||
@@ -16,7 +15,7 @@ export default () => {
|
|||||||
<img
|
<img
|
||||||
alt={env.APP_NAME}
|
alt={env.APP_NAME}
|
||||||
src={
|
src={
|
||||||
isCloudHosted
|
env.isCloudHosted()
|
||||||
? `${url}/email/header-logo.png`
|
? `${url}/email/header-logo.png`
|
||||||
: "cid:header-image"
|
: "cid:header-image"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -575,6 +575,14 @@ export class Environment {
|
|||||||
*/
|
*/
|
||||||
public APP_NAME = "Outline";
|
public APP_NAME = "Outline";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the current installation is the cloud hosted version at
|
||||||
|
* getoutline.com
|
||||||
|
*/
|
||||||
|
public isCloudHosted() {
|
||||||
|
return this.DEPLOYMENT === "hosted";
|
||||||
|
}
|
||||||
|
|
||||||
private toOptionalString(value: string | undefined) {
|
private toOptionalString(value: string | undefined) {
|
||||||
return value ? value : undefined;
|
return value ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { CollectionPermission, TeamPreference } from "@shared/types";
|
|||||||
import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
|
import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
|
import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
|
||||||
import isCloudHosted from "@server/utils/isCloudHosted";
|
|
||||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||||
import Attachment from "./Attachment";
|
import Attachment from "./Attachment";
|
||||||
import AuthenticationProvider from "./AuthenticationProvider";
|
import AuthenticationProvider from "./AuthenticationProvider";
|
||||||
@@ -67,9 +66,9 @@ class Team extends ParanoidModel {
|
|||||||
@Unique
|
@Unique
|
||||||
@Length({
|
@Length({
|
||||||
min: 2,
|
min: 2,
|
||||||
max: isCloudHosted ? 32 : 255,
|
max: env.isCloudHosted() ? 32 : 255,
|
||||||
msg: `subdomain must be between 2 and ${
|
msg: `subdomain must be between 2 and ${
|
||||||
isCloudHosted ? 32 : 255
|
env.isCloudHosted() ? 32 : 255
|
||||||
} characters`,
|
} characters`,
|
||||||
})
|
})
|
||||||
@Is({
|
@Is({
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
BeforeCreate,
|
BeforeCreate,
|
||||||
} from "sequelize-typescript";
|
} from "sequelize-typescript";
|
||||||
import { TeamValidation } from "@shared/validations";
|
import { TeamValidation } from "@shared/validations";
|
||||||
|
import env from "@server/env";
|
||||||
import { ValidationError } from "@server/errors";
|
import { ValidationError } from "@server/errors";
|
||||||
import isCloudHosted from "@server/utils/isCloudHosted";
|
|
||||||
import Team from "./Team";
|
import Team from "./Team";
|
||||||
import User from "./User";
|
import User from "./User";
|
||||||
import IdModel from "./base/IdModel";
|
import IdModel from "./base/IdModel";
|
||||||
@@ -23,7 +23,7 @@ import Length from "./validators/Length";
|
|||||||
@Fix
|
@Fix
|
||||||
class TeamDomain extends IdModel {
|
class TeamDomain extends IdModel {
|
||||||
@NotIn({
|
@NotIn({
|
||||||
args: isCloudHosted ? [emailProviders] : [],
|
args: env.isCloudHosted() ? [emailProviders] : [],
|
||||||
msg: "You chose a restricted domain, please try another.",
|
msg: "You chose a restricted domain, please try another.",
|
||||||
})
|
})
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
|
|||||||
42
server/models/helpers/AuthenticationHelper.ts
Normal file
42
server/models/helpers/AuthenticationHelper.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { find } from "lodash";
|
||||||
|
import env from "@server/env";
|
||||||
|
import Team from "@server/models/Team";
|
||||||
|
import providerConfigs from "../../routes/auth/providers";
|
||||||
|
|
||||||
|
export default class AuthenticationHelper {
|
||||||
|
/**
|
||||||
|
* Returns the enabled authentication provider configurations for a team,
|
||||||
|
* if given otherwise all enabled providers are returned.
|
||||||
|
*
|
||||||
|
* @param team The team to get enabled providers for
|
||||||
|
* @returns A list of authentication providers
|
||||||
|
*/
|
||||||
|
static providersForTeam(team?: Team) {
|
||||||
|
const isCloudHosted = env.isCloudHosted();
|
||||||
|
|
||||||
|
return providerConfigs
|
||||||
|
.sort((config) => (config.id === "email" ? 1 : -1))
|
||||||
|
.filter((config) => {
|
||||||
|
// guest sign-in is an exception as it does not have an authentication
|
||||||
|
// provider using passport, instead it exists as a boolean option on the team
|
||||||
|
if (config.id === "email") {
|
||||||
|
return team?.emailSigninEnabled;
|
||||||
|
}
|
||||||
|
if (!team) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authProvider = find(team.authenticationProviders, {
|
||||||
|
name: config.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If cloud hosted then the auth provider must be enabled for the team,
|
||||||
|
// If self-hosted then it must not be actively disabled, otherwise all
|
||||||
|
// providers are considered.
|
||||||
|
return (
|
||||||
|
(!isCloudHosted && authProvider?.enabled !== false) ||
|
||||||
|
(isCloudHosted && authProvider?.enabled)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ allow(User, "share", Team, (user, team) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
allow(User, "createTeam", Team, () => {
|
allow(User, "createTeam", Team, () => {
|
||||||
if (env.DEPLOYMENT !== "hosted") {
|
if (!env.isCloudHosted()) {
|
||||||
throw IncorrectEditionError("createTeam only available on cloud");
|
throw IncorrectEditionError("createTeam only available on cloud");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import presentMembership from "./membership";
|
|||||||
import presentNotificationSetting from "./notificationSetting";
|
import presentNotificationSetting from "./notificationSetting";
|
||||||
import presentPin from "./pin";
|
import presentPin from "./pin";
|
||||||
import presentPolicies from "./policy";
|
import presentPolicies from "./policy";
|
||||||
|
import presentProviderConfig from "./providerConfig";
|
||||||
import presentRevision from "./revision";
|
import presentRevision from "./revision";
|
||||||
import presentSearchQuery from "./searchQuery";
|
import presentSearchQuery from "./searchQuery";
|
||||||
import presentShare from "./share";
|
import presentShare from "./share";
|
||||||
@@ -43,6 +44,7 @@ export {
|
|||||||
presentNotificationSetting,
|
presentNotificationSetting,
|
||||||
presentPin,
|
presentPin,
|
||||||
presentPolicies,
|
presentPolicies,
|
||||||
|
presentProviderConfig,
|
||||||
presentRevision,
|
presentRevision,
|
||||||
presentSearchQuery,
|
presentSearchQuery,
|
||||||
presentShare,
|
presentShare,
|
||||||
|
|||||||
12
server/presenters/providerConfig.ts
Normal file
12
server/presenters/providerConfig.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { signin } from "@shared/utils/urlHelpers";
|
||||||
|
import { AuthenticationProviderConfig } from "@server/routes/auth/providers";
|
||||||
|
|
||||||
|
export default function presentProviderConfig(
|
||||||
|
config: AuthenticationProviderConfig
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id: config.id,
|
||||||
|
name: config.name,
|
||||||
|
authUrl: signin(config.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,55 +1,30 @@
|
|||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import { find, uniqBy } from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
import { TeamPreference } from "@shared/types";
|
import { TeamPreference } from "@shared/types";
|
||||||
import { parseDomain } from "@shared/utils/domains";
|
import { parseDomain } from "@shared/utils/domains";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import auth from "@server/middlewares/authentication";
|
import auth from "@server/middlewares/authentication";
|
||||||
import { transaction } from "@server/middlewares/transaction";
|
import { transaction } from "@server/middlewares/transaction";
|
||||||
import { Event, Team } from "@server/models";
|
import { Event, Team } from "@server/models";
|
||||||
|
import AuthenticationHelper from "@server/models/helpers/AuthenticationHelper";
|
||||||
import {
|
import {
|
||||||
presentUser,
|
presentUser,
|
||||||
presentTeam,
|
presentTeam,
|
||||||
presentPolicies,
|
presentPolicies,
|
||||||
|
presentProviderConfig,
|
||||||
presentAvailableTeam,
|
presentAvailableTeam,
|
||||||
} from "@server/presenters";
|
} from "@server/presenters";
|
||||||
import ValidateSSOAccessTask from "@server/queues/tasks/ValidateSSOAccessTask";
|
import ValidateSSOAccessTask from "@server/queues/tasks/ValidateSSOAccessTask";
|
||||||
import { APIContext } from "@server/types";
|
import { APIContext } from "@server/types";
|
||||||
import { getSessionsInCookie } from "@server/utils/authentication";
|
import { getSessionsInCookie } from "@server/utils/authentication";
|
||||||
import providers from "../auth/providers";
|
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
function filterProviders(team?: Team) {
|
|
||||||
return providers
|
|
||||||
.sort((provider) => (provider.id === "email" ? 1 : -1))
|
|
||||||
.filter((provider) => {
|
|
||||||
// guest sign-in is an exception as it does not have an authentication
|
|
||||||
// provider using passport, instead it exists as a boolean option on the team
|
|
||||||
if (provider.id === "email") {
|
|
||||||
return team?.emailSigninEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
!team ||
|
|
||||||
env.DEPLOYMENT !== "hosted" ||
|
|
||||||
find(team.authenticationProviders, {
|
|
||||||
name: provider.id,
|
|
||||||
enabled: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((provider) => ({
|
|
||||||
id: provider.id,
|
|
||||||
name: provider.name,
|
|
||||||
authUrl: provider.authUrl,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
router.post("auth.config", async (ctx: APIContext) => {
|
router.post("auth.config", async (ctx: APIContext) => {
|
||||||
// If self hosted AND there is only one team then that team becomes the
|
// If self hosted AND there is only one team then that team becomes the
|
||||||
// brand for the knowledge base and it's guest signin option is used for the
|
// brand for the knowledge base and it's guest signin option is used for the
|
||||||
// root login page.
|
// root login page.
|
||||||
if (env.DEPLOYMENT !== "hosted") {
|
if (!env.isCloudHosted()) {
|
||||||
const team = await Team.scope("withAuthenticationProviders").findOne();
|
const team = await Team.scope("withAuthenticationProviders").findOne();
|
||||||
|
|
||||||
if (team) {
|
if (team) {
|
||||||
@@ -59,7 +34,9 @@ router.post("auth.config", async (ctx: APIContext) => {
|
|||||||
logo: team.getPreference(TeamPreference.PublicBranding)
|
logo: team.getPreference(TeamPreference.PublicBranding)
|
||||||
? team.avatarUrl
|
? team.avatarUrl
|
||||||
: undefined,
|
: undefined,
|
||||||
providers: filterProviders(team),
|
providers: AuthenticationHelper.providersForTeam(team).map(
|
||||||
|
presentProviderConfig
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
@@ -83,7 +60,9 @@ router.post("auth.config", async (ctx: APIContext) => {
|
|||||||
? team.avatarUrl
|
? team.avatarUrl
|
||||||
: undefined,
|
: undefined,
|
||||||
hostname: ctx.request.hostname,
|
hostname: ctx.request.hostname,
|
||||||
providers: filterProviders(team),
|
providers: AuthenticationHelper.providersForTeam(team).map(
|
||||||
|
presentProviderConfig
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
@@ -107,7 +86,9 @@ router.post("auth.config", async (ctx: APIContext) => {
|
|||||||
? team.avatarUrl
|
? team.avatarUrl
|
||||||
: undefined,
|
: undefined,
|
||||||
hostname: ctx.request.hostname,
|
hostname: ctx.request.hostname,
|
||||||
providers: filterProviders(team),
|
providers: AuthenticationHelper.providersForTeam(team).map(
|
||||||
|
presentProviderConfig
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
@@ -117,7 +98,9 @@ router.post("auth.config", async (ctx: APIContext) => {
|
|||||||
// Otherwise, we're requesting from the standard root signin page
|
// Otherwise, we're requesting from the standard root signin page
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: {
|
data: {
|
||||||
providers: filterProviders(),
|
providers: AuthenticationHelper.providersForTeam().map(
|
||||||
|
presentProviderConfig
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ router.post(
|
|||||||
const domain = parseDomain(ctx.request.hostname);
|
const domain = parseDomain(ctx.request.hostname);
|
||||||
|
|
||||||
let team: Team | null | undefined;
|
let team: Team | null | undefined;
|
||||||
if (env.DEPLOYMENT !== "hosted") {
|
if (!env.isCloudHosted()) {
|
||||||
team = await Team.scope("withAuthenticationProviders").findOne();
|
team = await Team.scope("withAuthenticationProviders").findOne();
|
||||||
} else if (domain.custom) {
|
} else if (domain.custom) {
|
||||||
team = await Team.scope("withAuthenticationProviders").findOne({
|
team = await Team.scope("withAuthenticationProviders").findOne({
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import { sortBy } from "lodash";
|
import { sortBy } from "lodash";
|
||||||
import { signin } from "@shared/utils/urlHelpers";
|
|
||||||
import { requireDirectory } from "@server/utils/fs";
|
import { requireDirectory } from "@server/utils/fs";
|
||||||
|
|
||||||
export type AuthenticationProviderConfig = {
|
export type AuthenticationProviderConfig = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
authUrl: string;
|
|
||||||
router: Router;
|
router: Router;
|
||||||
};
|
};
|
||||||
|
|
||||||
const providers: AuthenticationProviderConfig[] = [];
|
const authenticationProviderConfigs: AuthenticationProviderConfig[] = [];
|
||||||
|
|
||||||
requireDirectory(__dirname).forEach(([module, id]) => {
|
requireDirectory(__dirname).forEach(([module, id]) => {
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'config' does not exist on type 'unknown'... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'config' does not exist on type 'unknown'... Remove this comment to see the full error message
|
||||||
@@ -34,14 +32,13 @@ requireDirectory(__dirname).forEach(([module, id]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config && config.enabled) {
|
if (config && config.enabled) {
|
||||||
providers.push({
|
authenticationProviderConfigs.push({
|
||||||
id,
|
id,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
enabled: config.enabled,
|
enabled: config.enabled,
|
||||||
authUrl: signin(id),
|
|
||||||
router,
|
router,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default sortBy(providers, "id");
|
export default sortBy(authenticationProviderConfigs, "id");
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import env from "@server/env";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the current installation is the cloud hosted version at getoutline.com
|
|
||||||
*/
|
|
||||||
const isCloudHosted = env.DEPLOYMENT === "hosted";
|
|
||||||
|
|
||||||
export default isCloudHosted;
|
|
||||||
@@ -102,7 +102,7 @@ export async function getTeamFromContext(ctx: Context) {
|
|||||||
const domain = parseDomain(host);
|
const domain = parseDomain(host);
|
||||||
|
|
||||||
let team;
|
let team;
|
||||||
if (env.DEPLOYMENT !== "hosted") {
|
if (!env.isCloudHosted()) {
|
||||||
team = await Team.findOne();
|
team = await Team.findOne();
|
||||||
} else if (domain.custom) {
|
} else if (domain.custom) {
|
||||||
team = await Team.findOne({ where: { domain: domain.host } });
|
team = await Team.findOne({ where: { domain: domain.host } });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
|
|
||||||
export const robotsResponse = () => {
|
export const robotsResponse = () => {
|
||||||
if (env.DEPLOYMENT === "hosted") {
|
if (env.isCloudHosted()) {
|
||||||
return `
|
return `
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function checkPendingMigrations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function checkMigrations() {
|
export async function checkMigrations() {
|
||||||
if (env.DEPLOYMENT === "hosted") {
|
if (env.isCloudHosted()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user