Skip to content

Commit

Permalink
Refactor Okta IDP manager
Browse files Browse the repository at this point in the history
  • Loading branch information
bcmmbaga committed Sep 26, 2023
1 parent 7706319 commit 5adebea
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 192 deletions.
173 changes: 18 additions & 155 deletions management/server/idp/okta.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"strings"
"time"

"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/okta/okta-sdk-golang/v2/okta"
"github.com/okta/okta-sdk-golang/v2/okta/query"

"github.com/netbirdio/netbird/management/server/telemetry"
)

// OktaManager okta manager client instance.
Expand Down Expand Up @@ -76,11 +76,6 @@ func NewOktaManager(config OktaClientConfig, appMetrics telemetry.AppMetrics) (*
return nil, err
}

err = updateUserProfileSchema(client)
if err != nil {
return nil, err
}

credentials := &OktaCredentials{
clientConfig: config,
httpClient: httpClient,
Expand All @@ -103,49 +98,8 @@ func (oc *OktaCredentials) Authenticate() (JWTToken, error) {
}

// CreateUser creates a new user in okta Idp and sends an invitation.
func (om *OktaManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
var (
sendEmail = true
activate = true
userProfile = okta.UserProfile{
"email": email,
"login": email,
wtAccountID: accountID,
wtPendingInvite: true,
}
)

fields := strings.Fields(name)
if n := len(fields); n > 0 {
userProfile["firstName"] = strings.Join(fields[:n-1], " ")
userProfile["lastName"] = fields[n-1]
}

user, resp, err := om.client.User.CreateUser(context.Background(),
okta.CreateUserRequest{
Profile: &userProfile,
},
&query.Params{
Activate: &activate,
SendEmail: &sendEmail,
},
)
if err != nil {
return nil, err
}

if om.appMetrics != nil {
om.appMetrics.IDPMetrics().CountCreateUser()
}

if resp.StatusCode != http.StatusOK {
if om.appMetrics != nil {
om.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
}

return parseOktaUser(user)
func (om *OktaManager) CreateUser(_, _, _, _ string) (*UserData, error) {
return nil, fmt.Errorf("method CreateUser not implemented")
}

// GetUserDataByID requests user data from keycloak via ID.
Expand All @@ -166,7 +120,13 @@ func (om *OktaManager) GetUserDataByID(userID string, appMetadata AppMetadata) (
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
}

return parseOktaUser(user)
userData, err := parseOktaUser(user)
if err != nil {
return nil, err
}
userData.AppMetadata = appMetadata

return userData, nil
}

// GetUserByEmail searches users with a given email.
Expand Down Expand Up @@ -200,8 +160,7 @@ func (om *OktaManager) GetUserByEmail(email string) ([]*UserData, error) {

// GetAccount returns all the users for a given profile.
func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) {
search := fmt.Sprintf("profile.wt_account_id eq %q", accountID)
users, resp, err := om.client.User.ListUsers(context.Background(), &query.Params{Search: search})
users, resp, err := om.client.User.ListUsers(context.Background(), nil)
if err != nil {
return nil, err
}
Expand All @@ -223,6 +182,7 @@ func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) {
if err != nil {
return nil, err
}
userData.AppMetadata.WTAccountID = accountID

list = append(list, userData)
}
Expand Down Expand Up @@ -256,60 +216,15 @@ func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) {
return nil, err
}

accountID := userData.AppMetadata.WTAccountID
if accountID != "" {
if _, ok := indexedUsers[accountID]; !ok {
indexedUsers[accountID] = make([]*UserData, 0)
}
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
}
accountID := "unset"
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
}

return indexedUsers, nil
}

// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
func (om *OktaManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
user, resp, err := om.client.User.GetUser(context.Background(), userID)
if err != nil {
return err
}

if resp.StatusCode != http.StatusOK {
if om.appMetrics != nil {
om.appMetrics.IDPMetrics().CountRequestStatusError()
}
return fmt.Errorf("unable to update user, statusCode %d", resp.StatusCode)
}

profile := *user.Profile

if appMetadata.WTPendingInvite != nil {
profile[wtPendingInvite] = *appMetadata.WTPendingInvite
}

if appMetadata.WTAccountID != "" {
profile[wtAccountID] = appMetadata.WTAccountID
}

user.Profile = &profile
_, resp, err = om.client.User.UpdateUser(context.Background(), userID, *user, nil)
if err != nil {
fmt.Println(err.Error())
return err
}

if om.appMetrics != nil {
om.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
}

if resp.StatusCode != http.StatusOK {
if om.appMetrics != nil {
om.appMetrics.IDPMetrics().CountRequestStatusError()
}
return fmt.Errorf("unable to update user, statusCode %d", resp.StatusCode)
}

return nil
}

