Merge branch 'main' into 14415

This commit is contained in:
Jacob Shandling 2023-12-05 13:59:02 -08:00
commit 6c25646499
9 changed files with 218 additions and 57 deletions

View File

@ -0,0 +1,45 @@
# Catch missed authorization checks during software development
<div class="video-container" style="position: relative; width: 100%; padding-bottom: 56.25%; margin-top: 24px; margin-bottom: 40px;">
<iframe class="video" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;" src="https://www.youtube.com/embed/jbkPLQpzPtc?si=k1BUb98QWRT1V8fZ" allowfullscreen></iframe>
</div>
Authorization is giving permission to a user to do an action on the server. As developers, we must ensure that users are only allowed to do what they are authorized.
One way to ensure that authorization has happened is to loudly flag when it hasnt. This is how we do it at [Fleet Device Management](https://www.linkedin.com/company/fleetdm/?lipi=urn%3Ali%3Apage%3Ad_flagship3_pulse_read%3BCaXkx0wxSNeQ8WfF5SZ17g%3D%3D).
In our code base, we use the [go-kit library](https://github.com/go-kit/kit). Most of the general endpoints are created in the handler.go file. For example:
```
// user-authenticated endpoints
ue := newUserAuthenticatedEndpointer(svc, opts, r, apiVersions...)
ue.POST("/api/_version_/fleet/trigger", triggerEndpoint, triggerRequest{})
```
Every endpoint calls **kithttp.NewServer** and wraps the endpoint with our **AuthzCheck**. From [handler.go](https://github.com/fleetdm/fleet/blob/36421bd5055d37a4c39a04e0f9bd96ad47951131/server/service/handler.go#L729):
```
e = authzcheck.NewMiddleware().AuthzCheck()(e)
return kithttp.NewServer(e, decodeFn, encodeResponse, opts...)
```
![Example check](../website/assets/images/articles/catch-missed-authorization-checks-during-software-development-720x179@2x.jpg
"Example check")
This means that after the business logic is processed, the AuthzCheck is called. This check ensures that authorization was checked. Otherwise, an error is returned. From [authzcheck.go](https://github.com/fleetdm/fleet/blob/36421bd5055d37a4c39a04e0f9bd96ad47951131/server/service/middleware/authzcheck/authzcheck.go#L51):
```
// If authorization was not checked, return a response that will
// marshal to a generic error and log that the check was missed.
if !authzctx.Checked() {
// Getting to here means there is an authorization-related bug in our code.
return nil, authz.CheckMissingWithResponse(response)
}
```
This additional check is useful during our development and QA process, to ensure that authorization always happens in our business logic.
<meta name="articleTitle" value="Catch missed authorization checks during software development">
<meta name="authorFullName" value="Victor Lyuboslavsky">
<meta name="authorGitHubUsername" value="getvictor">
<meta name="category" value="guides">
<meta name="publishedOn" value="2023-12-04">
<meta name="description" value="How to perform authorization checks in a golang codebase for cybersecurity">

View File

@ -28,7 +28,7 @@ This section will show you how to:
### Step 1: generate your certificate and key
If you're already using Fleet's macOS MDM features, you already have a certificate and key. These are your SCEP certificate and SCEP private key you used when turning on macOS MDM.
> If you're already using Fleet's macOS MDM features, you already have a SCEP certificate and key. Skip to step 2 and reuse the SCEP certificate and key as your WSTEP certificate and key.
If you're not using macOS MDM features, run the following command to download three files and send an email to you with an attached CSR file.

View File

@ -0,0 +1 @@
* Fix possible deadlocks when upserting to `host_batteries` (found during load test).

View File

@ -69,7 +69,7 @@ const OSSettings = ({
return (
<div className={baseClass}>
<p className={`${baseClass}__description`}>
Remotely enforce settings on macOS hosts assigned to this team.
Remotely enforce OS settings on hosts assigned to this team.
</p>
<ProfileStatusAggregate
isLoading={isLoadingAggregateProfileStatus}

View File

@ -206,16 +206,33 @@ We use these prefixes to organize the Fleet Slack:
Fleet uses Github as the [source of truth](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-one-repo) for our product and documentation; a platform to allow community members to interact with Fleet, [contribute and provide feedback](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#committing-changes).
### GitHub labels
We use special characters to define different types of GitHub labels. By combining labels, we
organize and categorize GitHub issues. This reduces the total number of labels required while
maintaining an expressive labeling system. For example, instead of a label called
`platform-dev-backend`, we use `#platform :dev ~backend`.
Fleet prefixes all GitHub labels with special characters or words to organize and categorize GitHub issues.
| Special character | Label type | Examples |
| Prefix | Label type | Examples |
|:------------------|:------------|:------------------------------------|
| `#` | Noun | `#g-marketing`, `#g-ceo`, `#agent`
| `:` | Verb | `:dev`, `:research`, `:design`
| `~` | Adjective | `~blocked`, `~frontend`, `~backend`
| `customer-` | [Customer request](TODO link to handbook section) | `customer-leo`, `customer-sagittarius`
| `#g-` | Group isssue | _An issue requesting something from a group at Fleet, such that it will be seen and procesed on their kanban board within 1 business day._
Opionated conventions help people work faster and spend less time figuring out what to name things, or misunderstanding why they're named what they are. This also reduces the total number of labels required while maintaining an expressive labeling system. For example, instead of a label called `platform-dev-backend`, we use `#platform :dev ~backend`.
> _**Note:** There are only a few "special" labels that are exceptions to this rule:
> - `bug`
> - `story`
> - `prospect-` _(TODO: This makes sense, but I noticed on Nov 30, 2023 that `customer-` was being used in at least one place `prospect-` was supposed to be. Let's decide what we'll be doing. Easiest thing is probably to switch to `customer-` for everything and then clarify who is a customer in the mapping between labels and actual organization names. Up to Luke and Zay.)_
> - `customer request` _(FUTURE: is this needed? if so, could use special symbol prefix in front of it)_
> - `needs prioritization` _(FUTURE: is this needed? could we use a symbol prefix in front of it?)_
> - `github_actions` _(TODO: why is this here? Instead we can use prefixes, like `~`)_
> - `docker` _(TODO: why is this here? Instead we can use prefixes, like `~`)_
> - `go` _(TODO: why is this here? Instead we can use prefixes, like `~`)_
> - `javascript` _(TODO: why is this here? Instead we can use prefixes, like `~`)_
> - `Epic` _(TODO: Find a way to remove this. It is an artifact from Zenhub and not something we actually want to exist or use, as it is confusing.)_
> - `p4` _(TODO: why is this here? Instead we can use prefixes, like `~`. Or better yet, delete it. What is a P4 anyway?)_
> - `p5` _(TODO: why is this here? Instead we can use prefixes, like `~`. Same as `p4`.)_
> - `story to demo` _(FUTURE: is this needed? if so, could use special symbol prefix in front of it)_
> - `bug to demo` _(FUTURE: is this needed? if so, could use special symbol prefix in front of it)_
### Process new requests
Team members [process their department's kanban boards](https://fleetdm.com/handbook/company/why-this-way#why-lean-software-development) daily, prioritizing all new requests including issues and PRs within one business day.

View File

@ -2862,14 +2862,14 @@ func (ds *Datastore) ReplaceHostDeviceMapping(ctx context.Context, hid uint, map
insPart = ` (?, ?, ?),`
)
// index the mappings by email and source, to quickly check which ones
// need to be deleted and inserted
toIns := make(map[string]*fleet.HostDeviceMapping)
for _, m := range mappings {
toIns[m.Email+"\n"+m.Source] = m
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// index the mappings by email and source, to quickly check which ones
// need to be deleted and inserted
toIns := make(map[string]*fleet.HostDeviceMapping)
for _, m := range mappings {
toIns[m.Email+"\n"+m.Source] = m
}
var prevMappings []*fleet.HostDeviceMapping
if err := sqlx.SelectContext(ctx, tx, &prevMappings, selStmt, hid); err != nil {
return ctxerr.Wrap(ctx, err, "select previous host emails")
@ -2912,65 +2912,95 @@ func (ds *Datastore) ReplaceHostDeviceMapping(ctx context.Context, hid uint, map
}
func (ds *Datastore) ReplaceHostBatteries(ctx context.Context, hid uint, mappings []*fleet.HostBattery) error {
for _, m := range mappings {
if hid != m.HostID {
return ctxerr.Errorf(ctx, "host batteries mapping are not all for the provided host id %d, found %d", hid, m.HostID)
}
}
// The following SQL statements assume a small number of batteries reported per host.
// This is using the same pattern as ReplaceHostDeviceMapping.
const (
replaceStmt = `
INSERT INTO
host_batteries (
selStmt = `
SELECT
id,
host_id,
serial_number,
cycle_count,
health
)
VALUES
%s
ON DUPLICATE KEY UPDATE
cycle_count = VALUES(cycle_count),
health = VALUES(health),
updated_at = CURRENT_TIMESTAMP
`
valuesPart = `(?, ?, ?, ?),`
cycle_count,
health
FROM
host_batteries
WHERE
host_id = ?`
deleteExceptStmt = `
DELETE FROM
host_batteries
WHERE
host_id = ? AND
serial_number NOT IN (?)
`
deleteAllStmt = `
DELETE FROM
host_batteries
WHERE
host_id = ?
`
delStmt = `
DELETE FROM
host_batteries
WHERE
id IN (?)`
insStmt = `
INSERT INTO
host_batteries (host_id, serial_number, cycle_count, health)
VALUES`
insPart = ` (?, ?, ?, ?),`
)
replaceArgs := make([]interface{}, 0, len(mappings)*4)
deleteNotIn := make([]string, 0, len(mappings))
for _, hb := range mappings {
deleteNotIn = append(deleteNotIn, hb.SerialNumber)
replaceArgs = append(replaceArgs, hid, hb.SerialNumber, hb.CycleCount, hb.Health)
keyFn := func(b *fleet.HostBattery) string {
return b.SerialNumber + ":" + fmt.Sprint(b.CycleCount) + ":" + b.Health
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// first, insert the new batteries or update the existing ones
if len(replaceArgs) > 0 {
if _, err := tx.ExecContext(ctx, fmt.Sprintf(replaceStmt, strings.TrimSuffix(strings.Repeat(valuesPart, len(mappings)), ",")), replaceArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "upsert host batteries")
// Index the mappings by serial_number, to quickly check which ones
// need to be deleted and inserted.
toIns := make(map[string]*fleet.HostBattery)
serials := make(map[string]struct{})
for _, m := range mappings {
if _, ok := serials[m.SerialNumber]; ok {
// Ignore multiple rows with the same serial number
// (e.g. in case of bugs in results reported by osquery).
continue
}
toIns[keyFn(m)] = m
serials[m.SerialNumber] = struct{}{}
}
var prevMappings []*fleet.HostBattery
if err := sqlx.SelectContext(ctx, tx, &prevMappings, selStmt, hid); err != nil {
return ctxerr.Wrap(ctx, err, "select previous host batteries")
}
var delIDs []uint
for _, pm := range prevMappings {
key := keyFn(pm)
if _, ok := toIns[key]; ok {
// already exists, no need to insert
delete(toIns, key)
} else {
// does not exist anymore, must be deleted
delIDs = append(delIDs, pm.ID)
}
}
// then, delete the old ones
if len(deleteNotIn) > 0 {
delStmt, args, err := sqlx.In(deleteExceptStmt, hid, deleteNotIn)
if len(delIDs) > 0 {
stmt, args, err := sqlx.In(delStmt, delIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "generating host batteries delete NOT IN statement")
return ctxerr.Wrap(ctx, err, "prepare delete statement")
}
if _, err := tx.ExecContext(ctx, delStmt, args...); err != nil {
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "delete host batteries")
}
} else if _, err := tx.ExecContext(ctx, deleteAllStmt, hid); err != nil {
return ctxerr.Wrap(ctx, err, "delete all host batteries")
}
if len(toIns) > 0 {
var args []interface{}
for _, m := range toIns {
args = append(args, hid, m.SerialNumber, m.CycleCount, m.Health)
}
stmt := insStmt + strings.TrimSuffix(strings.Repeat(insPart, len(toIns)), ",")
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "insert host batteries")
}
}
return nil
})

View File

@ -29,6 +29,7 @@ import (
"github.com/micromdm/nanodep/godep"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
var expLastExec = func() time.Time {
@ -134,6 +135,7 @@ func TestHosts(t *testing.T) {
{"DeleteHosts", testHostsDeleteHosts},
{"HostIDsByOSVersion", testHostIDsByOSVersion},
{"ReplaceHostBatteries", testHostsReplaceHostBatteries},
{"ReplaceHostBatteriesDeadlock", testHostsReplaceHostBatteriesDeadlock},
{"CountHostsNotResponding", testCountHostsNotResponding},
{"FailingPoliciesCount", testFailingPoliciesCount},
{"HostRecordNoPolicies", testHostsRecordNoPolicies},
@ -6000,6 +6002,31 @@ func testHostsReplaceHostBatteries(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.ElementsMatch(t, h1Bat, bat1)
type timestamp struct {
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
var timestamps1 []timestamp
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &timestamps1, `SELECT created_at, updated_at FROM host_batteries WHERE host_id = ?`, h1.ID)
})
// Insert the same battery data again.
err = ds.ReplaceHostBatteries(ctx, h1.ID, h1Bat)
require.NoError(t, err)
var timestamps2 []timestamp
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &timestamps2, `SELECT created_at, updated_at FROM host_batteries WHERE host_id = ?`, h1.ID)
})
// Verify that there were no inserts/updates (because reported data hasn't changed).
require.ElementsMatch(t, timestamps1, timestamps2)
bat1, err = ds.ListHostBatteries(ctx, h1.ID)
require.NoError(t, err)
require.ElementsMatch(t, h1Bat, bat1)
bat2, err := ds.ListHostBatteries(ctx, h2.ID)
require.NoError(t, err)
require.Len(t, bat2, 0)
@ -6045,6 +6072,46 @@ func testHostsReplaceHostBatteries(t *testing.T, ds *Datastore) {
require.ElementsMatch(t, h2Bat, bat2)
}
func testHostsReplaceHostBatteriesDeadlock(t *testing.T, ds *Datastore) {
ctx := context.Background()
var hosts []*fleet.Host
for i := 1; i <= 100; i++ {
h, err := ds.NewHost(ctx, &fleet.Host{
ID: uint(i),
OsqueryHostID: ptr.String(fmt.Sprintf("id-%d", i)),
NodeKey: ptr.String(fmt.Sprintf("key-%d", i)),
Platform: "linux",
Hostname: fmt.Sprintf("host-%d", i),
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
})
require.NoError(t, err)
hosts = append(hosts, h)
}
var g errgroup.Group
for _, h := range hosts {
hostID := h.ID
g.Go(func() error {
for i := 0; i < 100; i++ {
if err := ds.ReplaceHostBatteries(ctx, hostID, []*fleet.HostBattery{
{HostID: hostID, SerialNumber: fmt.Sprintf("%d-0000", hostID), CycleCount: 1, Health: "Good"},
{HostID: hostID, SerialNumber: fmt.Sprintf("%d-0000", hostID), CycleCount: 2, Health: "Fair"},
}); err != nil {
return err
}
time.Sleep(10 * time.Millisecond)
}
return nil
})
}
err := g.Wait()
require.NoError(t, err)
}
func testCountHostsNotResponding(t *testing.T, ds *Datastore) {
ctx := context.Background()
config := config.FleetConfig{Osquery: config.OsqueryConfig{DetailUpdateInterval: 1 * time.Hour}}

View File

@ -982,6 +982,7 @@ func (h *HostMDM) UnmarshalJSON(b []byte) error {
// HostBattery represents a host's battery, as reported by the osquery battery
// table.
type HostBattery struct {
ID uint `json:"-" db:"id"`
HostID uint `json:"-" db:"host_id"`
SerialNumber string `json:"-" db:"serial_number"`
CycleCount int `json:"cycle_count" db:"cycle_count"`

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB