mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-01 12:33:53 +01:00
Add Okta IdP (#859)
This commit is contained in:
parent
49c71b9b9d
commit
2eb9a97fee
3
go.mod
3
go.mod
@ -47,6 +47,7 @@ require (
|
|||||||
github.com/mdlayher/socket v0.4.0
|
github.com/mdlayher/socket v0.4.0
|
||||||
github.com/miekg/dns v1.1.43
|
github.com/miekg/dns v1.1.43
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||||
|
github.com/okta/okta-sdk-golang/v2 v2.18.0
|
||||||
github.com/open-policy-agent/opa v0.49.0
|
github.com/open-policy-agent/opa v0.49.0
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pion/logging v0.2.2
|
github.com/pion/logging v0.2.2
|
||||||
@ -99,6 +100,7 @@ require (
|
|||||||
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/josharian/native v1.0.0 // indirect
|
github.com/josharian/native v1.0.0 // indirect
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mdlayher/genetlink v1.1.0 // indirect
|
github.com/mdlayher/genetlink v1.1.0 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.1 // indirect
|
github.com/mdlayher/netlink v1.7.1 // indirect
|
||||||
@ -135,6 +137,7 @@ require (
|
|||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
|
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
|
||||||
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
6
go.sum
6
go.sum
@ -406,6 +406,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
|
|||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
@ -496,6 +498,8 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
|
|||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/okta/okta-sdk-golang/v2 v2.18.0 h1:cfDasMb7CShbZvOrF6n+DnLevWwiHgedWMGJ8M8xKDc=
|
||||||
|
github.com/okta/okta-sdk-golang/v2 v2.18.0/go.mod h1:dz30v3ctAiMb7jpsCngGfQUAEGm1/NsWT92uTbNDQIs=
|
||||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@ -1157,6 +1161,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
|||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||||
|
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
|
||||||
|
@ -41,6 +41,7 @@ type Config struct {
|
|||||||
KeycloakClientCredentials KeycloakClientConfig
|
KeycloakClientCredentials KeycloakClientConfig
|
||||||
ZitadelClientCredentials ZitadelClientConfig
|
ZitadelClientCredentials ZitadelClientConfig
|
||||||
AuthentikClientCredentials AuthentikClientConfig
|
AuthentikClientCredentials AuthentikClientConfig
|
||||||
|
OktaClientCredentials OktaClientConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManagerCredentials interface that authenticates using the credential of each type of idp
|
// ManagerCredentials interface that authenticates using the credential of each type of idp
|
||||||
@ -141,7 +142,6 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NewZitadelManager(zitadelClientConfig, appMetrics)
|
return NewZitadelManager(zitadelClientConfig, appMetrics)
|
||||||
|
|
||||||
case "authentik":
|
case "authentik":
|
||||||
authentikConfig := config.AuthentikClientCredentials
|
authentikConfig := config.AuthentikClientCredentials
|
||||||
if config.ClientConfig != nil {
|
if config.ClientConfig != nil {
|
||||||
@ -156,6 +156,19 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NewAuthentikManager(authentikConfig, appMetrics)
|
return NewAuthentikManager(authentikConfig, appMetrics)
|
||||||
|
case "okta":
|
||||||
|
oktaClientConfig := config.OktaClientCredentials
|
||||||
|
if config.ClientConfig != nil {
|
||||||
|
oktaClientConfig = OktaClientConfig{
|
||||||
|
Issuer: config.ClientConfig.Issuer,
|
||||||
|
TokenEndpoint: config.ClientConfig.TokenEndpoint,
|
||||||
|
GrantType: config.ClientConfig.GrantType,
|
||||||
|
APIToken: config.ExtraConfig["APIToken"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewOktaManager(oktaClientConfig, appMetrics)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
|
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
|
||||||
}
|
}
|
||||||
|
383
management/server/idp/okta.go
Normal file
383
management/server/idp/okta.go
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
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 string, name string, accountID 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 {
|
||||||
|
var pendingInvite bool
|
||||||
|
if appMetadata.WTPendingInvite != nil {
|
||||||
|
pendingInvite = *appMetadata.WTPendingInvite
|
||||||
|
}
|
||||||
|
|
||||||
|
_, resp, err := om.client.User.UpdateUser(context.Background(), userID,
|
||||||
|
okta.User{
|
||||||
|
Profile: &okta.UserProfile{
|
||||||
|
wtAccountID: appMetadata.WTAccountID,
|
||||||
|
wtPendingInvite: pendingInvite,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateUserProfileSchema updates the Okta user schema to include custom fields,
|
||||||
|
// wt_account_id and wt_pending_invite.
|
||||||
|
func updateUserProfileSchema(client *okta.Client) error {
|
||||||
|
required := true
|
||||||
|
_, 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: &required,
|
||||||
|
Scope: "NONE",
|
||||||
|
Title: "Wt Account Id",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
wtPendingInvite: {
|
||||||
|
Required: new(bool),
|
||||||
|
Scope: "NONE",
|
||||||
|
Title: "Wt Pending Invite",
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
95
management/server/idp/okta_test.go
Normal file
95
management/server/idp/okta_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package idp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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,
|
||||||
|
inputProfile: &okta.User{
|
||||||
|
Id: "123",
|
||||||
|
Profile: &okta.UserProfile{
|
||||||
|
"email": "test@example.com",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"wt_account_id": "456",
|
||||||
|
"wt_pending_invite": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUserData: &UserData{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Name: "John Doe",
|
||||||
|
ID: "123",
|
||||||
|
AppMetadata: AppMetadata{
|
||||||
|
WTAccountID: "456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assertErrFunc: assert.NoError,
|
||||||
|
}
|
||||||
|
|
||||||
|
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": "test@example.com",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"wt_account_id": "456",
|
||||||
|
"wt_pending_invite": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUserData: nil,
|
||||||
|
assertErrFunc: assert.Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range []parseOktaUserTest{parseOktaTestCase1, parseOktaTestCase2, parseOktaTestCase3} {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// userDataEqual helper function to compare UserData structs for equality.
|
||||||
|
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
|
||||||
|
}
|
@ -3,6 +3,7 @@ package idp
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,3 +58,14 @@ func GeneratePassword(passwordLength, minSpecialChar, minNum, minUpperCase int)
|
|||||||
})
|
})
|
||||||
return string(inRune)
|
return string(inRune)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// baseURL returns the base url by concatenating
|
||||||
|
// the scheme and host components of the parsed URL.
|
||||||
|
func baseURL(rawURL string) string {
|
||||||
|
parsedURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedURL.Scheme + "://" + parsedURL.Host
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user