mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
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:
commit
0dbf071c5f
17 changed files with 456 additions and 41 deletions
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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: `
|
||||
|
|
|
|||
|
|
@ -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]",
|
||||
|
|
|
|||
|
|
@ -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 workspace’s 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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue