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
137 changes: 133 additions & 4 deletions pkg/connector/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/okta/okta-sdk-golang/v2/okta"
"github.com/okta/okta-sdk-golang/v2/okta/query"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/structpb"
)

type appResourceType struct {
Expand Down Expand Up @@ -184,13 +185,21 @@ func (o *appResourceType) listAppGroupGrants(
for _, applicationGroupAssignment := range applicationGroupAssignments {
groupID := applicationGroupAssignment.Id
principalID := &v2.ResourceId{ResourceType: resourceTypeGroup.Id, Resource: groupID}
rv = append(rv, sdkGrant.NewGrant(resource, "access", principalID,

grantOptions := []sdkGrant.GrantOption{
sdkGrant.WithAnnotation(
&v2.V1Identifier{
Id: fmtGrantIdV1(V1MembershipEntitlementID(resource.Id.Resource), groupID),
},
),
))
}

// Include group assignment profile attributes as grant metadata if available
if profileMetadata := appGroupAssignmentProfileToMetadata(applicationGroupAssignment); profileMetadata != nil {
grantOptions = append(grantOptions, sdkGrant.WithGrantMetadata(profileMetadata))
}

rv = append(rv, sdkGrant.NewGrant(resource, "access", principalID, grantOptions...))
}

return rv, annos, bag, nil
Expand Down Expand Up @@ -228,13 +237,21 @@ func (o *appResourceType) listAppUsersGrants(

userID := applicationUser.Id
principalID := &v2.ResourceId{ResourceType: resourceTypeUser.Id, Resource: userID}
rv = append(rv, sdkGrant.NewGrant(resource, "access", principalID,

grantOptions := []sdkGrant.GrantOption{
sdkGrant.WithAnnotation(
&v2.V1Identifier{
Id: fmtGrantIdV1(V1MembershipEntitlementID(resource.Id.Resource), userID),
},
),
))
}

// Include app user profile attributes as grant metadata if available
if profileMetadata := appUserProfileToMetadata(applicationUser); profileMetadata != nil {
grantOptions = append(grantOptions, sdkGrant.WithGrantMetadata(profileMetadata))
}

rv = append(rv, sdkGrant.NewGrant(resource, "access", principalID, grantOptions...))
}

return rv, annos, bag, nil
Expand Down Expand Up @@ -587,6 +604,118 @@ func (o *appResourceType) Get(ctx context.Context, resourceId *v2.ResourceId, pa
return resource, annos, nil
}

// appUserProfileToMetadata converts an Okta AppUser's profile into a metadata map
// suitable for attaching to grants. This includes app-specific attributes like
// assigned scopes, app roles, and custom profile fields that are set on the user's
// app assignment in Okta.
func appUserProfileToMetadata(appUser *okta.AppUser) map[string]interface{} {
if appUser == nil || appUser.Profile == nil {
return nil
}

profile, ok := appUser.Profile.(map[string]interface{})
if !ok {
return nil
}

if len(profile) == 0 {
return nil
}

metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = toStructpbCompatibleValue(v)
}

// Include scope and status from the app user assignment itself.
if appUser.Scope != "" {
metadata["_scope"] = appUser.Scope
}
if appUser.Status != "" {
metadata["_status"] = appUser.Status
}
if appUser.ExternalId != "" {
metadata["_externalId"] = appUser.ExternalId
}

// Validate that the metadata can be converted to a structpb.Struct
// to avoid panics in WithGrantMetadata/NewGrant.
if _, err := structpb.NewStruct(metadata); err != nil {
return nil
}

return metadata
Comment on lines +621 to +647

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential panic risk: WithGrantMetadata calls structpb.NewStruct(metadata), and if that returns an error, NewGrant will panic (see grant.go:90). While JSON-deserialized profiles should contain only structpb-compatible types, Okta app profiles are arbitrary and could theoretically contain values that structpb can't handle (e.g., deeply nested structures or unexpected types from custom schema extensions).

Consider either:

  1. Pre-validating the metadata with structpb.NewStruct() before passing it, logging and skipping on error, or
  2. Converting all values to strings (safest for arbitrary profile data)

This is a low-probability issue since JSON-unmarshaled maps typically contain only compatible types, but since the failure mode is a panic (not a returned error), it's worth hardening.

Suggested change
if len(profile) == 0 {
return nil
}
metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}
// Include scope and status from the app user assignment itself
if appUser.Scope != "" {
metadata["_scope"] = appUser.Scope
}
if appUser.Status != "" {
metadata["_status"] = appUser.Status
}
if appUser.ExternalId != "" {
metadata["_externalId"] = appUser.ExternalId
}
return metadata
metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}
// Include scope and status from the app user assignment itself
if appUser.Scope != "" {
metadata["_scope"] = appUser.Scope
}
if appUser.Status != "" {
metadata["_status"] = appUser.Status
}
if appUser.ExternalId != "" {
metadata["_externalId"] = appUser.ExternalId
}
// Validate that metadata is structpb-compatible to avoid panic in NewGrant
if _, err := structpb.NewStruct(metadata); err != nil {
return nil
}
return metadata

}

// appGroupAssignmentProfileToMetadata converts an Okta ApplicationGroupAssignment's
// profile into a metadata map suitable for attaching to grants.
func appGroupAssignmentProfileToMetadata(assignment *okta.ApplicationGroupAssignment) map[string]interface{} {
if assignment == nil || assignment.Profile == nil {
return nil
}

profile, ok := assignment.Profile.(map[string]interface{})
if !ok {
return nil
}

if len(profile) == 0 {
return nil
}

metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = toStructpbCompatibleValue(v)
}

// Validate that the metadata can be converted to a structpb.Struct
// to avoid panics in WithGrantMetadata/NewGrant.
if _, err := structpb.NewStruct(metadata); err != nil {
return nil
}

return metadata
Comment on lines +663 to +677

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same panic risk as appUserProfileToMetadata — if the profile contains structpb-incompatible values, NewGrant will panic. Consider adding the same validation here.

Suggested change
return nil
}
metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}
return metadata
metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}
// Validate that metadata is structpb-compatible to avoid panic in NewGrant
if _, err := structpb.NewStruct(metadata); err != nil {
return nil
}
return metadata

}

// toStructpbCompatibleValue converts a value to a type compatible with
// structpb.NewStruct. structpb supports: nil, bool, int/uint/float (as float64),
// string, []interface{}, and map[string]interface{}. For unsupported types,
// we fall back to fmt.Sprintf to produce a string representation.
func toStructpbCompatibleValue(v interface{}) interface{} {
switch val := v.(type) {
case nil, bool, float64, string:
return val
case int:
return float64(val)
case int32:
return float64(val)
case int64:
return float64(val)
case uint:
return float64(val)
case uint32:
return float64(val)
case uint64:
return float64(val)
case float32:
return float64(val)
case []interface{}:
result := make([]interface{}, len(val))
for i, item := range val {
result[i] = toStructpbCompatibleValue(item)
}
return result
case map[string]interface{}:
result := make(map[string]interface{})
for k, item := range val {
result[k] = toStructpbCompatibleValue(item)
}
return result
default:
return fmt.Sprintf("%v", val)
}
}

func getApp(ctx context.Context, client *okta.Client, appID string) (*okta.Application, *responseContext, error) {
app, resp, err := client.Application.GetApplication(ctx, appID, okta.NewApplication(), nil)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ type userFilterConfig struct {

type Config struct {
Domain string
ApiToken string //nolint:gosec // Not a credential
ApiToken string
OktaClientId string
OktaPrivateKey string
OktaPrivateKeyId string
Expand Down
Loading