diff --git a/changes/21890-vpp-token-error b/changes/21890-vpp-token-error new file mode 100644 index 0000000000..da03734eca --- /dev/null +++ b/changes/21890-vpp-token-error @@ -0,0 +1 @@ +- Improve messaging for VPP token constraint errors diff --git a/server/datastore/mysql/vpp.go b/server/datastore/mysql/vpp.go index a40416697a..b9e43ddf15 100644 --- a/server/datastore/mysql/vpp.go +++ b/server/datastore/mysql/vpp.go @@ -16,6 +16,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" + "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) @@ -780,6 +781,7 @@ TEAMLOOP: } func (ds *Datastore) UpdateVPPTokenTeams(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) { + stmtTeamName := `SELECT name FROM teams WHERE id = ?` stmtRemove := `DELETE FROM vpp_token_teams WHERE vpp_token_id = ?` stmtInsert := ` INSERT INTO @@ -858,6 +860,17 @@ func (ds *Datastore) UpdateVPPTokenTeams(ctx context.Context, id uint, teams []u }) if err != nil { + var mysqlErr *mysql.MySQLError + // https://dev.mysql.com/doc/mysql-errors/8.4/en/server-error-reference.html#error_er_dup_entry + if errors.As(err, &mysqlErr) && IsDuplicate(err) { + var dupeTeamID uint + var dupeTeamName string + fmt.Sscanf(mysqlErr.Message, "Duplicate entry '%d' for", &dupeTeamID) + if err := sqlx.GetContext(ctx, ds.reader(ctx), &dupeTeamName, stmtTeamName, dupeTeamID); err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting team name for vpp token conflict error") + } + return nil, ctxerr.Wrap(ctx, fleet.ErrVPPTokenTeamConstraint{Name: dupeTeamName, ID: &dupeTeamID}) + } return nil, ctxerr.Wrap(ctx, err, "modifying vpp token team associations") } @@ -1127,7 +1140,7 @@ func checkVPPNullTeam(ctx context.Context, tx sqlx.ExtContext, currentID *uint, } if allTeamsFound && currentID != nil && *currentID != id { - return ctxerr.Wrap(ctx, errors.New("All teams token already exists")) + return ctxerr.Wrap(ctx, fleet.ErrVPPTokenTeamConstraint{Name: fleet.ReservedNameAllTeams}) } if nullTeam != fleet.NullTeamNone { @@ -1139,7 +1152,7 @@ func checkVPPNullTeam(ctx context.Context, tx sqlx.ExtContext, currentID *uint, return ctxerr.Wrap(ctx, err, "scanning row in check vpp token null team") } if currentID == nil || *currentID != id { - return ctxerr.Errorf(ctx, "vpp token for team %s already exists", nullTeam) + return ctxerr.Wrap(ctx, fleet.ErrVPPTokenTeamConstraint{Name: nullTeam.PrettyName()}) } } diff --git a/server/datastore/mysql/vpp_test.go b/server/datastore/mysql/vpp_test.go index b9bfb8dfe3..9a6c6796ce 100644 --- a/server/datastore/mysql/vpp_test.go +++ b/server/datastore/mysql/vpp_test.go @@ -981,8 +981,10 @@ func testVPPTokensCRUD(t *testing.T, ds *Datastore) { assert.Error(t, err) _, err = ds.UpdateVPPTokenTeams(ctx, tokBadConstraint.ID, []uint{team.ID}) assert.Error(t, err) + assert.ErrorContains(t, err, "\"Kritters\" team already has a VPP token.") _, err = ds.UpdateVPPTokenTeams(ctx, tokBadConstraint.ID, []uint{0}) assert.Error(t, err) + assert.ErrorContains(t, err, "\"No team\" team already has a VPP token.") toks, err = ds.ListVPPTokens(ctx) assert.NoError(t, err) diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 69df8e0323..0e5cf32971 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -801,6 +801,17 @@ const ( NullTeamNoTeam NullTeamType = "noteam" ) +func (n NullTeamType) PrettyName() string { + switch n { + case NullTeamAllTeams: + return ReservedNameAllTeams + case NullTeamNoTeam: + return ReservedNameNoTeam + default: + return string(n) + } +} + type AppleDevice int const ( diff --git a/server/fleet/vpp.go b/server/fleet/vpp.go index 8c5dafa15a..4e0803edfe 100644 --- a/server/fleet/vpp.go +++ b/server/fleet/vpp.go @@ -1,6 +1,7 @@ package fleet import ( + "fmt" "time" ) @@ -67,3 +68,12 @@ type VPPAppStatusSummary struct { // Failed is the number of hosts that have the VPP app installation failed. Failed uint `json:"failed" db:"failed"` } + +type ErrVPPTokenTeamConstraint struct { + Name string + ID *uint +} + +func (e ErrVPPTokenTeamConstraint) Error() string { + return fmt.Sprintf("Error: %q team already has a VPP token. Each team can only have one VPP token.", e.Name) +} diff --git a/server/service/appconfig.go b/server/service/appconfig.go index f384dec0d5..17b83dcbbf 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -557,6 +557,10 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle if appConfig.MDM.VolumePurchasingProgram.Set && appConfig.MDM.VolumePurchasingProgram.Valid { for tokenID, tokenTeams := range vppAssignments { if _, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, tokenTeams); err != nil { + var errTokConstraint fleet.ErrVPPTokenTeamConstraint + if errors.As(err, &errTokConstraint) { + return nil, ctxerr.Wrap(ctx, fleet.NewUserMessageError(errTokConstraint, http.StatusConflict)) + } return nil, ctxerr.Wrap(ctx, err, "saving ABM token assignments") } }