From cb559db5e144e68bdece16cf1cc57bf2b25f410e Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:27:45 -0500 Subject: [PATCH] Trying to bypass when not allowed returns an error (#40186) **Related issue:** Resolves #36105 --- .../mysql/conditional_access_bypass.go | 27 +++++++++- .../mysql/conditional_access_bypass_test.go | 51 +++++++++++++++++++ server/service/integration_enterprise_test.go | 33 ++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/server/datastore/mysql/conditional_access_bypass.go b/server/datastore/mysql/conditional_access_bypass.go index 0e488cf99f..ffeeb796c6 100644 --- a/server/datastore/mysql/conditional_access_bypass.go +++ b/server/datastore/mysql/conditional_access_bypass.go @@ -7,11 +7,24 @@ import ( "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" "github.com/jmoiron/sqlx" ) func (ds *Datastore) ConditionalAccessBypassDevice(ctx context.Context, hostID uint) error { - const stmt = ` + const checkStmt = ` + SELECT + COUNT(*) + FROM + policy_membership pm + INNER JOIN + policies p ON pm.policy_id = p.id + WHERE + pm.host_id = ? + AND p.conditional_access_bypass_enabled = 0 + AND pm.passes = 0 + ` + const insertStmt = ` INSERT INTO host_conditional_access (host_id, bypassed_at) VALUES @@ -19,7 +32,17 @@ func (ds *Datastore) ConditionalAccessBypassDevice(ctx context.Context, hostID u ON DUPLICATE KEY UPDATE bypassed_at = NOW(6)` - if _, err := ds.writer(ctx).ExecContext(ctx, stmt, hostID); err != nil { + var blockCount uint + + if err := sqlx.GetContext(ctx, ds.writer(ctx), &blockCount, checkStmt, hostID); err != nil { + return ctxerr.Wrap(ctx, err, "checking failing policy count") + } + + if blockCount != 0 { + return &fleet.BadRequestError{Message: "host has failing non-bypassable policies"} + } + + if _, err := ds.writer(ctx).ExecContext(ctx, insertStmt, hostID); err != nil { return ctxerr.Wrap(ctx, err, "inserting host conditional bypass") } diff --git a/server/datastore/mysql/conditional_access_bypass_test.go b/server/datastore/mysql/conditional_access_bypass_test.go index a084412d27..6bc5b7ec13 100644 --- a/server/datastore/mysql/conditional_access_bypass_test.go +++ b/server/datastore/mysql/conditional_access_bypass_test.go @@ -7,6 +7,8 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/test" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" ) @@ -18,6 +20,7 @@ func TestConditionalAccessBypass(t *testing.T) { fn func(t *testing.T, ds *Datastore) }{ {"ConditionalAccessBypassDevice", testConditionalAccessBypassDevice}, + {"ConditionalAccessBypassDeviceWithBlockingPolicy", testConditionalAccessBypassDeviceWithBlockingPolicy}, {"ConditionalAccessConsumeBypass", testConditionalAccessConsumeBypass}, {"ConditionalAccessClearBypasses", testConditionalAccessClearBypasses}, {"ConditionalAccessBypassDeletedWithHost", testConditionalAccessBypassDeletedWithHost}, @@ -268,3 +271,51 @@ func testConditionalAccessBypassedAt(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Nil(t, bypassedAtOther) } + +func testConditionalAccessBypassDeviceWithBlockingPolicy(t *testing.T, ds *Datastore) { + ctx := context.Background() + + user := test.NewUser(t, ds, "Alice", "alice@example.com", true) + + host, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("blocking-policy-host"), + UUID: "blocking-policy-uuid", + Hostname: "blocking.local", + PrimaryIP: "192.168.1.10", + PrimaryMac: "30-65-EC-6F-C4-70", + }) + require.NoError(t, err) + + // Create a global policy with conditional_access_bypass_enabled defaulting to 1 (bypassable) + policy, err := ds.NewGlobalPolicy(ctx, &user.ID, fleet.PolicyPayload{ + Name: "non-bypassable-policy", + Query: "select 1;", + }) + require.NoError(t, err) + + // Set conditional_access_bypass_enabled = 0 to make it non-bypassable + ExecAdhocSQL(t, ds, func(db sqlx.ExtContext) error { + _, err := db.ExecContext(ctx, `UPDATE policies SET conditional_access_bypass_enabled = 0 WHERE id = ?`, policy.ID) + return err + }) + + // Record a failing result for this policy on the host + err = ds.RecordPolicyQueryExecutions(ctx, host, map[uint]*bool{policy.ID: ptr.Bool(false)}, time.Now(), false) + require.NoError(t, err) + + // Bypass should fail because the host has a failing non-bypassable policy + err = ds.ConditionalAccessBypassDevice(ctx, host.ID) + require.Error(t, err) + var badReqErr *fleet.BadRequestError + require.ErrorAs(t, err, &badReqErr) + + // Verify no host_conditional_access row was created + var count int + innerErr := ds.writer(ctx).GetContext(ctx, &count, "SELECT COUNT(*) FROM host_conditional_access WHERE host_id = ?", host.ID) + require.NoError(t, innerErr) + require.Equal(t, 0, count) +} diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 57e282c557..1a28844297 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -24176,6 +24176,39 @@ FqU+KJOed6qlzj7qy+u5l6CQeajLGdjUxFlFyw== require.NoError(t, getDeviceHostResp.Err) require.False(t, getDeviceHostResp.Host.ConditionalAccessBypassed) }) + + t.Run("bypass fails when host has failing non-bypassable policy", func(t *testing.T) { + token := fmt.Sprintf("bypass-nonbypassable-%s", uuid.New().String()) + host := createHostAndDeviceToken(t, s.ds, token) + + // Create a global policy + adminUser := s.users["admin1@example.com"] + policy, err := s.ds.NewGlobalPolicy(ctx, &adminUser.ID, fleet.PolicyPayload{ + Name: fmt.Sprintf("non-bypassable-%s", uuid.New().String()), + Query: "select 1;", + }) + require.NoError(t, err) + + // Make the policy non-bypassable + mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error { + _, innerErr := db.ExecContext(ctx, `UPDATE policies SET conditional_access_bypass_enabled = 0 WHERE id = ?`, policy.ID) + return innerErr + }) + + // Record a failing result for this policy on the host + err = s.ds.RecordPolicyQueryExecutions(ctx, host, map[uint]*bool{policy.ID: ptr.Bool(false)}, time.Now(), false) + require.NoError(t, err) + + // Bypass should fail with 400 Bad Request + var bypassResp bypassConditionalAccessResponse + s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/device/%s/bypass_conditional_access", token), + nil, http.StatusBadRequest, &bypassResp) + + // Verify no bypass row was created + bypassedAt, err := s.ds.ConditionalAccessBypassedAt(ctx, host.ID) + require.NoError(t, err) + require.Nil(t, bypassedAt) + }) } // generateTestCertForDeviceAuth generates a test certificate for device authentication.