Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions changelog/fragments/1774968309-okta-enrich-enrolled-devices.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# REQUIRED
# Kind can be one of:
# - breaking-change: a change to previously-documented behavior
# - deprecation: functionality that is being removed in a later release
# - bug-fix: fixes a problem in a previous version
# - enhancement: extends functionality but does not break or fix existing behavior
# - feature: new functionality
# - known-issue: problems that we are aware of in a given version
# - security: impacts on the security of a product or a user's deployment.
# - upgrade: important information for someone upgrading from a prior version
# - other: does not fit into any of the other categories
kind: enhancement

# REQUIRED for all kinds
# Change summary; a 80ish characters long description of the change.
summary: Add enrolled_devices enrichment option to Okta entity analytics provider.

# REQUIRED for breaking-change, deprecation, known-issue
# Long description; in case the summary is not enough to describe the change
# this field accommodate a description without length limits.
description: |
Adds enrolled_devices as a new optional value for the enrich_with configuration
option in the Okta entity analytics provider. When enabled, each user is enriched
with the list of devices enrolled for that user via the List User Devices Okta API
endpoint. The enrichment is opt-in and excluded from the default configuration to
avoid the extra per-user API call that would increase Okta rate limit consumption.

# REQUIRED for breaking-change, deprecation, known-issue
# impact:

# REQUIRED for breaking-change, deprecation, known-issue
# action:

# REQUIRED for all kinds
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
component: filebeat

# AUTOMATED
# OPTIONAL to manually add other PR URLs
# PR URL: A link the PR that added the changeset.
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
# Please provide it if you are adding a fragment for a different PR.
pr: https://github.com/elastic/beats/pull/49813

# AUTOMATED
# OPTIONAL to manually add other issue URLs
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
# If not present is automatically filled by the tooling with the issue linked to the PR number.
# issue: https://github.com/owner/repo/1234
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,9 @@

#### `enrich_with` [_enrich_with]

The metadata to enrich users with. This is an array of values that may contain "groups", "roles" and "factors", or "none". If the array only contains "none", no metadata is collected for users. The default behavior is to collect "groups".
The metadata to enrich users with. This is an array of values that may contain "groups", "roles", "factors" and "devices", or "none". If the array only contains "none", no metadata is collected for users. The default behavior is to collect "groups".

Check notice on line 1234 in docs/reference/filebeat/filebeat-input-entity-analytics.md

View workflow job for this annotation

GitHub Actions / build / vale

Elastic.WordChoice: Consider using 'can, might' instead of 'may', unless the term is in the UI.

Check warning on line 1234 in docs/reference/filebeat/filebeat-input-entity-analytics.md

View workflow job for this annotation

GitHub Actions / build / vale

Elastic.QuotesPunctuation: Place punctuation inside closing quotation marks.

Check warning on line 1234 in docs/reference/filebeat/filebeat-input-entity-analytics.md

View workflow job for this annotation

GitHub Actions / build / vale

Elastic.QuotesPunctuation: Place punctuation inside closing quotation marks.

Check warning on line 1234 in docs/reference/filebeat/filebeat-input-entity-analytics.md

View workflow job for this annotation

GitHub Actions / build / vale

Elastic.QuotesPunctuation: Place punctuation inside closing quotation marks.

Check warning on line 1234 in docs/reference/filebeat/filebeat-input-entity-analytics.md

View workflow job for this annotation

GitHub Actions / build / vale

Elastic.QuotesPunctuation: Place punctuation inside closing quotation marks.

Check warning on line 1234 in docs/reference/filebeat/filebeat-input-entity-analytics.md

View workflow job for this annotation

GitHub Actions / build / vale

Elastic.QuotesPunctuation: Place punctuation inside closing quotation marks.

Check warning on line 1234 in docs/reference/filebeat/filebeat-input-entity-analytics.md

View workflow job for this annotation

GitHub Actions / build / vale

Elastic.QuotesPunctuation: Place punctuation inside closing quotation marks.

