Files
outline/server/commands/teamCreator.ts
Corey Alexander 51001cfac1 feat: Migrate allowedDomains to a Team Level Settings (#3489)
Fixes #3412

Previously the only way to restrict the domains for a Team were with the ALLOWED_DOMAINS environment variable for self hosted instances.
This PR migrates this to be a database backed setting on the Team object. This is done through the creation of a TeamDomain model that is associated with the Team and contains the domain name

This settings is updated on the Security Tab. Here domains can be added or removed from the Team.

On the server side, we take the code paths that previously were using ALLOWED_DOMAINS and switched them to use the Team allowed domains instead
2022-05-17 20:26:29 -04:00

137 lines
3.3 KiB
TypeScript

import invariant from "invariant";
import { DomainNotAllowedError, MaximumTeamsError } from "@server/errors";
import Logger from "@server/logging/logger";
import { APM } from "@server/logging/tracing";
import { Team, AuthenticationProvider } from "@server/models";
import { generateAvatarUrl } from "@server/utils/avatars";
type TeamCreatorResult = {
team: Team;
authenticationProvider: AuthenticationProvider;
isNewTeam: boolean;
};
type Props = {
name: string;
domain?: string;
subdomain: string;
avatarUrl?: string | null;
authenticationProvider: {
name: string;
providerId: string;
};
};
async function teamCreator({
name,
domain,
subdomain,
avatarUrl,
authenticationProvider,
}: Props): Promise<TeamCreatorResult> {
let authP = await AuthenticationProvider.findOne({
where: authenticationProvider,
include: [
{
model: Team,
as: "team",
required: true,
},
],
});
// This authentication provider already exists which means we have a team and
// there is nothing left to do but return the existing credentials
if (authP) {
return {
authenticationProvider: authP,
team: authP.team,
isNewTeam: false,
};
}
// This team has never been seen before, if self hosted the logic is different
// to the multi-tenant version, we want to restrict to a single team that MAY
// have multiple authentication providers
if (process.env.DEPLOYMENT !== "hosted") {
const teamCount = await Team.count();
// 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 (await team.isDomainAllowed(domain)) {
authP = await team.$create<AuthenticationProvider>(
"authenticationProvider",
authenticationProvider
);
return {
authenticationProvider: authP,
team,
isNewTeam: false,
};
} else {
throw DomainNotAllowedError();
}
}
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
if (!avatarUrl) {
avatarUrl = await generateAvatarUrl({
name,
domain,
id: subdomain,
});
}
const transaction = await Team.sequelize!.transaction();
let team;
try {
team = await Team.create(
{
name,
avatarUrl,
authenticationProviders: [authenticationProvider],
},
{
include: "authenticationProviders",
transaction,
}
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
try {
await team.provisionSubdomain(subdomain);
} catch (err) {
Logger.error("Provisioning subdomain failed", err, {
teamId: team.id,
subdomain,
});
}
return {
team,
authenticationProvider: team.authenticationProviders[0],
isNewTeam: true,
};
}
export default APM.traceFunction({
serviceName: "command",
spanName: "teamCreator",
})(teamCreator);