From e3906d8ceb1917208ea86b9da2af0343a2d37f1b Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Wed, 16 Oct 2024 16:55:47 +1030 Subject: [PATCH] x-pack/filebeat/input/entityanalytics/okta/internal: add role and factor client calls (#41044) (cherry picked from commit 10a2e9437436d9c403ae70aa7e2712fd0cf512bb) --- CHANGELOG-developer.next.asciidoc | 1 + .../provider/okta/internal/okta/okta.go | 98 ++++++++++++++++++- .../provider/okta/internal/okta/okta_test.go | 50 ++++++++++ 3 files changed, 147 insertions(+), 2 deletions(-) diff --git a/CHANGELOG-developer.next.asciidoc b/CHANGELOG-developer.next.asciidoc index b44f75e4f7b..01a7205e713 100644 --- a/CHANGELOG-developer.next.asciidoc +++ b/CHANGELOG-developer.next.asciidoc @@ -210,6 +210,7 @@ The list below covers the major changes between 7.0.0-rc2 and main only. - Simplified Azure Blob Storage input state checkpoint calculation logic. {issue}40674[40674] {pull}40936[40936] - Add field redaction package. {pull}40997[40997] - Add support for marked redaction to x-pack/filebeat/input/internal/private {pull}41212[41212] +- Add support for collecting Okta role and factor data for users with filebeat entityanalytics input. {pull}41044[41044] ==== Deprecated diff --git a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go index ef574ef4d26..3d8bdae11c9 100644 --- a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go +++ b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go @@ -44,7 +44,7 @@ type User struct { Profile map[string]any `json:"profile"` Credentials *Credentials `json:"credentials,omitempty"` Links HAL `json:"_links,omitempty"` // See https://developer.okta.com/docs/reference/api/users/#links-object for details. - Embedded HAL `json:"_embedded,omitempty"` + Embedded map[string]any `json:"_embedded,omitempty"` } // Credentials is a redacted Okta user's credential details. Only the credential provider is retained. @@ -72,6 +72,37 @@ type Group struct { Profile map[string]any `json:"profile"` } +// Factor is an Okta identity factor description. +// +// See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/UserFactor/#tag/UserFactor/operation/listFactors. +type Factor struct { + ID string `json:"id"` + FactorType string `json:"factorType"` + Provider string `json:"provider"` + VendorName string `json:"vendorName"` + Status string `json:"status"` + Created time.Time `json:"created"` + LastUpdated time.Time `json:"lastUpdated"` + Profile map[string]any `json:"profile"` + Links HAL `json:"_links,omitempty"` + Embedded map[string]any `json:"_embedded,omitempty"` +} + +// Role is an Okta user role description. +// +// See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/RoleAssignmentAUser/#tag/RoleAssignmentAUser/operation/listAssignedRolesForUser +// and https://developer.okta.com/docs/api/openapi/okta-management/management/tag/RoleAssignmentBGroup/#tag/RoleAssignmentBGroup/operation/listGroupAssignedRoles. +type Role struct { + ID string `json:"id"` + Label string `json:"label"` + Type string `json:"type"` + Status string `json:"status"` + Created time.Time `json:"created"` + LastUpdated time.Time `json:"lastUpdated"` + AssignmentType string `json:"assignmentType"` + Links HAL `json:"_links"` +} + // Device is an Okta device's details. // // See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Device/#tag/Device/operation/listDevices for details @@ -176,6 +207,48 @@ func GetUserDetails(ctx context.Context, cli *http.Client, host, key, user strin return getDetails[User](ctx, cli, u, key, user == "", omit, lim, window, log) } +// GetUserFactors 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. +// +// See GetUserDetails for details of the query and rate limit parameters. +// +// See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/UserFactor/#tag/UserFactor/operation/listFactors. +func GetUserFactors(ctx context.Context, cli *http.Client, host, key, user string, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]Factor, http.Header, error) { + const endpoint = "/api/v1/users" + + if user == "" { + return nil, nil, errors.New("no user specified") + } + + u := &url.URL{ + Scheme: "https", + Host: host, + Path: path.Join(endpoint, user, "factors"), + } + return getDetails[Factor](ctx, cli, u, key, true, OmitNone, lim, window, log) +} + +// GetUserRoles 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. +// +// See GetUserDetails for details of the query and rate limit parameters. +// +// See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/RoleAssignmentBGroup/#tag/RoleAssignmentBGroup/operation/listGroupAssignedRoles. +func GetUserRoles(ctx context.Context, cli *http.Client, host, key, user string, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]Role, http.Header, error) { + const endpoint = "/api/v1/users" + + if user == "" { + return nil, nil, errors.New("no user specified") + } + + u := &url.URL{ + Scheme: "https", + Host: host, + Path: path.Join(endpoint, user, "roles"), + } + return getDetails[Role](ctx, cli, u, key, true, OmitNone, lim, window, log) +} + // GetUserGroupDetails returns Okta group details using the users API endpoint. host is the // Okta user domain and key is the API token to use for the query. user must not be empty. // @@ -197,6 +270,27 @@ func GetUserGroupDetails(ctx context.Context, cli *http.Client, host, key, user return getDetails[Group](ctx, cli, u, key, true, OmitNone, lim, window, 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. +// +// See GetUserDetails for details of the query and rate limit parameters. +// +// See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/RoleAssignmentBGroup/#tag/RoleAssignmentBGroup/operation/listGroupAssignedRoles. +func GetGroupRoles(ctx context.Context, cli *http.Client, host, key, group string, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]Role, http.Header, error) { + const endpoint = "/api/v1/groups" + + if group == "" { + return nil, nil, errors.New("no group specified") + } + + u := &url.URL{ + Scheme: "https", + Host: host, + Path: path.Join(endpoint, group, "roles"), + } + return getDetails[Role](ctx, cli, u, key, true, OmitNone, lim, window, log) +} + // GetDeviceDetails returns Okta device details using the list devices API endpoint. host is the // Okta user domain and key is the API token to use for the query. If device is not empty, // details for the specific device are returned, otherwise a list of all devices is returned. @@ -250,7 +344,7 @@ func GetDeviceUsers(ctx context.Context, cli *http.Client, host, key, device str // entity is an Okta entity analytics entity. type entity interface { - User | Group | Device | devUser + User | Group | Role | Factor | Device | devUser } type devUser struct { diff --git a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta_test.go b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta_test.go index 2ce43925221..9b04d3996bf 100644 --- a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta_test.go +++ b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta_test.go @@ -116,6 +116,56 @@ func Test(t *testing.T) { t.Logf("groups: %s", b) }) + t.Run("my_roles", func(t *testing.T) { + query := make(url.Values) + query.Set("limit", "200") + roles, _, err := GetUserRoles(context.Background(), http.DefaultClient, host, key, me.ID, limiter, window, logger) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(roles) == 0 { + t.Fatalf("unexpected len(roles): got:%d want>0", len(roles)) + } + + if omit&OmitCredentials != 0 && me.Credentials != nil { + t.Errorf("unexpected credentials with %s: %#v", omit, me.Credentials) + } + + if !*logResponses { + return + } + b, err := json.Marshal(roles) + if err != nil { + t.Errorf("failed to marshal roles for logging: %v", err) + } + t.Logf("roles: %s", b) + }) + + t.Run("my_factors", func(t *testing.T) { + query := make(url.Values) + query.Set("limit", "200") + factors, _, err := GetUserFactors(context.Background(), http.DefaultClient, host, key, me.ID, limiter, window, logger) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(factors) == 0 { + t.Fatalf("unexpected len(factors): got:%d want>0", len(factors)) + } + + if omit&OmitCredentials != 0 && me.Credentials != nil { + t.Errorf("unexpected credentials with %s: %#v", omit, me.Credentials) + } + + if !*logResponses { + return + } + b, err := json.Marshal(factors) + if err != nil { + t.Errorf("failed to marshal factors for logging: %v", err) + } + t.Logf("factors: %s", b) + }) + t.Run("user", func(t *testing.T) { login, _ := me.Profile["login"].(string) if login == "" {