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

Optimize Cache and IDP Management #1147

Merged
merged 22 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50ecf6f
wip: Handle user metadata without transmitting them to Identity Provider
bcmmbaga Sep 14, 2023
8293c95
wip: load user account into cache for idp with no GetAccount support
bcmmbaga Sep 14, 2023
9027271
Merge branch 'main' into idp-user-cache
bcmmbaga Sep 22, 2023
519f18b
Add compatibility for the IDP lacking AppMetadata update capabilities
bcmmbaga Sep 22, 2023
a3f6de0
Refactor Authentik IdP manager
bcmmbaga Sep 22, 2023
7aa72f6
cleanup
bcmmbaga Sep 22, 2023
1c7d4e9
Merge branch 'main' into idp-user-cache
bcmmbaga Sep 25, 2023
62d5853
Refactor Zitadel IDP manager
bcmmbaga Sep 25, 2023
7706319
Refactor Keycloak IDP manager
bcmmbaga Sep 25, 2023
5adebea
Refactor Okta IDP manager
bcmmbaga Sep 26, 2023
c10ebdb
Refactor Azure IDP manager
bcmmbaga Sep 26, 2023
de1fa8b
Remove unused types declarations
bcmmbaga Sep 26, 2023
1c4e6b2
Refactor Google Workspace IdP manager
bcmmbaga Sep 26, 2023
baf2546
Merge branch 'main' into idp-user-cache
bcmmbaga Sep 26, 2023
b155ee2
Remove unused function and variables
bcmmbaga Sep 26, 2023
cebf9d9
Initialize a user data slice with defined capacity
bcmmbaga Sep 27, 2023
7c5c9a2
Initialize a user data slice with defined capacity
bcmmbaga Sep 27, 2023
1f8407a
Merge remote-tracking branch 'origin/idp-user-cache' into idp-user-cache
bcmmbaga Sep 27, 2023
e04fb2e
Initialize a user data slice with defined capacity
bcmmbaga Sep 27, 2023
cc215ac
Remove unused custom error and refactor unset account id into constant
bcmmbaga Oct 1, 2023
df0f12e
Change Google Workspace API permissions to read-only
bcmmbaga Oct 2, 2023
4638f80
use idp.UnsetAccountID
mlsmaycon Oct 3, 2023
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
21 changes: 21 additions & 0 deletions management/server/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,27 @@ func (am *DefaultAccountManager) warmupIDPCache() error {
return err
}

