mirror of
https://github.com/netbirdio/netbird.git
synced 2025-06-03 08:35:43 +02:00
Implement user deletion across all IDP-ss. Expires all user peers when the user is deleted. Users are permanently removed from a local store, but in IDP, we remove Netbird attributes for the user untilUserDeleteFromIDPEnabled setting is not enabled. To test, an admin user should remove any additional users. Until the UI incorporates this feature, use a curl DELETE request targeting the /users/<USER_ID> management endpoint. Note that this request only removes user attributes and doesn't trigger a delete from the IDP. To enable user removal from the IdP, set UserDeleteFromIDPEnabled to true in account settings. Until we have a UI for this, make this change directly in the store file. Store the deleted email addresses in encrypted in activity store.
427 lines
11 KiB
Go
427 lines
11 KiB
Go
package idp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"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"
|
|
)
|
|
|
|
// OktaManager okta manager client instance.
|
|
type OktaManager struct {
|
|
client *okta.Client
|
|
httpClient ManagerHTTPClient
|
|
credentials ManagerCredentials
|
|
helper ManagerHelper
|
|
appMetrics telemetry.AppMetrics
|
|
}
|
|
|
|
// OktaClientConfig okta manager client configurations.
|
|
type OktaClientConfig struct {
|
|
APIToken string
|
|
Issuer string
|
|
TokenEndpoint string
|
|
GrantType string
|
|
}
|
|
|
|
// OktaCredentials okta authentication information.
|
|
type OktaCredentials struct {
|
|
clientConfig OktaClientConfig
|
|
helper ManagerHelper
|
|
httpClient ManagerHTTPClient
|
|
appMetrics telemetry.AppMetrics
|
|
}
|
|
|
|
// NewOktaManager creates a new instance of the OktaManager.
|
|
func NewOktaManager(config OktaClientConfig, appMetrics telemetry.AppMetrics) (*OktaManager, error) {
|
|
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
httpTransport.MaxIdleConns = 5
|
|
|
|
httpClient := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Transport: httpTransport,
|
|
}
|
|
|
|
helper := JsonParser{}
|
|
config.Issuer = baseURL(config.Issuer)
|
|
|
|
if config.APIToken == "" {
|
|
return nil, fmt.Errorf("okta IdP configuration is incomplete, APIToken is missing")
|
|
}
|
|
|
|
if config.Issuer == "" {
|
|
return nil, fmt.Errorf("okta IdP configuration is incomplete, Issuer is missing")
|
|
}
|
|
|
|
if config.TokenEndpoint == "" {
|
|
return nil, fmt.Errorf("okta IdP configuration is incomplete, TokenEndpoint is missing")
|
|
}
|
|
|
|
if config.GrantType == "" {
|
|
return nil, fmt.Errorf("okta IdP configuration is incomplete, GrantType is missing")
|
|
}
|
|
|
|
_, client, err := okta.NewClient(context.Background(),
|
|
okta.WithOrgUrl(config.Issuer),
|
|
okta.WithToken(config.APIToken),
|
|
okta.WithHttpClientPtr(httpClient),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = updateUserProfileSchema(client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
credentials := &OktaCredentials{
|
|
clientConfig: config,
|
|
httpClient: httpClient,
|
|
helper: helper,
|
|
appMetrics: appMetrics,
|
|
}
|
|
|
|
return &OktaManager{
|
|
client: client,
|
|
httpClient: httpClient,
|
|
credentials: credentials,
|
|
helper: helper,
|
|
appMetrics: appMetrics,
|
|
}, nil
|
|
}
|
|
|
|
// Authenticate retrieves access token to use the okta user API.
|
|
func (oc *OktaCredentials) Authenticate() (JWTToken, error) {
|
|
return JWTToken{}, nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// GetUserDataByID requests user data from keycloak via ID.
|
|
func (om *OktaManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
|
user, resp, err := om.client.User.GetUser(context.Background(), userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountGetUserDataByID()
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
}
|
|
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
|
}
|
|
|
|
return parseOktaUser(user)
|
|
}
|
|
|
|
// GetUserByEmail searches users with a given email.
|
|
// If no users have been found, this function returns an empty list.
|
|
func (om *OktaManager) GetUserByEmail(email string) ([]*UserData, error) {
|
|
user, resp, err := om.client.User.GetUser(context.Background(), url.QueryEscape(email))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountGetUserByEmail()
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
}
|
|
return nil, fmt.Errorf("unable to get user %s, statusCode %d", email, resp.StatusCode)
|
|
}
|
|
|
|
userData, err := parseOktaUser(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
users := make([]*UserData, 0)
|
|
users = append(users, userData)
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// 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})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountGetAccount()
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
}
|
|
return nil, fmt.Errorf("unable to get account, statusCode %d", resp.StatusCode)
|
|
}
|
|
|
|
list := make([]*UserData, 0)
|
|
for _, user := range users {
|
|
userData, err := parseOktaUser(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list = append(list, userData)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
|
// It returns a list of users indexed by accountID.
|
|
func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) {
|
|
users, resp, err := om.client.User.ListUsers(context.Background(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountGetAllAccounts()
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
}
|
|
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
|
}
|
|
|
|
indexedUsers := make(map[string][]*UserData)
|
|
for _, user := range users {
|
|
userData, err := parseOktaUser(user)
|
|
if err != nil {
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// InviteUserByID resend invitations to users who haven't activated,
|
|
// their accounts prior to the expiration period.
|
|
func (om *OktaManager) InviteUserByID(_ string) error {
|
|
return fmt.Errorf("method InviteUserByID not implemented")
|
|
}
|
|
|
|
// DeleteUser from Okta
|
|
func (om *OktaManager) DeleteUser(userID string) error {
|
|
resp, err := om.client.User.DeactivateOrDeleteUser(context.Background(), userID, nil)
|
|
if err != nil {
|
|
fmt.Println(err.Error())
|
|
return err
|
|
}
|
|
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountDeleteUser()
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
if om.appMetrics != nil {
|
|
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
}
|
|
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
if user == nil {
|
|
return nil, fmt.Errorf("invalid okta user")
|
|
}
|
|
|
|
if user.Profile != nil {
|
|
helper := JsonParser{}
|
|
buf, err := helper.Marshal(*user.Profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = helper.Unmarshal(buf, &oktaUser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &UserData{
|
|
Email: oktaUser.Email,
|
|
Name: strings.Join([]string{oktaUser.FirstName, oktaUser.LastName}, " "),
|
|
ID: user.Id,
|
|
AppMetadata: AppMetadata{
|
|
WTAccountID: oktaUser.AccountID,
|
|
WTPendingInvite: &oktaUser.PendingInvite,
|
|
},
|
|
}, nil
|
|
}
|