fleet/server/service/conditional_access_microsoft.go
Lucas Manuel Rodriguez 1c5700a8c4
Microsoft Compliance Partner backend changes (#29540)
For #27042.

Ready for review, just missing integration tests that I will be writing
today.

- [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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For new Fleet configuration settings
- [X] Verified that the setting can be managed via GitOps, or confirmed
that the setting is explicitly being excluded from GitOps. If managing
via Gitops:
- [X] Verified that the setting is exported via `fleetctl
generate-gitops`
- [X] Added the setting to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [X] 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
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [X] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [x] Added/updated automated tests
- [X] Manual QA for all new/changed functionality

---------

Co-authored-by: jacobshandling <61553566+jacobshandling@users.noreply.github.com>
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-06-11 14:22:46 -03:00

241 lines
8.9 KiB
Go

package service
import (
"context"
"errors"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/go-kit/log/level"
)
type conditionalAccessMicrosoftCreateRequest struct {
// MicrosoftTenantID holds the Entra tenant ID.
MicrosoftTenantID string `json:"microsoft_tenant_id"`
}
type conditionalAccessMicrosoftCreateResponse struct {
// MicrosoftAuthenticationURL holds the URL to redirect the admin to consent access
// to the tenant to Fleet's multi-tenant application.
MicrosoftAuthenticationURL string `json:"microsoft_authentication_url"`
Err error `json:"error,omitempty"`
}
func (r conditionalAccessMicrosoftCreateResponse) Error() error { return r.Err }
func conditionalAccessMicrosoftCreateEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*conditionalAccessMicrosoftCreateRequest)
adminConsentURL, err := svc.ConditionalAccessMicrosoftCreateIntegration(ctx, req.MicrosoftTenantID)
if err != nil {
return conditionalAccessMicrosoftCreateResponse{Err: err}, nil
}
return conditionalAccessMicrosoftCreateResponse{
MicrosoftAuthenticationURL: adminConsentURL,
}, nil
}
func (svc *Service) ConditionalAccessMicrosoftCreateIntegration(ctx context.Context, tenantID string) (adminConsentURL string, err error) {
// 0. Check user is authorized to create an integration.
if err := svc.authz.Authorize(ctx, &fleet.ConditionalAccessMicrosoftIntegration{}, fleet.ActionWrite); err != nil {
return "", ctxerr.Wrap(ctx, err, "failed to authorize")
}
if !svc.config.MicrosoftCompliancePartner.IsSet() {
return "", &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
}
// Load current integration, if any.
existingIntegration, err := svc.ConditionalAccessMicrosoftGet(ctx)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "failed to load the integration")
}
switch {
case existingIntegration != nil && existingIntegration.TenantID == tenantID:
// Nothing to do, integration with same tenant ID has already been created.
// Retrieve settings of the integration to get the admin consent URL.
getResponse, err := svc.conditionalAccessMicrosoftProxy.Get(ctx, existingIntegration.TenantID, existingIntegration.ProxyServerSecret)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "failed to get the integration settings")
}
return getResponse.AdminConsentURL, nil
case existingIntegration != nil && existingIntegration.SetupDone:
return "", &fleet.BadRequestError{Message: "integration already setup"}
}
//
// At this point we have two scenarios:
// - There's no integration yet, so we need to create a new one.
// - There's an integration already with a different TenantID and has not been setup.
//
// Create integration on the proxy.
proxyCreateResponse, err := svc.conditionalAccessMicrosoftProxy.Create(ctx, tenantID)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "failed to create integration in proxy")
}
// Create integration in datastore.
if err := svc.ds.ConditionalAccessMicrosoftCreateIntegration(ctx, proxyCreateResponse.TenantID, proxyCreateResponse.Secret); err != nil {
return "", ctxerr.Wrap(ctx, err, "failed to create integration in datastore")
}
// Retrieve settings of the integration to get the admin consent URL.
getResponse, err := svc.conditionalAccessMicrosoftProxy.Get(ctx, proxyCreateResponse.TenantID, proxyCreateResponse.Secret)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "failed to get the integration settings")
}
return getResponse.AdminConsentURL, nil
}
type conditionalAccessMicrosoftConfirmRequest struct{}
type conditionalAccessMicrosoftConfirmResponse struct {
ConfigurationCompleted bool `json:"configuration_completed"`
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)
if err != nil {
return conditionalAccessMicrosoftConfirmResponse{Err: err}, nil
}
return conditionalAccessMicrosoftConfirmResponse{
ConfigurationCompleted: configurationCompleted,
}, nil
}
func (svc *Service) ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, 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")
}
if !svc.config.MicrosoftCompliancePartner.IsSet() {
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")
}
if integration.SetupDone {
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
}
if !getResponse.SetupDone {
return false, nil
}
if err := svc.ds.ConditionalAccessMicrosoftMarkSetupDone(ctx); err != nil {
return false, ctxerr.Wrap(ctx, err, "failed to mark setup_done=true")
}
if err := svc.NewActivity(
ctx,
authz.UserFromContext(ctx),
fleet.ActivityTypeAddedConditionalAccessIntegrationMicrosoft{},
); err != nil {
return false, ctxerr.Wrap(ctx, err, "create activity for conditional access integration microsoft")
}
return true, nil
}
type conditionalAccessMicrosoftDeleteRequest struct{}
type conditionalAccessMicrosoftDeleteResponse struct {
Err error `json:"error,omitempty"`
}
func (r conditionalAccessMicrosoftDeleteResponse) Error() error { return r.Err }
func conditionalAccessMicrosoftDeleteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
_ = request.(*conditionalAccessMicrosoftDeleteRequest)
if err := svc.ConditionalAccessMicrosoftDelete(ctx); err != nil {
return conditionalAccessMicrosoftDeleteResponse{Err: err}, nil
}
return conditionalAccessMicrosoftDeleteResponse{}, nil
}
func (svc *Service) ConditionalAccessMicrosoftDelete(ctx context.Context) error {
// Check user is authorized to delete an integration.
if err := svc.authz.Authorize(ctx, &fleet.ConditionalAccessMicrosoftIntegration{}, fleet.ActionWrite); err != nil {
return ctxerr.Wrap(ctx, err, "failed to authorize")
}
if !svc.config.MicrosoftCompliancePartner.IsSet() {
return &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
}
// Load current integration.
integration, err := svc.ds.ConditionalAccessMicrosoftGet(ctx)
if err != nil {
if fleet.IsNotFound(err) {
return &fleet.BadRequestError{Message: "integration not found"}
}
return ctxerr.Wrap(ctx, err, "failed to load the integration")
}
// Delete integration on the proxy.
deleteResponse, err := svc.conditionalAccessMicrosoftProxy.Delete(ctx, integration.TenantID, integration.ProxyServerSecret)
if err != nil {
if fleet.IsNotFound(err) {
// In case there's an issue on the Proxy database we want to make sure to
// allow deleting the integration in Fleet, so we continue.
svc.logger.Log("msg", "delete returned not found, continuing...")
} else {
return ctxerr.Wrap(ctx, err, "failed to delete the integration on the proxy")
}
} else if deleteResponse.Error != "" {
return ctxerr.Wrap(ctx, errors.New(deleteResponse.Error), "delete on the proxy failed")
}
// Delete integration in datastore.
if err := svc.ds.ConditionalAccessMicrosoftDelete(ctx); err != nil {
return ctxerr.Wrap(ctx, err, "failed to delete integration in datastore")
}
if err := svc.NewActivity(
ctx,
authz.UserFromContext(ctx),
fleet.ActivityTypeDeletedConditionalAccessIntegrationMicrosoft{},
); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for deletion of conditional access integration microsoft")
}
return nil
}
func (svc *Service) ConditionalAccessMicrosoftGet(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
// Check user is authorized to read app config (which is where expose integration information)
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
return nil, ctxerr.Wrap(ctx, err, "failed to authorize")
}
if !svc.config.MicrosoftCompliancePartner.IsSet() {
return nil, nil
}
// Load current integration.
integration, err := svc.ds.ConditionalAccessMicrosoftGet(ctx)
if err != nil {
if fleet.IsNotFound(err) {
return nil, nil
}
return nil, ctxerr.Wrap(ctx, err, "failed to load the integration")
}
return integration, nil
}