mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add sso_server_url configuration for dual URL SSO setups (#31497)
This change allows configuring a separate URL for SSO callbacks, which is useful when organizations have different URLs for admin access vs agent/API access. Fixes #31480 the SSO issue where organizations with dual URL setups were getting 'Destination does not match requested URL' errors after upgrading to v4.71.0 with the new SAML library. Video demo: https://www.youtube.com/watch?v=dFzNpUY3XKI # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [ ] QA'd all new/changed functionality manually ## New Fleet configuration settings - [x] Verified that the setting is exported via `fleetctl generate-gitops` - [x] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - Same PR since this is going to be a 4.71.1 patch - [ ] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [x] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for configuring a dedicated SSO URL, allowing organizations to restrict SSO authentication to a specific URL. * The new SSO URL option is available in both the UI and API configuration settings. * **Documentation** * Updated configuration and API documentation to include the new SSO URL option with usage examples. * **Bug Fixes** * Resolved authentication issues for organizations using separate URLs for admin and agent/API access. * **Tests** * Added new unit and integration tests to verify SSO behavior with and without the dedicated SSO URL. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
497401a75b
commit
949a1eeabb
24 changed files with 238 additions and 9 deletions
1
changes/31480-fix-sso-alternate-url
Normal file
1
changes/31480-fix-sso-alternate-url
Normal file
|
|
@ -0,0 +1 @@
|
|||
Added `sso_server_url` configuration option to support SSO setups with separate URLs for admin access vs agent/API access. When set, SSO authentication will only work from the specified URL. This fixes SSO authentication errors for organizations using dual URL configurations.
|
||||
|
|
@ -647,6 +647,7 @@ func (cmd *GenerateGitopsCommand) generateSSOSettings(ssoSettings *fleet.SSOSett
|
|||
jsonFieldName(t, "Metadata"): ssoSettings.Metadata,
|
||||
jsonFieldName(t, "MetadataURL"): ssoSettings.MetadataURL,
|
||||
jsonFieldName(t, "EnableSSOIdPLogin"): ssoSettings.EnableSSOIdPLogin,
|
||||
jsonFieldName(t, "SSOServerURL"): ssoSettings.SSOServerURL,
|
||||
}
|
||||
if cmd.AppConfig.License.IsPremium() {
|
||||
result[jsonFieldName(t, "EnableJITProvisioning")] = ssoSettings.EnableJITProvisioning
|
||||
|
|
|
|||
|
|
@ -2955,6 +2955,55 @@ func TestGitOpsSSOSettings(t *testing.T) {
|
|||
require.Nil(t, appConfig.SSOSettings)
|
||||
}
|
||||
|
||||
func TestGitOpsSSOServerURL(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
globalFile, err := os.CreateTemp(tmpDir, "*.yml")
|
||||
require.NoError(t, err)
|
||||
_, err = globalFile.WriteString(`
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: ` + fleetServerURL + `
|
||||
org_info:
|
||||
org_name: ` + orgName + `
|
||||
sso_settings:
|
||||
entity_id: "test-entity"
|
||||
idp_name: "Test IdP"
|
||||
metadata: "<xml>test-metadata</xml>"
|
||||
enable_sso: true
|
||||
sso_server_url: "https://sso.example.com"
|
||||
secrets:
|
||||
- secret: test-secret
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
globalFile.Close()
|
||||
|
||||
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
||||
|
||||
appConfig := fleet.AppConfig{}
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &appConfig, nil
|
||||
}
|
||||
|
||||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||||
appConfig = *config
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run GitOps with SSO settings including sso_url
|
||||
_, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name()})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, appConfig.SSOSettings)
|
||||
require.Equal(t, "https://sso.example.com", appConfig.SSOSettings.SSOServerURL)
|
||||
require.Equal(t, "test-entity", appConfig.SSOSettings.EntityID)
|
||||
require.True(t, appConfig.SSOSettings.EnableSSO)
|
||||
}
|
||||
|
||||
func TestGitOpsSMTPSettings(t *testing.T) {
|
||||
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
|
||||
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@
|
|||
"enable_jit_provisioning": false,
|
||||
"enable_jit_role_sync": false,
|
||||
"enable_sso": false,
|
||||
"enable_sso_idp_login": false
|
||||
"enable_sso_idp_login": false,
|
||||
"sso_server_url": ""
|
||||
},
|
||||
"fleet_desktop": {
|
||||
"transparency_url": "https://fleetdm.com/transparency"
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ spec:
|
|||
issuer_uri: ""
|
||||
metadata: ""
|
||||
metadata_url: ""
|
||||
sso_server_url: ""
|
||||
vulnerability_settings:
|
||||
databases_path: /some/path
|
||||
webhook_settings:
|
||||
|
|
|
|||
|
|
@ -112,7 +112,8 @@
|
|||
"metadata_url": "",
|
||||
"idp_name": "",
|
||||
"enable_sso": false,
|
||||
"enable_sso_idp_login": false
|
||||
"enable_sso_idp_login": false,
|
||||
"sso_server_url": ""
|
||||
},
|
||||
"fleet_desktop": {
|
||||
"transparency_url": "https://fleetdm.com/transparency"
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ spec:
|
|||
issuer_uri: ""
|
||||
metadata: ""
|
||||
metadata_url: ""
|
||||
sso_server_url: ""
|
||||
update_interval:
|
||||
osquery_detail: 1h0m0s
|
||||
osquery_policy: 1h0m0s
|
||||
|
|
|
|||
|
|
@ -143,7 +143,8 @@
|
|||
"enable_sso": true,
|
||||
"enable_sso_idp_login": false,
|
||||
"enable_jit_provisioning": true,
|
||||
"enable_jit_role_sync": false
|
||||
"enable_jit_role_sync": false,
|
||||
"sso_server_url": "https://sso.fleetdm.com"
|
||||
},
|
||||
"fleet_desktop": {
|
||||
"transparency_url": "https://fleetdm.com/transparency"
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ sso_settings:
|
|||
idp_name: some-idp-name
|
||||
metadata: some-sso-metadata
|
||||
metadata_url: http://some-sso-metadata-url.com
|
||||
sso_server_url: https://sso.fleetdm.com
|
||||
webhook_settings:
|
||||
activities_webhook:
|
||||
destination_url: https://some-activities-webhook-url.com
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ sso_settings:
|
|||
idp_name: some-idp-name
|
||||
metadata: ___GITOPS_COMMENT_9___
|
||||
metadata_url: ___GITOPS_COMMENT_10___
|
||||
sso_server_url: https://sso.fleetdm.com
|
||||
webhook_settings:
|
||||
activities_webhook:
|
||||
destination_url: https://some-activities-webhook-url.com
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ org_settings:
|
|||
idp_name: some-idp-name
|
||||
metadata: # TODO: Add your SSO metadata here
|
||||
metadata_url: # TODO: Add your SSO metadata URL here
|
||||
sso_server_url: https://sso.fleetdm.com
|
||||
webhook_settings:
|
||||
activities_webhook:
|
||||
destination_url: https://some-activities-webhook-url.com
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ org_settings:
|
|||
idp_name: some-idp-name
|
||||
metadata: # TODO: Add your SSO metadata here
|
||||
metadata_url: # TODO: Add your SSO metadata URL here
|
||||
sso_server_url: https://sso.fleetdm.com
|
||||
webhook_settings:
|
||||
activities_webhook:
|
||||
destination_url: https://some-activities-webhook-url.com
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ spec:
|
|||
issuer_uri: ""
|
||||
metadata: ""
|
||||
metadata_url: ""
|
||||
sso_server_url: ""
|
||||
vulnerability_settings:
|
||||
databases_path: ""
|
||||
webhook_settings:
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ spec:
|
|||
issuer_uri: ""
|
||||
metadata: ""
|
||||
metadata_url: ""
|
||||
sso_server_url: ""
|
||||
vulnerability_settings:
|
||||
databases_path: ""
|
||||
webhook_settings:
|
||||
|
|
|
|||
|
|
@ -664,6 +664,7 @@ org_settings:
|
|||
metadata: $SSO_METADATA
|
||||
enable_jit_provisioning: true # Available in Fleet Premium
|
||||
enable_sso_idp_login: true
|
||||
sso_server_url: https://admin.example.com # Optional, SSO will only work from this URL
|
||||
```
|
||||
|
||||
### integrations
|
||||
|
|
|
|||
|
|
@ -802,7 +802,8 @@ None.
|
|||
"idp_name": "",
|
||||
"enable_sso": false,
|
||||
"enable_sso_idp_login": false,
|
||||
"enable_jit_provisioning": false
|
||||
"enable_jit_provisioning": false,
|
||||
"sso_server_url": ""
|
||||
},
|
||||
"conditional_access": {
|
||||
"microsoft_entra_tenant_id": "<TENANT ID>",
|
||||
|
|
@ -1443,7 +1444,8 @@ Modifies the Fleet's configuration with the supplied information.
|
|||
"metadata": "",
|
||||
"idp_name": "",
|
||||
"enable_sso_idp_login": false,
|
||||
"enable_jit_provisioning": false
|
||||
"enable_jit_provisioning": false,
|
||||
"sso_server_url": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ export interface IConfig {
|
|||
enable_sso_idp_login: boolean;
|
||||
enable_jit_provisioning: boolean;
|
||||
enable_jit_role_sync: boolean;
|
||||
sso_server_url?: string;
|
||||
};
|
||||
// configuration details for conditional access. For enabled/disabled status per team, see
|
||||
// subfields under `integrations`
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { IAppConfigFormProps, IFormField } from "../constants";
|
|||
const baseClass = "app-config-form";
|
||||
|
||||
interface IAdvancedConfigFormData {
|
||||
ssoUserURL: string;
|
||||
mdmAppleServerURL: string;
|
||||
domain: string;
|
||||
verifySSLCerts: boolean;
|
||||
|
|
@ -33,12 +34,14 @@ interface IAdvancedConfigFormData {
|
|||
}
|
||||
|
||||
interface IAdvancedConfigFormErrors {
|
||||
ssoUserURL?: string | null;
|
||||
mdmAppleServerURL?: string | null;
|
||||
domain?: string | null;
|
||||
hostExpiryWindow?: string | null;
|
||||
}
|
||||
|
||||
const validateFormData = ({
|
||||
ssoUserURL,
|
||||
mdmAppleServerURL,
|
||||
domain,
|
||||
hostExpiryWindow,
|
||||
|
|
@ -46,6 +49,12 @@ const validateFormData = ({
|
|||
}: IAdvancedConfigFormData) => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!ssoUserURL) {
|
||||
delete errors.ssoUserURL;
|
||||
} else if (!validUrl({ url: ssoUserURL })) {
|
||||
errors.ssoUserURL = `${ssoUserURL} is not a valid URL`;
|
||||
}
|
||||
|
||||
if (!mdmAppleServerURL) {
|
||||
delete errors.mdmAppleServerURL;
|
||||
} else if (!validUrl({ url: mdmAppleServerURL })) {
|
||||
|
|
@ -75,6 +84,7 @@ const Advanced = ({
|
|||
const gitOpsModeEnabled = appConfig.gitops.gitops_mode_enabled;
|
||||
|
||||
const [formData, setFormData] = useState<IAdvancedConfigFormData>({
|
||||
ssoUserURL: appConfig.sso_settings?.sso_server_url || "",
|
||||
mdmAppleServerURL: appConfig.mdm?.apple_server_url || "",
|
||||
domain: appConfig.smtp_settings?.domain || "",
|
||||
verifySSLCerts: appConfig.smtp_settings?.verify_ssl_certs || false,
|
||||
|
|
@ -97,6 +107,7 @@ const Advanced = ({
|
|||
});
|
||||
|
||||
const {
|
||||
ssoUserURL,
|
||||
mdmAppleServerURL,
|
||||
domain,
|
||||
verifySSLCerts,
|
||||
|
|
@ -179,6 +190,9 @@ const Advanced = ({
|
|||
mdm: {
|
||||
apple_server_url: mdmAppleServerURL,
|
||||
},
|
||||
sso_settings: {
|
||||
sso_server_url: ssoUserURL,
|
||||
},
|
||||
};
|
||||
|
||||
handleSubmit(formDataToSubmit);
|
||||
|
|
@ -193,6 +207,25 @@ const Advanced = ({
|
|||
Most users do not need to modify these options.
|
||||
</p>
|
||||
<div className="form">
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="left"
|
||||
renderChildren={(disableChildren) => (
|
||||
<InputField
|
||||
disabled={disableChildren}
|
||||
label="SSO user URL"
|
||||
onChange={onInputChange}
|
||||
onBlur={onInputBlur}
|
||||
name="ssoUserURL"
|
||||
value={ssoUserURL}
|
||||
parseTarget
|
||||
error={formErrors.ssoUserURL}
|
||||
tooltip={
|
||||
!disableChildren &&
|
||||
"Update this URL if you want your Fleet users (admins, maintainers, observers) to login via SSO using a URL that's different than the base URL of your Fleet instance. If not configured, login via SSO will use the base URL of the Fleet instance."
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{appConfig.mdm.enabled_and_configured && (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="left"
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ CREATE TABLE `app_config_json` (
|
|||
PRIMARY KEY (`id`)
|
||||
) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"manual_agent_install\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"windows_require_bitlocker_pin\": null, \"android_enabled_and_configured\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"gitops\": {\"repository_url\": \"\", \"gitops_mode_enabled\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null, \"custom_scep_proxy\": null, \"conditional_access_enabled\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
|
||||
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"manual_agent_install\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"windows_require_bitlocker_pin\": null, \"android_enabled_and_configured\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"gitops\": {\"repository_url\": \"\", \"gitops_mode_enabled\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null, \"custom_scep_proxy\": null, \"conditional_access_enabled\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"sso_server_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `batch_script_execution_host_results` (
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ type SSOSettings struct {
|
|||
// EnableJITRoleSync sets whether the roles of existing accounts will be updated
|
||||
// every time SSO users log in (does not have effect if EnableJITProvisioning is false).
|
||||
EnableJITRoleSync bool `json:"enable_jit_role_sync"`
|
||||
// SSOServerURL is an optional URL to use for SSO authentication.
|
||||
// When set, SSO will only work from this URL, not from the server URL.
|
||||
// This is useful for organizations with separate URLs for admin access vs agent/API access.
|
||||
SSOServerURL string `json:"sso_server_url"`
|
||||
}
|
||||
|
||||
// ConditionalAccessSettings holds the global settings for the "Conditional access" feature.
|
||||
|
|
|
|||
|
|
@ -528,3 +528,59 @@ func (s *integrationSSOTestSuite) TestSSOLoginSAMLResponseTampered() {
|
|||
require.NoError(t, err)
|
||||
require.Contains(t, string(body), "/login?status=error")
|
||||
}
|
||||
|
||||
func (s *integrationSSOTestSuite) TestSSOServerURL() {
|
||||
t := s.T()
|
||||
|
||||
// Use the test metadata instead of trying to fetch from localhost:9080
|
||||
testMetadata := `
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="http://localhost:9080/simplesaml/saml2/idp/metadata.php">
|
||||
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:9080/simplesaml/saml2/idp/SingleLogoutService.php"/>
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
|
||||
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:9080/simplesaml/saml2/idp/SSOService.php"/>
|
||||
</md:IDPSSODescriptor>
|
||||
</md:EntityDescriptor>`
|
||||
|
||||
// Configure SSO with a specific SSO server URL and inline metadata
|
||||
acResp := appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
|
||||
"sso_settings": {
|
||||
"enable_sso": true,
|
||||
"entity_id": "https://localhost:8080",
|
||||
"idp_name": "SimpleSAML",
|
||||
"metadata": %q,
|
||||
"enable_jit_provisioning": false,
|
||||
"sso_server_url": "https://admin.localhost:8080"
|
||||
}
|
||||
}`, testMetadata)), http.StatusOK, &acResp)
|
||||
require.NotNil(t, acResp)
|
||||
|
||||
// Verify the SSO server URL is set
|
||||
require.NotNil(t, acResp.SSOSettings)
|
||||
require.Equal(t, "https://admin.localhost:8080", acResp.SSOSettings.SSOServerURL)
|
||||
|
||||
// Initiate SSO
|
||||
var resIni initiateSSOResponse
|
||||
s.DoJSON("POST", "/api/v1/fleet/sso", map[string]string{}, http.StatusOK, &resIni)
|
||||
require.NotEmpty(t, resIni.URL)
|
||||
|
||||
// Parse the auth request to verify it uses the SSO URL
|
||||
parsed, err := url.Parse(resIni.URL)
|
||||
require.NoError(t, err)
|
||||
q := parsed.Query()
|
||||
encoded := q.Get("SAMLRequest")
|
||||
assert.NotEmpty(t, encoded)
|
||||
authReq := inflate(t, encoded)
|
||||
|
||||
// Check that the ACS URL in the auth request uses the SSO server URL
|
||||
require.NotNil(t, authReq.AssertionConsumerServiceURL)
|
||||
assert.Equal(t, "https://admin.localhost:8080/api/v1/fleet/sso/callback", authReq.AssertionConsumerServiceURL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -441,7 +441,12 @@ func (svc *Service) InitiateSSO(ctx context.Context, redirectURL string) (sessio
|
|||
}
|
||||
|
||||
serverURL := appConfig.ServerSettings.ServerURL
|
||||
acsURL := serverURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback"
|
||||
// Use SSO server URL if configured, otherwise use the server URL
|
||||
ssoURL := serverURL
|
||||
if appConfig.SSOSettings != nil && appConfig.SSOSettings.SSOServerURL != "" {
|
||||
ssoURL = appConfig.SSOSettings.SSOServerURL
|
||||
}
|
||||
acsURL := ssoURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback"
|
||||
|
||||
// If entityID is not explicitly set, default to host name.
|
||||
//
|
||||
|
|
@ -650,7 +655,12 @@ func (svc *Service) InitSSOCallback(
|
|||
}
|
||||
|
||||
serverURL := appConfig.ServerSettings.ServerURL
|
||||
acsURL, err := url.Parse(serverURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback")
|
||||
// Use SSO server URL if configured, otherwise use the server URL
|
||||
ssoURL := serverURL
|
||||
if appConfig.SSOSettings != nil && appConfig.SSOSettings.SSOServerURL != "" {
|
||||
ssoURL = appConfig.SSOSettings.SSOServerURL
|
||||
}
|
||||
acsURL, err := url.Parse(ssoURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback")
|
||||
if err != nil {
|
||||
return nil, "", ctxerr.Wrap(ctx, err, "failed to parse ACS URL")
|
||||
}
|
||||
|
|
@ -658,7 +668,14 @@ func (svc *Service) InitSSOCallback(
|
|||
expectedAudiences := []string{
|
||||
appConfig.SSOSettings.EntityID,
|
||||
appConfig.ServerSettings.ServerURL,
|
||||
appConfig.ServerSettings.ServerURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback", // ACS
|
||||
appConfig.ServerSettings.ServerURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback", // ACS with server URL
|
||||
}
|
||||
// Add SSO server URL to expected audiences if configured
|
||||
if appConfig.SSOSettings != nil && appConfig.SSOSettings.SSOServerURL != "" {
|
||||
expectedAudiences = append(expectedAudiences,
|
||||
appConfig.SSOSettings.SSOServerURL,
|
||||
appConfig.SSOSettings.SSOServerURL+svc.config.Server.URLPrefix+"/api/v1/fleet/sso/callback", // ACS with SSO server URL
|
||||
)
|
||||
}
|
||||
samlProvider, requestID, redirectURL, err := sso.SAMLProviderFromSessionOrConfiguredMetadata(
|
||||
ctx, sessionID, svc.ssoSessionStore, acsURL, appConfig.SSOSettings, expectedAudiences,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
|
|
@ -436,3 +437,54 @@ func TestGetSSOUser(t *testing.T) {
|
|||
_, err = svc.GetSSOUser(ctx, auth)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestInitiateSSOWithSSOServerURL(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
pool := redistest.SetupRedis(t, t.Name(), false, false, false)
|
||||
|
||||
svc, ctx := newTestServiceWithConfig(t, ds, config.TestConfig(), nil, nil, &TestServerOpts{
|
||||
Pool: pool,
|
||||
})
|
||||
|
||||
// Mock app config with SSO server URL
|
||||
appConfig := &fleet.AppConfig{
|
||||
ServerSettings: fleet.ServerSettings{
|
||||
ServerURL: "https://fleet.example.com",
|
||||
},
|
||||
SSOSettings: &fleet.SSOSettings{
|
||||
EnableSSO: true,
|
||||
SSOServerURL: "https://admin.fleet.example.com",
|
||||
SSOProviderSettings: fleet.SSOProviderSettings{
|
||||
EntityID: "fleet",
|
||||
IDPName: "TestIDP",
|
||||
Metadata: `<?xml version="1.0"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="test-idp">
|
||||
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.example.com/sso"/>
|
||||
</md:IDPSSODescriptor>
|
||||
</md:EntityDescriptor>`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return appConfig, nil
|
||||
}
|
||||
|
||||
// Test that ACS URL uses SSO URL
|
||||
sessionID, _, idpURL, err := svc.InitiateSSO(ctx, "/dashboard")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, sessionID)
|
||||
require.NotEmpty(t, idpURL)
|
||||
|
||||
// The ACS URL should use the SSO server URL
|
||||
// We can't directly test the ACS URL in the SAML request here since it's embedded in the XML,
|
||||
// but the integration test verifies this works correctly
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ github.com/fleetdm/fleet/v4/server/fleet/SSOSettings EnableSSO bool
|
|||
github.com/fleetdm/fleet/v4/server/fleet/SSOSettings EnableSSOIdPLogin bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/SSOSettings EnableJITProvisioning bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/SSOSettings EnableJITRoleSync bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/SSOSettings SSOServerURL string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig FleetDesktop fleet.FleetDesktopSettings
|
||||
github.com/fleetdm/fleet/v4/server/fleet/FleetDesktopSettings TransparencyURL string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig VulnerabilitySettings fleet.VulnerabilitySettings
|
||||
|
|
|
|||
Loading…
Reference in a new issue