When "devices" is included, each user is enriched with the list of devices enrolled for that user by calling the [List User Devices](https://developer.okta.com/docs/api/openapi/okta-management/management/tags/userresources/other/listuserdevices) API. This requires one additional API request per user, so it is disabled by default to avoid hitting Okta rate limits.

Check notice on line 1236 in docs/reference/filebeat/filebeat-input-entity-analytics.md

View workflow job for this annotation

GitHub Actions / build / vale

Elastic.WordChoice: Consider using 'deactivated, deselected, hidden, turned off, unavailable' instead of 'disabled', unless the term is in the UI.


#### `sync_interval` [_sync_interval_4]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
Dataset string `config:"dataset"`
// EnrichWith specifies the additional data that
// will be used to enrich user data. It can include
// "groups", "roles" and "factors".
// "groups", "roles", "factors" and "devices".
// If it is a single element with "none", no
// enrichment is performed.
EnrichWith []string `config:"enrich_with"`
Expand Down Expand Up @@ -349,7 +349,7 @@
}

// populateJSONFromFile reads a JSON file and populates the destination.
func populateJSONFromFile(file string, dst *[]byte) error {

Check failure on line 352 in x-pack/filebeat/input/entityanalytics/provider/okta/conf.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

func populateJSONFromFile is unused (unused)

Check failure on line 352 in x-pack/filebeat/input/entityanalytics/provider/okta/conf.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

func populateJSONFromFile is unused (unused)

Check failure on line 352 in x-pack/filebeat/input/entityanalytics/provider/okta/conf.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

func populateJSONFromFile is unused (unused)
_, err := os.Stat(file)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("the file %q cannot be found", file)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,29 @@
return getDetails[Group](ctx, cli, u, endpoint, key, true, OmitNone, lim, log)
}

// GetUserDevices returns Okta device details for devices enrolled by the provided user
// using the list user devices API. host is the Okta user domain and key is the API
// token to use for the query. user must not be empty.
//
// See GetUserDetails for details of the query and rate limit parameters.
//
// See https://developer.okta.com/docs/api/openapi/okta-management/management/tags/userresources/other/listuserdevices for details.
func GetUserDevices(ctx context.Context, cli *http.Client, host, key, user string, lim *RateLimiter, log *logp.Logger) ([]Device, http.Header, error) {
if user == "" {
return nil, nil, errors.New("no user specified")
}

const endpoint = "/api/v1/users/{user}/devices"
path := strings.Replace(endpoint, "{user}", user, 1)

u := &url.URL{
Scheme: "https",
Host: host,
Path: path,
}
return getDetails[Device](ctx, cli, u, endpoint, key, true, OmitNone, lim, log)
}

// GetGroupRoles returns Okta group roles using the groups API endpoint. host is the
// Okta user domain and key is the API token to use for the query. group must not be empty.
//
Expand Down Expand Up @@ -414,7 +437,7 @@
defer resp.Body.Close()
err = lim.Update(endpoint, resp.Header, log)
if err != nil {
io.Copy(io.Discard, resp.Body)

Check failure on line 440 in x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

Error return value of `io.Copy` is not checked (errcheck)

Check failure on line 440 in x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

Error return value of `io.Copy` is not checked (errcheck)

Check failure on line 440 in x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

Error return value of `io.Copy` is not checked (errcheck)
return nil, nil, err
}

Expand Down
9 changes: 9 additions & 0 deletions x-pack/filebeat/input/entityanalytics/provider/okta/okta.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@

next, err := okta.Next(h)
if err != nil {
if err == io.EOF {

Check failure on line 525 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)

Check failure on line 525 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)

Check failure on line 525 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)
break
}
p.logger.Debugf("received %d users from API", n)
Expand Down Expand Up @@ -579,6 +579,14 @@
su.Roles = roles
}
}
if slices.Contains(p.cfg.EnrichWith, "devices") {
devices, _, err := okta.GetUserDevices(ctx, p.client, p.cfg.OktaDomain, p.getAuthToken(), u.ID, p.lim, p.logger)
if err != nil {
p.logger.Warnf("failed to get enrolled devices for user %s: %v", u.ID, err)
} else {
su.Devices = devices
}
}
return su
}

Expand Down Expand Up @@ -667,7 +675,7 @@

next, err := okta.Next(h)
if err != nil {
if err == io.EOF {

Check failure on line 678 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)

Check failure on line 678 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)