Expand Down Expand Up @@ -341,60 +256,12 @@ func (om *OktaManager) DeleteUser(userID string) error {
return nil
}

// updateUserProfileSchema updates the Okta user schema to include custom fields,
// wt_account_id and wt_pending_invite.
func updateUserProfileSchema(client *okta.Client) error {
// Ensure Okta doesn't enforce user input for these fields, as they are solely used by Netbird
userPermissions := []*okta.UserSchemaAttributePermission{{Action: "HIDE", Principal: "SELF"}}

_, resp, err := client.UserSchema.UpdateUserProfile(
context.Background(),
"default",
okta.UserSchema{
Definitions: &okta.UserSchemaDefinitions{
Custom: &okta.UserSchemaPublic{
Id: "#custom",
Type: "object",
Properties: map[string]*okta.UserSchemaAttribute{
wtAccountID: {
MaxLength: 100,
MinLength: 1,
Required: new(bool),
Scope: "NONE",
Title: "Wt Account Id",
Type: "string",
Permissions: userPermissions,
},
wtPendingInvite: {
Required: new(bool),
Scope: "NONE",
Title: "Wt Pending Invite",
Type: "boolean",
Permissions: userPermissions,
},
},
},
},
})
if err != nil {
return err
}

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unable to update user profile schema, statusCode %d", resp.StatusCode)
}

return nil
}

// parseOktaUserToUserData parse okta user to UserData.
func parseOktaUser(user *okta.User) (*UserData, error) {
var oktaUser struct {
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
AccountID string `json:"wt_account_id"`
PendingInvite bool `json:"wt_pending_invite"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}

if user == nil {
Expand All @@ -418,9 +285,5 @@ func parseOktaUser(user *okta.User) (*UserData, error) {
Email: oktaUser.Email,
Name: strings.Join([]string{oktaUser.FirstName, oktaUser.LastName}, " "),
ID: user.Id,
AppMetadata: AppMetadata{
WTAccountID: oktaUser.AccountID,
WTPendingInvite: &oktaUser.PendingInvite,
},
}, nil
}
44 changes: 7 additions & 37 deletions management/server/idp/okta_test.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
package idp

import (
"testing"

"github.com/okta/okta-sdk-golang/v2/okta"
"github.com/stretchr/testify/assert"
"testing"
)

func TestParseOktaUser(t *testing.T) {
type parseOktaUserTest struct {
name string
invite bool
inputProfile *okta.User
expectedUserData *UserData
assertErrFunc assert.ErrorAssertionFunc
}

parseOktaTestCase1 := parseOktaUserTest{
name: "Good Request",
invite: true,
name: "Good Request",
inputProfile: &okta.User{
Id: "123",
Profile: &okta.UserProfile{
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"wt_account_id": "456",
"wt_pending_invite": true,
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
},
},
expectedUserData: &UserData{
Expand All @@ -41,36 +38,17 @@ func TestParseOktaUser(t *testing.T) {

parseOktaTestCase2 := parseOktaUserTest{
name: "Invalid okta user",
invite: true,
inputProfile: nil,
expectedUserData: nil,
assertErrFunc: assert.Error,
}

parseOktaTestCase3 := parseOktaUserTest{
name: "Invalid pending invite type",
invite: false,
inputProfile: &okta.User{
Id: "123",
Profile: &okta.UserProfile{
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"wt_account_id": "456",
"wt_pending_invite": "true",
},
},
expectedUserData: nil,
assertErrFunc: assert.Error,
}

for _, testCase := range []parseOktaUserTest{parseOktaTestCase1, parseOktaTestCase2, parseOktaTestCase3} {
for _, testCase := range []parseOktaUserTest{parseOktaTestCase1, parseOktaTestCase2} {
t.Run(testCase.name, func(t *testing.T) {
userData, err := parseOktaUser(testCase.inputProfile)
testCase.assertErrFunc(t, err, testCase.assertErrFunc)

if err == nil {
testCase.expectedUserData.AppMetadata.WTPendingInvite = &testCase.invite
assert.True(t, userDataEqual(testCase.expectedUserData, userData), "user data should match")
}
})
Expand All @@ -83,13 +61,5 @@ func userDataEqual(a, b *UserData) bool {
if a.Email != b.Email || a.Name != b.Name || a.ID != b.ID {
return false
}
if a.AppMetadata.WTAccountID != b.AppMetadata.WTAccountID {
return false
}

if a.AppMetadata.WTPendingInvite != nil && b.AppMetadata.WTPendingInvite != nil &&
*a.AppMetadata.WTPendingInvite != *b.AppMetadata.WTPendingInvite {
return false
}
return true
}

0 comments on commit 5adebea

Please sign in to comment.