Allow Microsoft conditional access on premium self-hosted (#49414)

Resolves #47699.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Microsoft Entra Conditional Access is now supported for self-hosted
Fleet Premium instances.
* Conditional Access is available only on the Fleet Premium license
tier.
* **Changes**
* Removed the Microsoft Compliance Partner API key configuration and
updated the proxy behavior accordingly.
* Removed the managed-cloud indicator from license/config responses and
adjusted related UI rendering and gating.
* **Tests / Maintenance**
* Updated fixtures and automated tests to reflect the new licensing
gates and API/proxy behavior (including updated failure codes).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Lucas Manuel Rodriguez
2026-07-17 10:59:32 -03:00
committed by GitHub
parent f271e2b9d1
commit da3f30df79
24 changed files with 78 additions and 99 deletions
@@ -0,0 +1 @@
- Enabled Microsoft Entra conditional access for self-hosted Fleet Premium instances (previously available only on Fleet Cloud). The `microsoft_compliance_partner.proxy_api_key` server configuration has been removed; the feature is now gated on the Fleet Premium license tier.
+15 -17
View File
@@ -428,23 +428,21 @@ func runServeCmd(cmd *cobra.Command, configManager configpkg.Manager, debug, dev
}
})
var conditionalAccessMicrosoftProxy *conditional_access_microsoft_proxy.Proxy
if config.MicrosoftCompliancePartner.IsSet() {
var err error
conditionalAccessMicrosoftProxy, err = conditional_access_microsoft_proxy.New(
config.MicrosoftCompliancePartner.ProxyURI,
config.MicrosoftCompliancePartner.ProxyAPIKey,
func() (string, error) {
appCfg, err := ds.AppConfig(ctx)
if err != nil {
return "", fmt.Errorf("failed to load appconfig: %w", err)
}
return appCfg.ServerSettings.ServerURL, nil
},
)
if err != nil {
initFatal(err, "new microsoft compliance proxy")
}
// The Microsoft Compliance Partner proxy is available to all Fleet Premium
// instances (including self-hosted). The feature itself is gated on the
// license tier at the service layer.
conditionalAccessMicrosoftProxy, err := conditional_access_microsoft_proxy.New(
config.MicrosoftCompliancePartner.ProxyURI,
func() (string, error) {
appCfg, err := ds.AppConfig(ctx)
if err != nil {
return "", fmt.Errorf("failed to load appconfig: %w", err)
}
return appCfg.ServerSettings.ServerURL, nil
},
)
if err != nil {
initFatal(err, "new microsoft compliance proxy")
}
eh := errorstore.NewHandler(ctx, redisPool, logger, config.Logging.ErrorRetentionPeriod)
@@ -232,8 +232,7 @@
},
"license": {
"tier": "free",
"expiration": "0001-01-01T00:00:00Z",
"managed_cloud": false
"expiration": "0001-01-01T00:00:00Z"
},
"logging": {
"debug": true,
@@ -120,7 +120,6 @@ spec:
license:
expiration: "0001-01-01T00:00:00Z"
tier: free
managed_cloud: false
logging:
debug: true
json: false
@@ -88,6 +88,12 @@ func RunServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*http
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
// On Premium, AppConfig assembly reads the Microsoft conditional access
// integration. Default to an empty (not set up) integration so tests that
// don't care about it don't panic on a nil mock.
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
}
ds.NewGlobalPolicyFunc = func(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
return &fleet.Policy{
PolicyData: fleet.PolicyData{
-1
View File
@@ -72,7 +72,6 @@ export const DEFAULT_LICENSE_MOCK: ILicense = {
device_count: 4,
note: "",
organization: "",
managed_cloud: true,
allow_disable_telemetry: false,
};
-1
View File
@@ -4,7 +4,6 @@ const DEFAULT_LICENSE_MOCK = {
expiration: "2050-01-01T00:00:00Z",
note: "test license",
organization: "test org",
managed_cloud: false,
allow_disable_telemetry: false,
};
-2
View File
@@ -15,8 +15,6 @@ export interface ILicense {
expiration: string;
note: string;
organization: string;
// Whether the Fleet instance is managed by FleetDM
managed_cloud: boolean;
allow_disable_telemetry: boolean;
}
@@ -39,7 +39,7 @@ describe("Integrations Page", () => {
describe("Conditional access", () => {
it("Does not render the conditional access sidenav for self-hosted Fleet instances", () => {
const mockConfig = createMockConfig({
license: { ...DEFAULT_LICENSE_MOCK, managed_cloud: false },
license: { ...DEFAULT_LICENSE_MOCK },
});
const render = createCustomRenderer({
@@ -249,9 +249,6 @@ const ConditionalAccess = () => {
const oktaConfigured = isOktaConditionalAccessConfigured(config);
// Check if this is a managed cloud deployment (Microsoft Entra requires proxy infrastructure)
const isManagedCloud = config?.license?.managed_cloud || false;
// Check Entra configuration state
// Note: entraPhase is intentionally included in the dependency array to allow
// manual phase overrides (e.g., AwaitingOAuth) to persist until config changes
@@ -490,7 +487,7 @@ const ConditionalAccess = () => {
return (
<div className={`${baseClass}__cards`}>
{renderOktaContent()}
{isManagedCloud && renderEntraContent()}
{renderEntraContent()}
</div>
);
};
@@ -57,7 +57,7 @@ const AutomationsModal = ({
onExit,
}: IAutomationsModalProps): JSX.Element | null => {
const queryClient = useQueryClient();
const { setConfig } = useContext(AppContext);
const { setConfig, isPremiumTier } = useContext(AppContext);
const otherFormRef = useRef<
IAutomationFormHandle<IOtherWorkflowsModalSubmit>
@@ -104,8 +104,7 @@ const AutomationsModal = ({
? globalConfig?.integrations.conditional_access_enabled
: teamConfig?.integrations.conditional_access_enabled) ?? false;
const isManagedCloud = globalConfig?.license?.managed_cloud || false;
const conditionalAccessProviderText = isManagedCloud
const conditionalAccessProviderText = isPremiumTier
? "Okta or Microsoft Entra"
: "Okta";
+1 -12
View File
@@ -863,20 +863,11 @@ func (c ConditionalAccessConfig) Validate(initFatal func(err error, msg string))
}
// MicrosoftCompliancePartnerConfig holds the server configuration for the "Conditional access" feature.
// Currently only set on Cloud environments.
type MicrosoftCompliancePartnerConfig struct {
// ProxyAPIKey is a shared key required to use the Microsoft Compliance Partner proxy API (fleetdm.com).
ProxyAPIKey string `yaml:"proxy_api_key"`
// ProxyURI is the URI of the Microsoft Compliance Partner proxy (for development/testing).
ProxyURI string `yaml:"proxy_uri"`
}
// IsSet returns if the compliance partner configuration is set.
// Currently only set on Cloud environments.
func (m MicrosoftCompliancePartnerConfig) IsSet() bool {
return m.ProxyAPIKey != ""
}
type MDMConfig struct {
AppleAPNsCert string `yaml:"apple_apns_cert"`
AppleAPNsCertBytes string `yaml:"apple_apns_cert_bytes"`
@@ -1830,7 +1821,6 @@ func (man Manager) addConfigs() {
man.addConfigBool("partnerships.enable_secureframe", false, "Point transparency URL at Secureframe landing page")
// Microsoft Compliance Partner
man.addConfigString("microsoft_compliance_partner.proxy_api_key", "", "Shared key required to use the Microsoft Compliance Partner proxy API")
man.addConfigString("microsoft_compliance_partner.proxy_uri", "https://fleetdm.com", "URI of the Microsoft Compliance Partner proxy (for development/testing)")
man.addConfigBool("partnerships.enable_primo", false, "Disables the ability to manage multiple fleets in an instance, even in premium tier")
@@ -2173,8 +2163,7 @@ func (man Manager) LoadConfig() FleetConfig {
EnablePrimo: man.getConfigBool("partnerships.enable_primo"),
},
MicrosoftCompliancePartner: MicrosoftCompliancePartnerConfig{
ProxyAPIKey: man.getConfigString("microsoft_compliance_partner.proxy_api_key"),
ProxyURI: man.getConfigString("microsoft_compliance_partner.proxy_uri"),
ProxyURI: man.getConfigString("microsoft_compliance_partner.proxy_uri"),
},
ConditionalAccess: ConditionalAccessConfig{
CertSerialFormat: man.getConfigString("conditional_access.cert_serial_format"),
+6 -4
View File
@@ -186,7 +186,7 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
stats.ConditionalAccessBypassDisabled = !appConfig.ConditionalAccess.BypassEnabled()
}
stats.EntraConditionalAccessConfigured, err = ds.entraConditionalAccessConfigured(ctx, config)
stats.EntraConditionalAccessConfigured, err = ds.entraConditionalAccessConfigured(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "entra conditional access configured")
}
@@ -325,9 +325,11 @@ func fleetMaintainedAppsInUseDB(ctx context.Context, db sqlx.QueryerContext) (ma
return macOSApps, windowsApps, nil
}
func (ds *Datastore) entraConditionalAccessConfigured(ctx context.Context, fleetConfig config.FleetConfig) (bool, error) {
// Check if the needed server configuration for Conditional Access is set.
if !fleetConfig.MicrosoftCompliancePartner.IsSet() {
func (ds *Datastore) entraConditionalAccessConfigured(ctx context.Context) (bool, error) {
// Conditional access is a Fleet Premium feature. Gate on the current license
// tier so that an integration left over from a previous Premium license
// (e.g. after a downgrade or expiry) isn't reported as configured.
if !license.IsPremium(ctx) {
return false, nil
}
+4 -6
View File
@@ -574,9 +574,6 @@ func testConditionalAccessStatistics(t *testing.T, ds *Datastore) {
markStatisticsStale(t, ctx, ds)
// Test Entra conditional access: create the integration but without setup done
fleetConfig.MicrosoftCompliancePartner = config.MicrosoftCompliancePartnerConfig{
ProxyAPIKey: "test-key",
}
err = ds.ConditionalAccessMicrosoftCreateIntegration(ctx, "test-tenant", "test-secret")
require.NoError(t, err)
@@ -598,9 +595,10 @@ func testConditionalAccessStatistics(t *testing.T, ds *Datastore) {
markStatisticsStale(t, ctx, ds)
// Without the fleet config proxy key, should be false even with setup done
fleetConfig.MicrosoftCompliancePartner = config.MicrosoftCompliancePartnerConfig{}
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, premiumLicense), time.Millisecond, fleetConfig)
// On Fleet Free (e.g. after a license downgrade/expiry) the leftover
// integration row must not be reported as configured.
freeLicense := &fleet.LicenseInfo{Tier: fleet.TierFree}
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, freeLicense), time.Millisecond, fleetConfig)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.False(t, stats.EntraConditionalAccessConfigured)
-3
View File
@@ -1932,9 +1932,6 @@ type LicenseInfo struct {
Note string `json:"note,omitempty"`
// AllowDisableTelemetry allows specific customers to not send analytics
AllowDisableTelemetry bool `json:"allow_disable_telemetry,omitempty"`
// ManagedCloud indicates whether this Fleet instance is a cloud instance.
// Currently only used to display UI features only present on cloud instances.
ManagedCloud bool `json:"managed_cloud"`
}
func (l *LicenseInfo) IsPremium() bool {
@@ -6,6 +6,7 @@ import (
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
)
@@ -40,8 +41,8 @@ func (svc *Service) ConditionalAccessMicrosoftCreateIntegration(ctx context.Cont
return "", ctxerr.Wrap(ctx, err, "failed to authorize")
}
if !svc.config.MicrosoftCompliancePartner.IsSet() {
return "", &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
if lic, _ := license.FromContext(ctx); lic == nil || !lic.IsPremium() {
return "", fleet.ErrMissingLicense
}
// Load current integration, if any.
@@ -116,8 +117,8 @@ func (svc *Service) ConditionalAccessMicrosoftConfirm(ctx context.Context) (conf
return false, "", ctxerr.Wrap(ctx, err, "failed to authorize")
}
if !svc.config.MicrosoftCompliancePartner.IsSet() {
return false, "", &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
if lic, _ := license.FromContext(ctx); lic == nil || !lic.IsPremium() {
return false, "", fleet.ErrMissingLicense
}
// Load current integration.
@@ -182,8 +183,8 @@ func (svc *Service) ConditionalAccessMicrosoftDelete(ctx context.Context) error
return ctxerr.Wrap(ctx, err, "failed to authorize")
}
if !svc.config.MicrosoftCompliancePartner.IsSet() {
return &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
if lic, _ := license.FromContext(ctx); lic == nil || !lic.IsPremium() {
return fleet.ErrMissingLicense
}
// Load current integration.
@@ -231,7 +232,7 @@ func (svc *Service) ConditionalAccessMicrosoftGet(ctx context.Context) (*fleet.C
return nil, ctxerr.Wrap(ctx, err, "failed to authorize")
}
if !svc.config.MicrosoftCompliancePartner.IsSet() {
if lic, _ := license.FromContext(ctx); lic == nil || !lic.IsPremium() {
return nil, nil
}
@@ -18,20 +18,18 @@ import (
// Proxy holds functionality to send requests to Entra via Fleet's MS proxy.
type Proxy struct {
uri string
apiKey string
originGetter func() (string, error)
c *http.Client
}
// New creates a Proxy that will use the given URI and API key.
func New(uri string, apiKey string, originGetter func() (string, error)) (*Proxy, error) {
// New creates a Proxy that will use the given URI.
func New(uri string, originGetter func() (string, error)) (*Proxy, error) {
if _, err := url.Parse(uri); err != nil {
return nil, fmt.Errorf("parse uri: %w", err)
}
return &Proxy{
uri: uri,
apiKey: apiKey,
uri: uri,
originGetter: originGetter,
@@ -339,7 +337,6 @@ func (p *Proxy) setHeaders(r *http.Request) error {
if origin == "" {
return fmt.Errorf("missing origin: %w", err)
}
r.Header.Add("MS-API-Key", p.apiKey)
r.Header.Add("Origin", origin)
return nil
}
@@ -19,7 +19,7 @@ func TestProxyStatusErrorCapturesBody(t *testing.T) {
}))
defer srv.Close()
p, err := New(srv.URL, "key", func() (string, error) { return "https://fleet.example.com", nil })
p, err := New(srv.URL, func() (string, error) { return "https://fleet.example.com", nil })
require.NoError(t, err)
_, err = p.SetComplianceStatus(t.Context(), "tenant", "secret", "device", "upn", true, "name", "macOS", "14.0", false, time.Now())
@@ -48,7 +48,7 @@ func TestProxyStatusErrorCapturesBody(t *testing.T) {
}))
defer srv.Close()
p, err := New(srv.URL, "key", func() (string, error) { return "https://fleet.example.com", nil })
p, err := New(srv.URL, func() (string, error) { return "https://fleet.example.com", nil })
require.NoError(t, err)
_, err = p.SetComplianceStatus(t.Context(), "tenant", "secret", "device", "upn", true, "name", "macOS", "14.0", false, time.Now())
+7 -12
View File
@@ -15883,24 +15883,19 @@ func (s *integrationTestSuite) TestHostReenrollWithSameHostRowRefetchOsquery() {
}
}
func (s *integrationTestSuite) TestConditionalAccessOnlyCloud() {
t := s.T()
var resp appConfigResponse
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &resp)
require.False(t, resp.License.ManagedCloud)
// Microsoft compliance partner APIs should fail if the setting is not set (only set on Cloud).
func (s *integrationTestSuite) TestConditionalAccessRequiresPremium() {
// Microsoft compliance partner APIs should fail on Fleet Free (this suite
// runs without a premium license).
var r conditionalAccessMicrosoftCreateResponse
s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateRequest{
MicrosoftTenantID: "foobar",
}, http.StatusBadRequest, &r)
}, http.StatusPaymentRequired, &r)
var c conditionalAccessMicrosoftConfirmResponse
s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{},
http.StatusBadRequest, &c)
http.StatusPaymentRequired, &c)
var d conditionalAccessMicrosoftDeleteResponse
s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{},
http.StatusBadRequest, &d)
s.DoJSON("DELETE", "/api/latest/fleet/conditional-access/microsoft", nil,
http.StatusPaymentRequired, &d)
}
func (s *integrationTestSuite) TestUpdateHostCertificateTemplate() {
@@ -23545,10 +23545,7 @@ func (s *integrationEnterpriseTestSuite) TestConditionalAccessBasicSetup() {
s.clearOktaConditionalAccess()
})
// Test license.managed_cloud is set on Cloud environments.
var acResp appConfigResponse
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
require.True(t, acResp.License.ManagedCloud)
// Test global maintainer fails to create the integration.
u := &fleet.User{
+4 -2
View File
@@ -2622,8 +2622,10 @@ func (svc *Service) processScriptsForNewlyFailingPolicies(
}
func (svc *Service) conditionalAccessConfiguredAndEnabledForTeam(ctx context.Context, hostTeamID *uint) (configured bool, enabledForTeam bool, err error) {
// Check if the needed server configuration for Conditional Access is set.
if !svc.config.MicrosoftCompliancePartner.IsSet() {
// Conditional access is a Fleet Premium feature. Gate on the current license
// tier so that an integration left over from a previous Premium license
// (e.g. after a downgrade or expiry) doesn't keep the feature active.
if !license.IsPremium(ctx) {
return false, false, nil
}
+1 -8
View File
@@ -86,16 +86,9 @@ func (svc *Service) License(ctx context.Context) (*fleet.LicenseInfo, error) {
}
licChecker, _ := license.FromContext(ctx)
// Type assert to get the concrete type for modification and return
// Type assert to get the concrete type to return.
lic, _ := licChecker.(*fleet.LicenseInfo)
// Currently we use the presence of Microsoft Compliance Partner settings
// (only configured in cloud instances) to determine if a Fleet instance
// is a cloud managed instance.
if lic != nil && svc.config.MicrosoftCompliancePartner.IsSet() {
lic.ManagedCloud = true
}
return lic, nil
}
+3 -1
View File
@@ -170,7 +170,9 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
}
if len(opts) > 0 && opts[0].ConditionalAccessMicrosoftProxy != nil {
conditionalAccessMicrosoftProxy = opts[0].ConditionalAccessMicrosoftProxy
fleetConfig.MicrosoftCompliancePartner.ProxyAPIKey = "insecure" // setting this so the feature is "enabled".
// The Conditional Access feature is gated on Fleet Premium; callers that
// exercise it must provide a premium license via opts[0].License.
require.True(t, lic.IsPremium(), "ConditionalAccessMicrosoftProxy requires a premium license via opts.License")
}
if len(opts) > 0 && opts[0].AndroidModule != nil {
+12 -1
View File
@@ -98,6 +98,15 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
if mockDS.ValidateReferencedCustomHostVitalsFunc == nil {
mockDS.ValidateReferencedCustomHostVitalsFunc = func(ctx context.Context, documents []string) error { return nil }
}
// On Premium, AppConfig assembly and the osquery detail-query flow read the
// Microsoft conditional access integration. Default to an empty (not set up)
// integration so premium tests that don't care about it don't panic on a nil
// mock. Tests that assert on it can override.
if mockDS.ConditionalAccessMicrosoftGetFunc == nil {
mockDS.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
}
}
}
lic := &fleet.LicenseInfo{Tier: fleet.TierFree}
@@ -224,7 +233,9 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
}
if len(opts) > 0 && opts[0].ConditionalAccessMicrosoftProxy != nil {
conditionalAccessMicrosoftProxy = opts[0].ConditionalAccessMicrosoftProxy
fleetConfig.MicrosoftCompliancePartner.ProxyAPIKey = "insecure" // setting this so the feature is "enabled".
// The Conditional Access feature is gated on Fleet Premium; callers that
// exercise it must provide a premium license via opts[0].License.
require.True(t, lic.IsPremium(), "ConditionalAccessMicrosoftProxy requires a premium license via opts.License")
}
if len(opts) > 0 && opts[0].AndroidModule != nil {