chore: Subdomain blacklist when creating org (#9728)

Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Leo Giovanetti
2023-06-27 20:32:19 +00:00
committed by GitHub
co-authored by Bailey Pumfleet Omar López
parent d8017e4d2d
commit b799fdcb7e
6 changed files with 37 additions and 29 deletions
+6 -2
View File
@@ -59,6 +59,11 @@ SAML_CLIENT_SECRET_VERIFIER=
# PGSSLMODE='no-verify'
PGSSLMODE=
# Define which hostnames are expected for the app to work on
ALLOWED_HOSTNAMES='"cal.com","cal.dev","cal-staging.com","cal.community","cal.local:3000","localhost:3000"'
# Reserved orgs subdomains for our own usage
RESERVED_SUBDOMAINS='"app","auth","docs","design","console","go","status","api","saml","www","matrix","developer","cal","my","team","support","security","blog","learn","admin"'
# - NEXTAUTH
# @see: https://github.com/calendso/calendso/issues/263
# @see: https://next-auth.js.org/configuration/options#nextauth_url
@@ -172,7 +177,6 @@ CLOSECOM_API_KEY=
# Sendgrid internal sync service
SENDGRID_SYNC_API_KEY=
# Change your Brand
NEXT_PUBLIC_APP_NAME="Cal.com"
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
@@ -210,4 +214,4 @@ AUTH_BEARER_TOKEN_VERCEL=
NEW_BOOKER_ENABLED_FOR_EMBED=0
#Enables New booker for All but Embed requests
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
@@ -1,15 +1,4 @@
import { WEBAPP_URL } from "@calcom/lib/constants";
// Define which hostnames are expected for the app
export const appHostnames = [
"cal.com",
"cal.dev",
"cal-staging.com",
"cal.community",
"cal.local:3000",
// ⬇️ Prevents 404 error for normal localhost development, makes it backwards compatible
"localhost:3000",
];
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
/**
* return the org slug
@@ -17,7 +6,7 @@ export const appHostnames = [
*/
export function getOrgDomain(hostname: string) {
// Find which hostname is being currently used
const currentHostname = appHostnames.find((ahn) => {
const currentHostname = ALLOWED_HOSTNAMES.find((ahn) => {
const url = new URL(WEBAPP_URL);
const testHostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
return testHostname.endsWith(`.${ahn}`);
@@ -34,8 +23,7 @@ export function orgDomainConfig(hostname: string) {
const currentOrgDomain = getOrgDomain(hostname);
return {
currentOrgDomain,
isValidOrgDomain:
currentOrgDomain !== null && currentOrgDomain !== "app" && !appHostnames.includes(currentOrgDomain),
isValidOrgDomain: currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain),
};
}
+19 -10
View File
@@ -1,47 +1,56 @@
import { describe, expect, it } from "vitest";
import { orgDomainConfig, getOrgDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
import * as constants from "@calcom/lib/constants";
function setupEnvs({ WEBAPP_URL = "https://app.cal.com" } = {}) {
Object.defineProperty(constants, "WEBAPP_URL", { value: WEBAPP_URL });
Object.defineProperty(constants, "ALLOWED_HOSTNAMES", {
value: ["cal.com", "cal.dev", "cal-staging.com", "cal.community", "cal.local:3000", "localhost:3000"],
});
Object.defineProperty(constants, "RESERVED_SUBDOMAINS", {
value: [ "app", "auth", "docs", "design", "console", "go", "status", "api", "saml", "www", "matrix", "developer", "cal", "my", "team", "support", "security", "blog", "learn", "admin"],
});
}
describe("Org Domains Utils", () => {
describe("orgDomainConfig", () => {
it("should return a valid org domain", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
setupEnvs();
expect(orgDomainConfig("acme.cal.com")).toEqual({
currentOrgDomain: "acme",
isValidOrgDomain: true
isValidOrgDomain: true,
});
});
it("should return a non valid org domain", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
setupEnvs();
expect(orgDomainConfig("app.cal.com")).toEqual({
currentOrgDomain: "app",
isValidOrgDomain: false
isValidOrgDomain: false,
});
});
});
describe("getOrgDomain", () => {
it("should handle a prod web app url with a prod subdomain hostname", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
setupEnvs();
expect(getOrgDomain("acme.cal.com")).toEqual("acme");
});
it("should handle a prod web app url with a staging subdomain hostname", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
setupEnvs();
expect(getOrgDomain("acme.cal.dev")).toEqual(null);
});
it("should handle a local web app with port url with a local subdomain hostname", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"http://app.cal.local:3000"});
setupEnvs({ WEBAPP_URL: "http://app.cal.local:3000" });
expect(getOrgDomain("acme.cal.local:3000")).toEqual("acme");
});
it("should handle a local web app with port url with a non-local subdomain hostname", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"http://app.cal.local:3000"});
setupEnvs({ WEBAPP_URL: "http://app.cal.local:3000" });
expect(getOrgDomain("acme.cal.com:3000")).toEqual(null);
});
})
});
});
+4
View File
@@ -70,3 +70,7 @@ export const IS_STRIPE_ENABLED = !!(
export const IS_TEAM_BILLING_ENABLED = IS_STRIPE_ENABLED && (!IS_SELF_HOSTED || HOSTED_CAL_FEATURES);
export const FULL_NAME_LENGTH_MAX_LIMIT = 50;
export const MINUTES_TO_BOOK = process.env.NEXT_PUBLIC_MINUTES_TO_BOOK || "5";
// Needed for orgs
export const ALLOWED_HOSTNAMES = JSON.parse(`[${process.env.ALLOWED_HOSTNAMES || ""}]`) as string[];
export const RESERVED_SUBDOMAINS = JSON.parse(`[${process.env.RESERVED_SUBDOMAINS || ""}]`) as string[];
@@ -4,7 +4,7 @@ import { totp } from "otplib";
import { sendOrganizationEmailVerification } from "@calcom/emails";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { IS_PRODUCTION, IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { IS_PRODUCTION, IS_TEAM_BILLING_ENABLED, RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
@@ -66,7 +66,8 @@ export const createHandler = async ({ input }: CreateOptions) => {
},
});
if (slugCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "organization_url_taken" });
if (slugCollisions || RESERVED_SUBDOMAINS.includes(slug))
throw new TRPCError({ code: "BAD_REQUEST", message: "organization_url_taken" });
if (userCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "admin_email_taken" });
const password = createHash("md5")
+2
View File
@@ -174,6 +174,7 @@
},
"globalDependencies": ["yarn.lock"],
"globalEnv": [
"ALLOWED_HOSTNAMES",
"ANALYZE",
"API_KEY_PREFIX",
"APP_USER_NAME",
@@ -246,6 +247,7 @@
"QUICK",
"RAILWAY_STATIC_URL",
"RENDER_EXTERNAL_URL",
"RESERVED_SUBDOMAINS",
"SALESFORCE_CONSUMER_KEY",
"SALESFORCE_CONSUMER_SECRET",
"SAML_ADMINS",