Merge pull request #14850 from ToolJet/feat/Added-password-domains

Added allowed and restricted domain for password login and signup.
This commit is contained in:
Adish M 2026-01-05 21:57:07 +05:30 committed by GitHub
commit 0dbf071c5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 456 additions and 41 deletions

View file

@ -266,6 +266,10 @@ Cypress.Commands.add(
.and(assertion, value, ...arg);
}
);
Cypress.Commands.add("scrollToElement", (selector) => {
cy.get(selector).scrollIntoView()
.should("be.visible");
});
Cypress.Commands.add("openInCurrentTab", (selector) => {
cy.get(selector).first().invoke("removeAttr", "target").click({ force: true });

View file

@ -749,6 +749,19 @@ Cypress.Commands.add(
});
}
);
Cypress.Commands.add(
"apiUpdateAutoSSO",
(state, scope = "instance", returnCached = false) => {
cy.getAuthHeaders(returnCached).then((headers) => {
cy.request({
method: "PATCH",
url: `${Cypress.env("server_host")}/api/login-configs/${scope}-general`,
headers: headers,
body: { automaticSsoLogin: state },
});
});
}
);
Cypress.Commands.add(
"apiFullUserOnboarding",
@ -782,7 +795,7 @@ Cypress.Commands.add(
performOnboarding(userEmail, userPassword, organizationToken);
}
function performOnboarding (email, password, orgToken) {
function performOnboarding(email, password, orgToken) {
cy.task("dbConnection", {
dbconfig: Cypress.env("app_db"),
sql: `

View file

@ -2,16 +2,14 @@ export const ssoSelector = {
pagetitle: "[data-cy=manage-sso-page-title]",
instanceLoginPage: {
cardTitle: "[data-cy=card-title]",
allowedDomainLabel: "[data-cy=allowed-domain-label]",
allowedDomainInput: "[data-cy=allowed-domains]",
allowedDomainHelperText: '[data-cy="allowed-allowedDomains-helper-text"]',
configurationLabel: "[data-cy=configuration-label]",
domainConstraintsLabel: "[data-cy=domain-constraints-label]",
domainConstraintsHelperText: '[data-cy="domain-constraints-helper-text"]',
superAdminUrlLabel: '[data-cy="workspace-login-url-label"]',
superAdminUrl: '[data-cy="workspace-login-url"]',
superAdminUrlHelperText: '[data-cy="workspace-login-help-text"]',
enableSignupLabel: '[data-cy="enable-sign-up-label"]',
enableSignupHelperText: "[data-cy=enable-sign-up-helper-text]",
passwordLoginLabel: '[data-cy="label-password-login"]',
passwordDisableHelperText: '[data-cy="disable-password-helper-text"]',
workspaceConfigurationLabel: '[data-cy="enable-workspace-configuration-label"]',
workspaceConfigurationHelperText: '[data-cy="enable-workspace-configuration-helper-text"]',
autoSSOLabel: '[data-cy="auto-sso-label"]',
@ -20,6 +18,9 @@ export const ssoSelector = {
customLogoutUrlLabel: '[data-cy="custom-logout-url-label"]',
customLogoutUrlPlaceholder: '[data-cy="custom-logout-url-input"]',
customLogoutUrlHelperText: '[data-cy="custom-logout-url-helper-text"]',
loginMethodsLabel: '[data-cy="login-methods-label"]',
passwordLoginLabel: '[data-cy="label-password-login"]',
passwordDisableHelperText: '[data-cy="disable-password-helper-text"]',
ssoHeader: '[data-cy="sso-header"]',
googleLabel: '[data-cy="google-label"]',
githubLabel: '[data-cy="github-label"]',
@ -27,18 +28,19 @@ export const ssoSelector = {
},
workspaceLoginPage: {
cardTitle: "[data-cy=card-title]",
allowedDomainLabel: "[data-cy=allowed-domain-label]",
allowedDomainInput: "[data-cy=allowed-domains]",
allowedDomainHelperText: '[data-cy="allowed-domain-helper-text"]',
configurationLabel: "[data-cy=configuration-label]",
domainConstraintsLabel: "[data-cy=domain-constraints-label]",
domainConstraintsHelperText: '[data-cy="domain-constraints-helper-text"]',
workspaceLoginUrlLabel: '[data-cy="workspace-login-url-label"]',
workspaceLoginUrl: '[data-cy="workspace-login-url"]',
workspaceLoginUrlHelperText: '[data-cy="workspace-login-help-text"]',
enableSignupLabel: '[data-cy="enable-sign-up-label"]',
enableSignupHelperText: "[data-cy=enable-sign-up-helper-text]",
passwordLoginLabel: '[data-cy="label-password-login"]',
passwordDisableHelperText: '[data-cy="disable-password-helper-text"]',
autoSSOLabel: '[data-cy="auto-sso-label"]',
autoSSOHelperText: '[data-cy="auto-sso-helper-text"]',
loginMethodsLabel: '[data-cy="login-methods-label"]',
passwordLoginLabel: '[data-cy="label-password-login"]',
passwordDisableHelperText: '[data-cy="disable-password-helper-text"]',
ssoHeader: '[data-cy="sso-header"]',
instanceSsoCard: '[data-cy="instance-sso-card"]',
instanceSsoHelperText: '[data-cy="instance-sso-helper-text"]',
@ -108,7 +110,16 @@ export const ssoSelector = {
allowDefaultSSOToggle:
'[style="padding-left: 0px; margin-bottom: 1px;"] > .switch > .slider',
defaultSSOImage: '[data-cy="default-sso-status-image"]',
allowedDomainInput: '[data-cy="allowed-domains"]',
passwordLoginDropdown: '[data-cy="password-login-dropdown"]',
passwordLoginDropdownLabel: '[data-cy="password-login-dropdown-label"]',
passwordAllowedDomainsLabel: '[data-cy="password-allowed-domains-label"]',
passwordAllowedDomainsInput: '[data-cy="password-allowed-domains-input"]',
passwordRestrictedDomainsLabel: '[data-cy="password-restricted-domains-label"]',
passwordRestrictedDomainsInput: '[data-cy="password-restricted-domains-input"]',
ssoLoginDropdown: '[data-cy="sso-login-dropdown"]',
ssoLoginDropdownLabel: '[data-cy="sso-login-dropdown-label"]',
ssoAllowedDomainsLabel: '[data-cy="sso-allowed-domains-label"]',
ssoAllowedDomainsInput: '[data-cy="sso-allowed-domains-input"]',
workspaceLoginUrl: '[data-cy="workspace-login-url"]',
cancelButton: "[data-cy=cancel-button]",
saveButton: "[data-cy=save-button]",

View file

@ -3,9 +3,9 @@ export const ssoText = {
instanceLoginPage: {
cardTitle: "Instance login",
allowedDomainLabel: "Allowed domains",
allowedDomainInput: "",
allowedDomainHelperText: "Support multiple domains. Enter allowed domains names separated by comma. example: tooljet.com,tooljet.io,yourorganization.com",
configurationLabel: "CONFIGURATION",
domainConstraintsLabel: "Domain constraints",
domainConstraintsHelperText: "Support multiple domains. Enter domain names separated by comma. Example: tooljet.com, tooljet.io",
superAdminUrlLabel: "Super admin login URL",
superAdminUrl: Cypress.env('proxy') === true
? `${Cypress.config("server_host")}/login/super-admin`
@ -13,8 +13,6 @@ export const ssoText = {
superAdminUrlHelperText: "Use this URL for super admin to login via password",
enableSignupLabel: "Enable Signup",
enableSignupHelperText: "Users will be able to sign up without being invited",
passwordLoginLabel: "Password login",
passwordDisableHelperText: 'Disable password login only if your SSO is configured otherwise you will get locked out',
workspaceConfigurationLabel: "Enable workspace configuration",
workspaceConfigurationHelperText: "Allow workspace admin to configure their workspaces login differently",
autoSSOLabel: "Automatic SSO login",
@ -23,6 +21,9 @@ export const ssoText = {
customLogoutUrlLabel: "Custom logout URL",
customLogoutUrlPlaceholder: "",
customLogoutUrlHelperText: "Set a personalized logout URL for users logging out of this instance.",
loginMethodsLabel: "LOGIN METHODS",
passwordLoginLabel: "Password login",
passwordDisableHelperText: 'Disable password login only if your SSO is configured otherwise you will get locked out',
ssoHeader: "SSO",
googleLabel: "Google",
githubLabel: "GitHub",
@ -30,9 +31,9 @@ export const ssoText = {
},
workspaceLoginPage: {
cardTitle: "Workspace login",
allowedDomainLabel: "Allowed domains",
allowedDomainInput: "",
allowedDomainHelperText: "Support multiple domains. Enter domain names separated by comma. example: tooljet.com,tooljet.io,yourorganization.com",
configurationLabel: "CONFIGURATION",
domainConstraintsLabel: "Domain constraints",
domainConstraintsHelperText: "Support multiple domains. Enter domain names separated by comma. Example: tooljet.com, tooljet.io",
workspaceLoginUrlLabel: "Login URL",
workspaceLoginUrl: Cypress.env('proxy') === true
? `${Cypress.config("server_host")}/login/workspace`
@ -40,10 +41,11 @@ export const ssoText = {
workspaceLoginUrlHelperText: "Use this URL to login directly to this workspace",
enableSignupLabel: "Enable signup",
enableSignupHelperText: "Users will be able to sign up without being invited",
passwordLoginLabel: "Password login",
passwordDisableHelperText: 'Disable password login only if your SSO is configured otherwise you will get locked out',
autoSSOLabel: "Automatic SSO login",
autoSSOHelperText: "This will simulate the configured SSO login, bypassing the login screen in ToolJet",
loginMethodsLabel: "LOGIN METHODS",
passwordLoginLabel: "Password login",
passwordDisableHelperText: 'Disable password login only if your SSO is configured otherwise you will get locked out',
ssoHeader: "SSO",
instanceSsoCard: "Instance SSO (3)",
instanceSsoHelperText: "Display instance SSO for workspace URL login",
@ -105,6 +107,11 @@ export const ssoText = {
cancelButton: "Cancel",
saveButton: "Save changes",
allowedDomain: "tooljet.io,gmail.com",
passwordLoginDropdownLabel: "Password login",
passwordAllowedDomainsLabel: "Allowed domains",
passwordRestrictedDomainsLabel: "Restricted domains",
ssoLoginDropdownLabel: "SSO login",
ssoAllowedDomainsLabel: "Allowed domains",
passwordDisableWarning: "Please ensure SSO is configured successfully before disabling password login or else you will get locked out. Are you sure you want to continue?",
superAdminInfoText: `Super admin can still access their account via ${
Cypress.env('proxy') === true

View file

@ -40,6 +40,7 @@ describe("Manage SSO for multi workspace", () => {
});
it("Should verify workspace settings page elements and their functionality", () => {
cy.apiUpdateAutoSSO(false, "organization");
SSO.setSignupStatus(false);
SSO.defaultSSO(true);
common.navigateToManageSSO();

View file

@ -11,10 +11,41 @@ import { commonText } from "Texts/common";
import { ssoText } from "Texts/manageSSO";
export const verifyLoginSettings = (pageName) => {
cy.get(ssoSelector.enableSignUpToggle).should("be.visible");
cy.get(ssoSelector.allowedDomainInput).should("be.visible");
cy.get(ssoSelector.workspaceLoginUrl).should("be.visible");
cy.get(commonSelectors.copyIcon).should("be.visible");
//Verify Password and SSO Domain section
cy.get(ssoSelector.passwordLoginDropdown).verifyVisibleElement("have.text", ssoText.passwordLoginDropdownLabel).click();
cy.get(ssoSelector.passwordAllowedDomainsLabel).should("be.visible");
cy.scrollToElement(ssoSelector.passwordAllowedDomainsInput).should("be.enabled");
cy.clearAndType(ssoSelector.passwordAllowedDomainsInput, "allow.com");
cy.scrollToElement(ssoSelector.passwordRestrictedDomainsLabel);
cy.scrollToElement(ssoSelector.passwordRestrictedDomainsInput).should("be.enabled");
cy.clearAndType(ssoSelector.passwordRestrictedDomainsInput, "restrict.com");
cy.get(ssoSelector.ssoLoginDropdown).verifyVisibleElement("have.text", ssoText.ssoLoginDropdownLabel).click();
cy.scrollToElement(ssoSelector.ssoAllowedDomainsLabel);
cy.scrollToElement(ssoSelector.ssoAllowedDomainsInput).should("be.enabled");
cy.clearAndType(ssoSelector.ssoAllowedDomainsInput, "allow.com");
cy.get(ssoSelector.saveButton).verifyVisibleElement("have.text", ssoText.saveButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
ssoText[`${pageName}SsoToast`]
);
[
ssoSelector.passwordAllowedDomainsInput,
ssoSelector.passwordRestrictedDomainsInput,
ssoSelector.ssoAllowedDomainsInput
].forEach(selector => {
cy.clearAndType(selector, `{selectall}{backspace}`);
});
cy.scrollToElement(ssoSelector.saveButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
ssoText[`${pageName}SsoToast`]
);
cy.scrollToElement(ssoSelector.ssoLoginDropdown).click();
cy.scrollToElement(ssoSelector.passwordLoginDropdown).click();
cy.scrollToElement(ssoSelector.workspaceLoginUrl);
cy.scrollToElement(commonSelectors.copyIcon);
cy.get(ssoSelector.cancelButton).verifyVisibleElement(
"have.text",
@ -24,8 +55,7 @@ export const verifyLoginSettings = (pageName) => {
"have.text",
ssoText.saveButton
);
cy.get(ssoSelector.passwordEnableToggle).should("be.visible");
cy.scrollToElement(ssoSelector.passwordEnableToggle);
//Configure sign up toggle
cy.get(ssoSelector.enableSignUpToggle).check();
@ -39,13 +69,6 @@ export const verifyLoginSettings = (pageName) => {
cy.get(ssoSelector.saveButton).click();
cy.get(ssoSelector.enableSignUpToggle).should("not.be.checked");
cy.clearAndType(ssoSelector.allowedDomainInput, ssoText.allowedDomain);
cy.get(ssoSelector.saveButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
ssoText[`${pageName}SsoToast`]
);
cy.get(ssoSelector.passwordEnableToggle).uncheck();
cy.get(commonSelectors.modalComponent).should("be.visible");
cy.get(ssoSelector.disablePasswordLoginTitle).verifyVisibleElement(

View file

@ -111,7 +111,7 @@ export default function WorkspaceSettingsPage({ extraLinks, ...props }) {
<OrganizationList />
</div>
<div className={cx('col workspace-content-wrapper')} style={{ paddingTop: '40px' }}>
<div className={cx('col workspace-content-wrapper')} style={{ paddingTop: '40px', scrollbarGutter: 'stable' }}>
<div className="w-100">
<Outlet />
</div>

View file

@ -0,0 +1,49 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import {
INSTANCE_CONFIGS_DATA_TYPES,
INSTANCE_SETTINGS_TYPE,
} from '@modules/instance-settings/constants';
import { INSTANCE_SYSTEM_SETTINGS } from '@modules/instance-settings/constants';
export class AddGranularDomainSettings1765958549099 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`
INSERT INTO instance_settings (key, label, data_type, value, type)
VALUES
(
'PASSWORD_ALLOWED_DOMAINS',
'Password Allowed Domains',
'${INSTANCE_CONFIGS_DATA_TYPES.TEXT}',
COALESCE(
(
SELECT value
FROM instance_settings
WHERE key = '${INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS}'
LIMIT 1
),
''
),
'${INSTANCE_SETTINGS_TYPE.SYSTEM}'
),
(
'PASSWORD_RESTRICTED_DOMAINS',
'Password Restricted Domains',
'${INSTANCE_CONFIGS_DATA_TYPES.TEXT}',
'',
'${INSTANCE_SETTINGS_TYPE.SYSTEM}'
)
ON CONFLICT (key) DO NOTHING;
`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`
DELETE FROM instance_settings
WHERE key IN ('PASSWORD_ALLOWED_DOMAINS', 'PASSWORD_RESTRICTED_DOMAINS');
`,
);
}
}

View file

@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddPasswordDomainColumnsToOrganizations1766224841342 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumns('organizations', [
new TableColumn({
name: 'password_allowed_domains',
type: 'varchar',
isNullable: true,
}),
new TableColumn({
name: 'password_restricted_domains',
type: 'varchar',
isNullable: true,
}),
]);
// Prefill password_allowed_domains with existing domain values
await queryRunner.query(
`UPDATE organizations SET password_allowed_domains = domain WHERE domain IS NOT NULL AND domain != ''`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('organizations', 'password_allowed_domains');
await queryRunner.dropColumn('organizations', 'password_restricted_domains');
}
}

View file

@ -37,6 +37,12 @@ export class Organization extends BaseEntity {
@Column({ name: 'domain' })
domain: string;
@Column({ name: 'password_allowed_domains', nullable: true })
passwordAllowedDomains: string;
@Column({ name: 'password_restricted_domains', nullable: true })
passwordRestrictedDomains: string;
@Column({ name: 'is_default', default: false })
isDefault: boolean;

View file

@ -9,6 +9,7 @@ import { getEnvVars } from 'scripts/database-config-utils';
import { decamelizeKeys } from 'humps';
import * as semver from 'semver';
import { BadRequestException } from '@nestjs/common';
import { INSTANCE_SYSTEM_SETTINGS } from '@modules/instance-settings/constants';
const PASSWORD_REGEX =
/^(?=.{12,24}$)[A-Za-z0-9!@#\$%\^&\*\(\)_+\-=\{\}\[\]:;\"',\.\?\/\\\|]+$/;
@ -465,6 +466,125 @@ export const isValidDomain = (email: string, restrictedDomain: string): boolean
return true;
};
/**
* Validates email domain for SSO sign-in with organization/instance hierarchy.
* Organization domain settings override instance settings.
* This feature is only available in EE edition. For CE edition, always returns true.
* @param email - User email address
* @param orgDomain - Organization domain setting (backward compatible with old 'domain' field)
* @param instanceAllowedDomains - Instance level SSO allowed domains
* @returns true if domain is valid, false otherwise
*/
export const isValidSSODomain = (
email: string,
orgDomain: string | null | undefined,
instanceAllowedDomains: string | null | undefined
): boolean => {
// Skip validation for CE edition, validate for EE edition
if (getTooljetEdition() === 'ce') {
return true;
}
if (!email) {
return false;
}
// Use organization domain if present (overrides instance), otherwise use instance setting
const allowedDomains = orgDomain || instanceAllowedDomains;
// If no domain restriction, allow all
if (!allowedDomains || !allowedDomains.trim()) {
return true;
}
const domain = email.substring(email.lastIndexOf('@') + 1);
if (!domain) {
return false;
}
// Check if domain is in allowed list
const allowedDomainList = allowedDomains
.split(',')
.map((e) => e && e.trim())
.filter((e) => !!e);
return allowedDomainList.includes(domain);
};
/**
* Validates email domain for password sign-in with organization/instance hierarchy.
* Organization domain settings override instance settings.
* Supports both allowed domains (whitelist) and restricted domains (blacklist).
* This feature is only available in EE edition. For CE edition, always returns true.
* @param email - User email address
* @param orgAllowedDomains - Organization level password allowed domains
* @param orgRestrictedDomains - Organization level password restricted domains
* @param instanceAllowedDomains - Instance level password allowed domains
* @param instanceRestrictedDomains - Instance level password restricted domains
* @returns true if domain is valid, false otherwise
*/
export const isValidPasswordDomain = (
email: string,
orgAllowedDomains: string | null | undefined,
orgRestrictedDomains: string | null | undefined,
instanceAllowedDomains: string | null | undefined,
instanceRestrictedDomains: string | null | undefined
): boolean => {
// Skip validation for CE edition, validate for EE edition
if (getTooljetEdition() === 'ce') {
return true;
}
if (!email) {
return false;
}
const domain = email.substring(email.lastIndexOf('@') + 1);
if (!domain) {
return false;
}
// Use organization settings if present (overrides instance), otherwise use instance settings
const allowedDomains = orgAllowedDomains || instanceAllowedDomains;
// Check restricted domains from both org and instance (blacklist takes precedence)
// If domain is restricted in either org or instance, deny access
if (orgRestrictedDomains && orgRestrictedDomains.trim()) {
const orgRestrictedDomainList = orgRestrictedDomains
.split(',')
.map((e) => e && e.trim())
.filter((e) => !!e);
if (orgRestrictedDomainList.includes(domain)) {
return false;
}
}
if (instanceRestrictedDomains && instanceRestrictedDomains.trim()) {
const instanceRestrictedDomainList = instanceRestrictedDomains
.split(',')
.map((e) => e && e.trim())
.filter((e) => !!e);
if (instanceRestrictedDomainList.includes(domain)) {
return false;
}
}
// Check allowed domains (whitelist)
if (allowedDomains && allowedDomains.trim()) {
const allowedDomainList = allowedDomains
.split(',')
.map((e) => e && e.trim())
.filter((e) => !!e);
return allowedDomainList.includes(domain);
}
// If no restrictions configured, allow all
return true;
};
export const isHttpsEnabled = () => {
return !!process.env.TOOLJET_HOST?.startsWith('https');
};
@ -567,3 +687,83 @@ export function throwIfSignalAborted(signal: AbortSignal, timeout: number) {
throw new QueryError(`Defined query timeout of ${timeout}ms exceeded when running query.`, 'Query timed out', {});
}
}
/**
* Validates email domain for SSO sign-in with organization/instance hierarchy.
* Fetches instance settings internally. Organization domain settings override instance settings.
* This feature is only available in EE edition. For CE edition, always returns true.
* @param email - User email address
* @param orgDomain - Organization domain setting (backward compatible with old 'domain' field)
* @param instanceSettingsUtilService - Instance settings util service to fetch settings
* @param isInstanceSSO - Whether this is instance-level SSO (domain already contains instance value)
* @returns Promise<boolean> - true if domain is valid, false otherwise
*/
export async function validateSSODomain(
email: string,
orgDomain: string | null | undefined,
instanceSettingsUtilService: any,
isInstanceSSO: boolean = false
): Promise<boolean> {
// Skip validation for CE edition, validate for EE edition
if (getTooljetEdition() === 'ce') {
return true;
}
if (!email) {
return false;
}
// For instance SSO, orgDomain already contains instance value, so use it directly
if (isInstanceSSO) {
return isValidSSODomain(email, null, orgDomain);
}
// Fetch instance settings
const instanceSettings = await instanceSettingsUtilService.getSettings([
INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS,
]);
const instanceAllowedDomains = instanceSettings?.ALLOWED_DOMAINS;
return isValidSSODomain(email, orgDomain, instanceAllowedDomains);
}
/**
* Validates email domain for password sign-in with organization/instance hierarchy.
* Fetches instance settings internally. Organization domain settings override instance settings.
* Supports both allowed domains (whitelist) and restricted domains (blacklist).
* This feature is only available in EE edition. For CE edition, always returns true.
* @param email - User email address
* @param orgAllowedDomains - Organization level password allowed domains
* @param orgRestrictedDomains - Organization level password restricted domains
* @param instanceSettingsUtilService - Instance settings util service to fetch settings
* @returns Promise<boolean> - true if domain is valid, false otherwise
*/
export async function validatePasswordDomain(
email: string,
orgAllowedDomains: string | null | undefined,
orgRestrictedDomains: string | null | undefined,
instanceSettingsUtilService: any
): Promise<boolean> {
// Skip validation for CE edition, validate for EE edition
if (getTooljetEdition() === 'ce') {
return true;
}
if (!email) {
return false;
}
// Fetch instance settings
const instanceSettings = await instanceSettingsUtilService.getSettings([
INSTANCE_SYSTEM_SETTINGS.PASSWORD_ALLOWED_DOMAINS,
INSTANCE_SYSTEM_SETTINGS.PASSWORD_RESTRICTED_DOMAINS,
]);
return isValidPasswordDomain(
email,
orgAllowedDomains,
orgRestrictedDomains,
instanceSettings?.PASSWORD_ALLOWED_DOMAINS,
instanceSettings?.PASSWORD_RESTRICTED_DOMAINS
);
}

View file

@ -4,7 +4,11 @@ import { Organization } from 'src/entities/organization.entity';
import { SSOConfigs } from 'src/entities/sso_config.entity';
import { EntityManager } from 'typeorm';
import { WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle';
import { isSuperAdmin, generateNextNameAndSlug } from 'src/helpers/utils.helper';
import {
isSuperAdmin,
generateNextNameAndSlug,
validatePasswordDomain,
} from 'src/helpers/utils.helper';
import { dbTransactionWrap } from 'src/helpers/database.helper';
import { InstanceSettingsUtilService } from '@modules/instance-settings/util.service';
import { Response } from 'express';
@ -96,6 +100,20 @@ export class AuthService implements IAuthService {
if (organization) user.organizationId = organization.id;
/* CASE: No active workspace. But one workspace with invited status. waiting for activation */
if (isInviteRedirect && !organization) user.organizationId = invitingOrganizationId ?? '';
// Validate password domain for global login (org overrides instance)
if (organization && !isSuperAdmin(user)) {
if (
!(await validatePasswordDomain(
email,
organization.passwordAllowedDomains,
organization.passwordRestrictedDomains,
this.instanceSettingsUtilService
))
) {
throw new UnauthorizedException(`This login method is not available for your domain. Please contact admin or try another method.`);
}
}
} else {
// organization specific login
// No need to validate user status, validateUser() already covers it
@ -109,6 +127,20 @@ export class AuthService implements IAuthService {
// no configurations in organization side or Form login disabled for the organization
throw new UnauthorizedException('Password login is disabled for the organization');
}
// Validate password domain with org/instance hierarchy (org overrides instance)
if (!isSuperAdmin(user)) {
if (
!(await validatePasswordDomain(
email,
organization.passwordAllowedDomains,
organization.passwordRestrictedDomains,
this.instanceSettingsUtilService
))
) {
throw new UnauthorizedException(`This login method is not available for your domain. Please contact admin or try another method.`);
}
}
}
const shouldUpdateDefaultOrgId =

View file

@ -56,6 +56,8 @@ export class ConfigService implements IConfigService {
INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE,
INSTANCE_USER_SETTINGS.ENABLE_MULTIPLAYER_EDITING,
INSTANCE_USER_SETTINGS.ENABLE_COMMENTS,
INSTANCE_SYSTEM_SETTINGS.PASSWORD_ALLOWED_DOMAINS,
INSTANCE_SYSTEM_SETTINGS.PASSWORD_RESTRICTED_DOMAINS,
INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS,
INSTANCE_SYSTEM_SETTINGS.ENABLE_SIGNUP,
INSTANCE_SYSTEM_SETTINGS.ENABLE_WORKSPACE_LOGIN_CONFIGURATION,

View file

@ -12,6 +12,8 @@ export enum INSTANCE_SETTINGS_TYPE {
export enum INSTANCE_SYSTEM_SETTINGS {
ALLOWED_DOMAINS = 'ALLOWED_DOMAINS',
PASSWORD_ALLOWED_DOMAINS = 'PASSWORD_ALLOWED_DOMAINS',
PASSWORD_RESTRICTED_DOMAINS = 'PASSWORD_RESTRICTED_DOMAINS',
ENABLE_SIGNUP = 'ENABLE_SIGNUP',
ENABLE_WORKSPACE_LOGIN_CONFIGURATION = 'ENABLE_WORKSPACE_LOGIN_CONFIGURATION',
AUTOMATIC_SSO_LOGIN = 'AUTOMATIC_SSO_LOGIN',

View file

@ -9,6 +9,18 @@ export class OrganizationConfigsUpdateDto {
@MaxLength(250, { message: 'Domain cannot be longer than 250 characters' })
domain?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => sanitizeInput(value))
@MaxLength(250, { message: 'Password allowed domains cannot be longer than 250 characters' })
passwordAllowedDomains?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => sanitizeInput(value))
@MaxLength(250, { message: 'Password restricted domains cannot be longer than 250 characters' })
passwordRestrictedDomains?: string;
@IsOptional()
@IsBoolean()
enableSignUp?: boolean;
@ -28,6 +40,18 @@ export class InstanceConfigsUpdateDto {
@Transform(({ value }) => sanitizeInput(value))
@MaxLength(250, { message: 'Domains cannot be longer than 250 characters' })
allowedDomains?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => sanitizeInput(value))
@MaxLength(250, { message: 'Allowed domains cannot be longer than 250 characters' })
passwordAllowedDomains?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => sanitizeInput(value))
@MaxLength(250, { message: 'Restricted domains cannot be longer than 250 characters' })
passwordRestrictedDomains?: string;
@IsOptional()
@IsBoolean()

View file

@ -145,10 +145,12 @@ export class LoginConfigsService implements ILoginConfigsService {
async updateGeneralOrganizationConfigs(user: User, params: OrganizationConfigsUpdateDto) {
const organizationId = user.organizationId;
const { domain, enableSignUp, inheritSSO, automaticSsoLogin } = params;
const { domain, passwordAllowedDomains, passwordRestrictedDomains, enableSignUp, inheritSSO, automaticSsoLogin } = params;
const updatableParams = {
domain,
passwordAllowedDomains,
passwordRestrictedDomains,
enableSignUp,
inheritSSO,
automaticSsoLogin,

View file

@ -32,6 +32,7 @@ import {
isValidDomain,
generateWorkspaceSlug,
validatePasswordServer,
validatePasswordDomain,
} from 'src/helpers/utils.helper';
import { dbTransactionWrap } from 'src/helpers/database.helper';
import { Response } from 'express';
@ -99,12 +100,21 @@ export class OnboardingService implements IOnboardingService {
throw new NotFoundException('Could not found organization details. Please verify the orgnization id');
}
/* Check if the workspace allows user signup or not */
const { enableSignUp, domain } = signingUpOrganization;
const { enableSignUp, passwordAllowedDomains, passwordRestrictedDomains } = signingUpOrganization;
if (!enableSignUp) {
throw new ForbiddenException('Workspace signup has been disabled. Please contact the workspace admin.');
}
if (!isValidDomain(email, domain)) {
throw new ForbiddenException('You cannot sign up using the email address - Domain verification failed.');
if (
!(await validatePasswordDomain(email, passwordAllowedDomains, passwordRestrictedDomains, this.instanceSettingsUtilService))
) {
throw new ForbiddenException('This login method is not available for your domain. Please contact admin or try another method.');
}
} else {
// No organization provided - validate against instance-level settings
if (
!(await validatePasswordDomain(email, undefined, undefined, this.instanceSettingsUtilService))
) {
throw new ForbiddenException('This login method is not available for your domain. Please contact admin or try another method.');
}
}