Check failure on line 678 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)
break
}
p.logger.Debugf("received %d devices from API", n)
Expand Down Expand Up @@ -697,7 +705,7 @@

next, err := okta.Next(h)
if err != nil {
if err == io.EOF {

Check failure on line 708 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)

Check failure on line 708 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)

Check failure on line 708 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)
break
}
p.logger.Debugf("received %d devices from API", n)
Expand Down Expand Up @@ -727,7 +735,7 @@
return b
}

type entity interface {

Check failure on line 738 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

type entity is unused (unused)

Check failure on line 738 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

type entity is unused (unused)

Check failure on line 738 in x-pack/filebeat/input/entityanalytics/provider/okta/okta.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

type entity is unused (unused)
*User | *Device | okta.User
}

Expand Down Expand Up @@ -770,6 +778,7 @@
_, _ = userDoc.Put("groups", u.Groups)
_, _ = userDoc.Put("roles", u.Roles)
_, _ = userDoc.Put("factors", u.Factors)
_, _ = userDoc.Put("devices", u.Devices)

switch u.State {
case Deleted:
Expand Down
38 changes: 28 additions & 10 deletions x-pack/filebeat/input/entityanalytics/provider/okta/okta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
var trace = flag.Bool("request_trace", false, "enable request tracing during tests")

func TestOktaDoFetch(t *testing.T) {
logp.TestingSetup()

Check failure on line 30 in x-pack/filebeat/input/entityanalytics/provider/okta/okta_test.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

SA1019: logp.TestingSetup is deprecated: Prefer using localized loggers. Use logptest.NewTestingLogger. (staticcheck)

Check failure on line 30 in x-pack/filebeat/input/entityanalytics/provider/okta/okta_test.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

SA1019: logp.TestingSetup is deprecated: Prefer using localized loggers. Use logptest.NewTestingLogger. (staticcheck)

Check failure on line 30 in x-pack/filebeat/input/entityanalytics/provider/okta/okta_test.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

SA1019: logp.TestingSetup is deprecated: Prefer using localized loggers. Use logptest.NewTestingLogger. (staticcheck)

tests := []struct {
dataset string
Expand All @@ -39,6 +39,7 @@
{dataset: "all", enrichWith: []string{"groups"}, wantUsers: true, wantDevices: true},
{dataset: "users", enrichWith: []string{"groups", "roles", "factors"}, wantUsers: true, wantDevices: false},
{dataset: "devices", enrichWith: []string{"groups"}, wantUsers: false, wantDevices: true},
{dataset: "users", enrichWith: []string{"groups", "devices"}, wantUsers: true, wantDevices: false},
}

for _, test := range tests {
Expand All @@ -61,14 +62,17 @@
groups = `[{"id":"USERID","profile":{"description":"All users in your organization","name":"Everyone"}}]`
factors = `[{"id":"ufs2bysphxKODSZKWVCT","factorType":"question","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2014-04-15T18:10:06.000Z","lastUpdated":"2014-04-15T18:10:06.000Z","profile":{"question":"favorite_art_piece","questionText":"What is your favorite piece of art?"}},{"id":"ostf2gsyictRQDSGTDZE","factorType":"token:software:totp","provider":"OKTA","status":"PENDING_ACTIVATION","created":"2014-06-27T20:27:33.000Z","lastUpdated":"2014-06-27T20:27:33.000Z","profile":{"credentialId":"dade.murphy@example.com"}},{"id":"sms2gt8gzgEBPUWBIFHN","factorType":"sms","provider":"OKTA","status":"ACTIVE","created":"2014-06-27T20:27:26.000Z","lastUpdated":"2014-06-27T20:27:26.000Z","profile":{"phoneNumber":"+1-555-415-1337"}}]`
devices = `[{"id":"DEVICEID","status":"STATUS","created":"2019-10-02T18:03:07.000Z","lastUpdated":"2019-10-02T18:03:07.000Z","profile":{"displayName":"Example Device name 1","platform":"WINDOWS","serialNumber":"XXDDRFCFRGF3M8MD6D","sid":"S-1-11-111","registered":true,"secureHardwarePresent":false,"diskEncryptionType":"ALL_INTERNAL_VOLUMES"},"resourceType":"UDDevice","resourceDisplayName":{"value":"Example Device name 1","sensitive":false},"resourceAlternateId":null,"resourceId":"DEVICEID","_links":{"activate":{"href":"https://localhost/api/v1/devices/DEVICEID/lifecycle/activate","hints":{"allow":["POST"]}},"self":{"href":"https://localhost/api/v1/devices/DEVICEID","hints":{"allow":["GET","PATCH","PUT"]}},"users":{"href":"https://localhost/api/v1/devices/DEVICEID/users","hints":{"allow":["GET"]}}}}]`
// userDevices is sample data from https://developer.okta.com/docs/api/openapi/okta-management/management/tags/userresources/other/listuserdevices
userDevices = `[{"id":"guo4a5uyerdpvAiJT0h7","status":"ACTIVE","created":"2022-05-14T13:37:20.000Z","lastUpdated":"2022-05-14T13:37:20.000Z","profile":{"displayName":"DESKTOP-XXXX","platform":"WINDOWS","manufacturer":"LENOVO","model":"20BH002DUS","osVersion":"10.0.19043","serialNumber":"1XXXX0X0X","registered":true,"secureHardwarePresent":false,"diskEncryptionType":"ALL_INTERNAL_VOLUMES"},"resourceType":"UDDevice","resourceDisplayName":{"value":"DESKTOP-XXXX","sensitive":false},"resourceAlternateId":null,"resourceId":"guo4a5uyerdpvAiJT0h7","_links":{"activate":{"href":"https://localhost/api/v1/devices/guo4a5uyerdpvAiJT0h7/lifecycle/activate","hints":{"allow":["POST"]}},"self":{"href":"https://localhost/api/v1/devices/guo4a5uyerdpvAiJT0h7","hints":{"allow":["GET","PATCH","PUT"]}}}}]`
)

data := map[string]string{
"users": users,
"roles": roles,
"groups": groups,
"devices": devices,
"factors": factors,
"users": users,
"roles": roles,
"groups": groups,
"devices": devices,
"factors": factors,
"user_devices": userDevices,
}

var wantUsers []User
Expand Down Expand Up @@ -107,6 +111,13 @@
t.Fatalf("failed to unmarshal role data: %v", err)
}
}
var wantUserDevices []okta.Device
if slices.Contains(test.enrichWith, "devices") {
err := json.Unmarshal([]byte(userDevices), &wantUserDevices)
if err != nil {
t.Fatalf("failed to unmarshal user device data: %v", err)
}
}