// If the Identity Provider does not support writing AppMetadata,
// in cases like this, we expect it to return all users in an "unset" field.
// We iterate over the users in the "unset" field, look up their AccountID in our store, and
// update their AppMetadata with the AccountID.
if unsetData, ok := userData[idp.UnsetAccountID]; ok {
for _, user := range unsetData {
accountID, err := am.Store.GetAccountByUser(user.ID)
if err == nil {
data := userData[accountID.Id]
if data == nil {
data = make([]*idp.UserData, 0, 1)
}

user.AppMetadata.WTAccountID = accountID.Id

userData[accountID.Id] = append(data, user)
bcmmbaga marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
delete(userData, idp.UnsetAccountID)

for accountID, users := range userData {
err = am.cacheManager.Set(am.ctx, accountID, users, cacheStore.WithExpiration(cacheEntryExpiration()))
if err != nil {
Expand Down
178 changes: 16 additions & 162 deletions management/server/idp/authentik.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,47 +210,7 @@ func (ac *AuthentikCredentials) Authenticate() (JWTToken, error) {
}

// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
func (am *AuthentikManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
ctx, err := am.authenticationContext()
if err != nil {
return err
}

userPk, err := strconv.ParseInt(userID, 10, 32)
if err != nil {
return err
}

var pendingInvite bool
if appMetadata.WTPendingInvite != nil {
pendingInvite = *appMetadata.WTPendingInvite
}

patchedUserReq := api.PatchedUserRequest{
Attributes: map[string]interface{}{
wtAccountID: appMetadata.WTAccountID,
wtPendingInvite: pendingInvite,
},
}
_, resp, err := am.apiClient.CoreApi.CoreUsersPartialUpdate(ctx, int32(userPk)).
PatchedUserRequest(patchedUserReq).
Execute()
if err != nil {
return err
}
defer resp.Body.Close()

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

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

func (am *AuthentikManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
return nil
}

Expand Down Expand Up @@ -283,7 +243,10 @@ func (am *AuthentikManager) GetUserDataByID(userID string, appMetadata AppMetada
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
}

return parseAuthentikUser(*user)
userData := parseAuthentikUser(*user)
userData.AppMetadata = appMetadata

return userData, nil
}

// GetAccount returns all the users for a given profile.
Expand All @@ -293,8 +256,7 @@ func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) {
return nil, err
}

accountFilter := fmt.Sprintf("{%q:%q}", wtAccountID, accountID)
userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Attributes(accountFilter).Execute()
userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Execute()
if err != nil {
return nil, err
}
Expand All @@ -313,10 +275,9 @@ func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) {

users := make([]*UserData, 0)
for _, user := range userList.Results {
userData, err := parseAuthentikUser(user)
if err != nil {
return nil, err
}
userData := parseAuthentikUser(user)
userData.AppMetadata.WTAccountID = accountID

users = append(users, userData)
}

Expand Down Expand Up @@ -350,65 +311,16 @@ func (am *AuthentikManager) GetAllAccounts() (map[string][]*UserData, error) {

indexedUsers := make(map[string][]*UserData)
for _, user := range userList.Results {
userData, err := parseAuthentikUser(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)
}
userData := parseAuthentikUser(user)
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
}

return indexedUsers, nil
}

// CreateUser creates a new user in authentik Idp and sends an invitation.
func (am *AuthentikManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
ctx, err := am.authenticationContext()
if err != nil {
return nil, err
}

groupID, err := am.getUserGroupByName("netbird")
if err != nil {
return nil, err
}

defaultBoolValue := true
createUserRequest := api.UserRequest{
Email: &email,
Name: name,
IsActive: &defaultBoolValue,
Groups: []string{groupID},
Username: email,
Attributes: map[string]interface{}{
wtAccountID: accountID,
wtPendingInvite: &defaultBoolValue,
},
}
user, resp, err := am.apiClient.CoreApi.CoreUsersCreate(ctx).UserRequest(createUserRequest).Execute()
if err != nil {
return nil, err
}
defer resp.Body.Close()

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

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

return parseAuthentikUser(*user)
func (am *AuthentikManager) CreateUser(_, _, _, _ string) (*UserData, error) {
return nil, fmt.Errorf("method CreateUser not implemented")
}

// GetUserByEmail searches users with a given email.
Expand Down Expand Up @@ -438,11 +350,7 @@ func (am *AuthentikManager) GetUserByEmail(email string) ([]*UserData, error) {

users := make([]*UserData, 0)
for _, user := range userList.Results {
userData, err := parseAuthentikUser(user)
if err != nil {
return nil, err
}
users = append(users, userData)
users = append(users, parseAuthentikUser(user))
}

return users, nil
Expand Down Expand Up @@ -501,64 +409,10 @@ func (am *AuthentikManager) authenticationContext() (context.Context, error) {
return context.WithValue(context.Background(), api.ContextAPIKeys, value), nil
}

// getUserGroupByName retrieves the user group for assigning new users.
// If the group is not found, a new group with the specified name will be created.
func (am *AuthentikManager) getUserGroupByName(name string) (string, error) {
ctx, err := am.authenticationContext()
if err != nil {
return "", err
}

groupList, resp, err := am.apiClient.CoreApi.CoreGroupsList(ctx).Name(name).Execute()
if err != nil {
return "", err
}
defer resp.Body.Close()

if groupList != nil {
if len(groupList.Results) > 0 {
return groupList.Results[0].Pk, nil
}
}

createGroupRequest := api.GroupRequest{Name: name}
group, resp, err := am.apiClient.CoreApi.CoreGroupsCreate(ctx).GroupRequest(createGroupRequest).Execute()
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("unable to create user group, statusCode: %d", resp.StatusCode)
}

return group.Pk, nil
}

func parseAuthentikUser(user api.User) (*UserData, error) {
var attributes struct {
AccountID string `json:"wt_account_id"`
PendingInvite bool `json:"wt_pending_invite"`
}

helper := JsonParser{}
buf, err := helper.Marshal(user.Attributes)
if err != nil {
return nil, err
}

err = helper.Unmarshal(buf, &attributes)
if err != nil {
return nil, err
}

func parseAuthentikUser(user api.User) *UserData {
return &UserData{
Email: *user.Email,
Name: user.Name,
ID: strconv.FormatInt(int64(user.Pk), 10),
AppMetadata: AppMetadata{
WTAccountID: attributes.AccountID,
WTPendingInvite: &attributes.PendingInvite,
},
}, nil
}
}
Loading
Loading