Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x](backport #41044) x-pack/filebeat/input/entityanalytics/okta/internal: add role and factor client calls #41251

Open
wants to merge 2 commits into
base: 8.x
Choose a base branch
from
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
1 change: 1 addition & 0 deletions CHANGELOG-developer.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ The list below covers the major changes between 7.0.0-rc2 and main only.
- Simplified GCS input state checkpoint calculation logic. {issue}40878[40878] {pull}40937[40937]
- 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 collecting Okta role and factor data for users with filebeat entityanalytics input. {pull}41044[41044]

==== Deprecated

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
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.
Expand Down Expand Up @@ -72,6 +72,37 @@
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
Expand Down Expand Up @@ -176,6 +207,48 @@
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.
//
Expand All @@ -197,6 +270,27 @@
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.
Expand Down Expand Up @@ -250,7 +344,7 @@

// entity is an Okta entity analytics entity.
type entity interface {
User | Group | Device | devUser
User | Group | Role | Factor | Device | devUser
}

type devUser struct {
Expand Down Expand Up @@ -288,7 +382,7 @@
defer resp.Body.Close()
err = oktaRateLimit(resp.Header, window, lim, log)
if err != nil {
io.Copy(io.Discard, resp.Body)

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

View workflow job for this annotation

GitHub Actions / lint (linux)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,56 @@
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 == "" {
Expand Down Expand Up @@ -387,7 +437,7 @@
func TestNext(t *testing.T) {
for i, test := range nextTests {
got, err := Next(test.header)
if err != test.wantErr {

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

View workflow job for this annotation

GitHub Actions / lint (linux)

comparing with != will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)
t.Errorf("unexpected ok result for %d: got:%v want:%v", i, err, test.wantErr)
}
if got.Encode() != test.want {
Expand Down
Loading