wantStates := make(map[string]State)

Expand All @@ -123,13 +134,17 @@
mux.Handle("/api/v1/users/{userid}/{metadata}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
setHeaders(w)
attr := r.PathValue("metadata")
if attr != "groups" {
switch attr {
case "groups":
// Replace USERID placeholder with the actual user ID.
userid := r.PathValue("userid")
fmt.Fprintln(w, strings.ReplaceAll(data[attr], "USERID", userid))
case "devices":
// User-enrolled devices are served from a separate data key.
fmt.Fprintln(w, data["user_devices"])
default:
fmt.Fprintln(w, data[attr])
return
}
// Give the groups if this is a get user groups request.
userid := r.PathValue("userid")
fmt.Fprintln(w, strings.ReplaceAll(data[attr], "USERID", userid))
}))
mux.Handle("/api/v1/devices/{deviceid}/users", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
setHeaders(w)
Expand Down Expand Up @@ -236,6 +251,9 @@
if len(g.Roles) != len(wantRoles) {
t.Errorf("number of roles for user %d: got:%d want:%d", i, len(g.Roles), len(wantRoles))
}
if len(g.Devices) != len(wantUserDevices) {
t.Errorf("number of enrolled devices for user %d: got:%d want:%d", i, len(g.Devices), len(wantUserDevices))
}
for j, gg := range g.Groups {
if gg.ID != wantID {
t.Errorf("unexpected used ID for user group %d in %d: got:%s want:%s", j, i, gg.ID, wantID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type User struct {
Groups []okta.Group `json:"groups"`
Roles []okta.Role `json:"roles"`
Factors []okta.Factor `json:"factors"`
Devices []okta.Device `json:"devices"`
State State `json:"state"`
}

Expand Down
Loading