Add easy to understand errors when setting up Entra conditional access (#33453)

Resolves #32420.

Demo of the changes:

https://github.com/user-attachments/assets/c5ee28ba-7f67-48bb-aa25-c934a5515de4

- [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] QA'd all new/changed functionality manually
This commit is contained in:
Lucas Manuel Rodriguez
2025-09-25 22:52:28 -03:00
committed by GitHub
parent 662b4f317d
commit ee4fae8d69
7 changed files with 66 additions and 23 deletions
@@ -0,0 +1 @@
* Added easy to understand error messages when configuring Entra conditional access in fleet.
@@ -174,7 +174,7 @@ const ConditionalAccess = () => {
...DEFAULT_USE_QUERY_OPTIONS,
// only make this call at the appropriate UI phase
enabled: phase === Phase.ConfirmingConfigured && isPremiumTier,
onSuccess: ({ configuration_completed }) => {
onSuccess: ({ configuration_completed, setup_error }) => {
if (configuration_completed) {
setPhase(Phase.Configured);
renderFlash(
@@ -183,10 +183,42 @@ const ConditionalAccess = () => {
);
} else {
setPhase(Phase.Form);
renderFlash(
"error",
"Could not verify conditional access integration. Please try connecting again."
);
if (
// IT admin did not complete the consent.
!setup_error ||
// IT admin clicked "Cancel" in the consent dialog.
setup_error.includes(
"A Microsoft Entra admin did not consent to the permissions requested by the conditional access integration"
)
) {
renderFlash(
"error",
"Couldn't update. Fleet didn't get permissions for Entra. Please try again and accept the permissions."
);
} else if (
setup_error.includes(
'No "Fleet conditional access" Entra ID group was found'
)
) {
renderFlash(
"error",
`Couldn't connect. The "Fleet conditional access" group doesn't exist in Entra. Please create the group and try again.`
);
} else {
// For other kind of errors we just show a generic error.
// We won't render the error as is because the error comes from the MS proxy and they may be too big or unformatted
// to display in the banner.
//
// For troubleshooting:
// - The API response contains the setup_error.
// - The Fleet server logs the error.
// - The MS proxy stores the error in its database.
renderFlash(
"error",
"Couldn't connect. Please contact your Fleet administrator."
);
}
}
},
onError: () => {
@@ -7,6 +7,7 @@ export type TriggerMSConditionalStatusResponse = {
};
export type ConfirmMSConditionalAccessResponse = {
configuration_completed: boolean;
setup_error: string;
};
const conditionalAccessService = {
+1 -1
View File
@@ -1325,7 +1325,7 @@ type Service interface {
// ConditionalAccessMicrosoftGet returns the current (currently unique) integration.
ConditionalAccessMicrosoftGet(ctx context.Context) (*ConditionalAccessMicrosoftIntegration, error)
// ConditionalAccessMicrosoftConfirm finalizes the integration (marks integration as done).
ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, err error)
ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, setupError string, err error)
// ConditionalAccessMicrosoftDelete deletes the integration and deprovisions the tenant on Entra.
ConditionalAccessMicrosoftDelete(ctx context.Context) error
+2 -2
View File
@@ -811,7 +811,7 @@ type ConditionalAccessMicrosoftCreateIntegrationFunc func(ctx context.Context, t
type ConditionalAccessMicrosoftGetFunc func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error)
type ConditionalAccessMicrosoftConfirmFunc func(ctx context.Context) (configurationCompleted bool, err error)
type ConditionalAccessMicrosoftConfirmFunc func(ctx context.Context) (configurationCompleted bool, setupError string, err error)
type ConditionalAccessMicrosoftDeleteFunc func(ctx context.Context) error
@@ -4835,7 +4835,7 @@ func (s *Service) ConditionalAccessMicrosoftGet(ctx context.Context) (*fleet.Con
return s.ConditionalAccessMicrosoftGetFunc(ctx)
}
func (s *Service) ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, err error) {
func (s *Service) ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, setupError string, err error) {
s.mu.Lock()
s.ConditionalAccessMicrosoftConfirmFuncInvoked = true
s.mu.Unlock()
+20 -13
View File
@@ -91,55 +91,62 @@ func (svc *Service) ConditionalAccessMicrosoftCreateIntegration(ctx context.Cont
type conditionalAccessMicrosoftConfirmRequest struct{}
type conditionalAccessMicrosoftConfirmResponse struct {
ConfigurationCompleted bool `json:"configuration_completed"`
Err error `json:"error,omitempty"`
ConfigurationCompleted bool `json:"configuration_completed"`
SetupError string `json:"setup_error"`
Err error `json:"error,omitempty"`
}
func (r conditionalAccessMicrosoftConfirmResponse) Error() error { return r.Err }
func conditionalAccessMicrosoftConfirmEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
_ = request.(*conditionalAccessMicrosoftConfirmRequest)
configurationCompleted, err := svc.ConditionalAccessMicrosoftConfirm(ctx)
configurationCompleted, setupError, err := svc.ConditionalAccessMicrosoftConfirm(ctx)
if err != nil {
return conditionalAccessMicrosoftConfirmResponse{Err: err}, nil
}
return conditionalAccessMicrosoftConfirmResponse{
ConfigurationCompleted: configurationCompleted,
SetupError: setupError,
}, nil
}
func (svc *Service) ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, err error) {
func (svc *Service) ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, setupError string, err error) {
// Check user is authorized to write integrations.
if err := svc.authz.Authorize(ctx, &fleet.ConditionalAccessMicrosoftIntegration{}, fleet.ActionWrite); err != nil {
return false, ctxerr.Wrap(ctx, err, "failed to authorize")
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"}
return false, "", &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
}
// Load current integration.
integration, err := svc.ds.ConditionalAccessMicrosoftGet(ctx)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "failed to load the integration")
return false, "", ctxerr.Wrap(ctx, err, "failed to load the integration")
}
if integration.SetupDone {
return true, nil
return true, "", nil
}
getResponse, err := svc.conditionalAccessMicrosoftProxy.Get(ctx, integration.TenantID, integration.ProxyServerSecret)
if err != nil {
level.Error(svc.logger).Log("msg", "failed to get integration settings from proxy", "err", err)
return false, nil
return false, "", nil
}
if !getResponse.SetupDone {
return false, nil
var setupError string
if getResponse.SetupError != nil {
level.Error(svc.logger).Log("msg", "setup is not done", "setup_error", getResponse.SetupError)
setupError = *getResponse.SetupError
}
return false, setupError, nil
}
if err := svc.ds.ConditionalAccessMicrosoftMarkSetupDone(ctx); err != nil {
return false, ctxerr.Wrap(ctx, err, "failed to mark setup_done=true")
return false, "", ctxerr.Wrap(ctx, err, "failed to mark setup_done=true")
}
if err := svc.NewActivity(
@@ -147,10 +154,10 @@ func (svc *Service) ConditionalAccessMicrosoftConfirm(ctx context.Context) (conf
authz.UserFromContext(ctx),
fleet.ActivityTypeAddedConditionalAccessIntegrationMicrosoft{},
); err != nil {
return false, ctxerr.Wrap(ctx, err, "create activity for conditional access integration microsoft")
return false, "", ctxerr.Wrap(ctx, err, "create activity for conditional access integration microsoft")
}
return true, nil
return true, "", nil
}
type conditionalAccessMicrosoftDeleteRequest struct{}
@@ -42,6 +42,7 @@ module.exports = {
// If an error or error_description are provided, then the admin did not consent, and we will return a 200 response.
if(error || error_description) {// eslint-disable-line camelcase
// If an admin did not consent (or a user who started connecting the integration does not have admin permissions), try to match the provided state to a MicrosoftComplianceTenant record, and redirect to that.
// IMPORTANT: Don't change the below setupError value. The error string is checked by the Fleet UI frontend.
let newSetupError = 'A Microsoft Entra admin did not consent to the permissions requested by the conditional access integration.';
let tenantRecordWithThisState = await MicrosoftComplianceTenant.findOne({stateTokenForAdminConsent: state});
// If we couldn't find a MicrosoftComplianceTenant record with this state, return a notFound response.
@@ -202,6 +203,7 @@ module.exports = {
// If the response from the Microsoft Graph API did not contain any groups, log a warning and save a setup error on the database record for this tenant.
if(parsedGroupResponse.value.length === 0){
sails.log.warn(`When an Entra tenant (${informationAboutThisTenant.fleetInstanceUrl}) tried setting up a conditional access integration, no "Fleet conditional access" Entra ID group was found on this Entra tenant.`);
// IMPORTANT: Don't change the below setupError value. The error string is checked by the Fleet UI frontend.
await MicrosoftComplianceTenant.updateOne({id: informationAboutThisTenant.id}).set({setupError: `No "Fleet conditional access" Entra ID group was found on this Entra tenant.`});
throw {redirect: fleetInstanceUrlToRedirectTo };
}
@@ -209,7 +211,7 @@ module.exports = {
let groupId = parsedGroupResponse.value[0].id;
// Send a request to assign the new compliance policy to the "All users" group.
// Send a request to assign the new compliance policy to the "Fleet conditional access" group.
let assignPolicyResponse = await sails.helpers.http.sendHttpRequest.with({
method: 'POST',
url: `${tenantDataSyncUrl}/PartnerCompliancePolicies(guid'${encodeURIComponent(createdPolicyId)}')/Assign?api-version=1.6`,
@@ -224,7 +226,7 @@ module.exports = {
}
}).intercept(async (err)=>{
await MicrosoftComplianceTenant.updateOne({id: informationAboutThisTenant.id}).set({setupError: `${require('util').inspect(err, {depth: null})}`});
sails.log.warn(`An error occurred when sending a assign a new compliance policy to "All users" on a Microsoft compliance tenant. Full error: ${require('util').inspect(err, {depth: 3})}`);
sails.log.warn(`An error occurred when sending a assign a new compliance policy to "Fleet conditional access" group on a Microsoft compliance tenant. Full error: ${require('util').inspect(err, {depth: 3})}`);
return {redirect: fleetInstanceUrlToRedirectTo };
});
// Example response: