pkg/client/keycloak/adapter/gocloak_adapter_user.go (322 lines of code) (raw):
package adapter
import (
"context"
"fmt"
"slices"
"github.com/Nerzal/gocloak/v12"
"github.com/pkg/errors"
keycloak_go_client "github.com/zmotso/keycloak-go-client"
)
type KeycloakUser struct {
Username string
Enabled bool
EmailVerified bool
Email string
FirstName string
LastName string
RequiredUserActions []string
Roles []string
Groups []string
Attributes map[string]string
Password string
}
type UserRealmRoleMapping struct {
ID string `json:"id"`
Name string `json:"name"`
}
type UserGroupMapping struct {
ID string `json:"id"`
Name string `json:"name"`
}
func (a GoCloakAdapter) SyncRealmUser(ctx context.Context, realmName string, userDto *KeycloakUser, addOnly bool) error {
userID, err := a.createOrUpdateUser(ctx, realmName, userDto, addOnly)
if err != nil {
return err
}
if userDto.Password != "" {
if err = a.setUserPassword(realmName, userID, userDto.Password); err != nil {
return err
}
}
if err = a.syncUserRoles(ctx, realmName, userID, userDto.Roles, addOnly); err != nil {
return err
}
if err = a.syncUserGroups(ctx, realmName, userID, userDto.Groups, addOnly); err != nil {
return err
}
return nil
}
func (a GoCloakAdapter) createOrUpdateUser(ctx context.Context, realmName string, userDto *KeycloakUser, addOnly bool) (string, error) {
user, err := a.GetUserByName(ctx, realmName, userDto.Username)
if err != nil {
if !IsErrNotFound(err) {
return "", fmt.Errorf("unable to get user: %w", err)
}
kcUser := gocloak.User{
Username: &userDto.Username,
Enabled: &userDto.Enabled,
EmailVerified: &userDto.EmailVerified,
FirstName: &userDto.FirstName,
LastName: &userDto.LastName,
RequiredActions: &userDto.RequiredUserActions,
Email: &userDto.Email,
}
if len(userDto.Attributes) > 0 {
kcUser.Attributes = a.makeUserAttributes(&kcUser, userDto, addOnly)
}
var userID string
userID, err = a.client.CreateUser(ctx, a.token.AccessToken, realmName, kcUser)
if err != nil {
return "", fmt.Errorf("unable to create user: %w", err)
}
return userID, nil
}
user.Username = &userDto.Username
user.Enabled = &userDto.Enabled
user.EmailVerified = &userDto.EmailVerified
user.FirstName = &userDto.FirstName
user.LastName = &userDto.LastName
user.RequiredActions = &userDto.RequiredUserActions
user.Email = &userDto.Email
if len(userDto.Attributes) > 0 {
user.Attributes = a.makeUserAttributes(user, userDto, addOnly)
}
if err = a.client.UpdateUser(ctx, a.token.AccessToken, realmName, *user); err != nil {
return "", fmt.Errorf("unable to update user: %w", err)
}
return *user.ID, nil
}
func (a GoCloakAdapter) GetUserByName(ctx context.Context, realmName, username string) (*gocloak.User, error) {
params := gocloak.GetUsersParams{
Username: &username,
Exact: gocloak.BoolP(true),
}
users, err := a.client.GetUsers(ctx, a.token.AccessToken, realmName, params)
if err != nil {
return nil, fmt.Errorf("unable to get users: %w", err)
}
for _, user := range users {
if user.Username != nil && *user.Username == username {
return user, nil
}
}
return nil, NotFoundError("user not found")
}
func (a GoCloakAdapter) syncUserGroups(ctx context.Context, realmName string, userID string, groups []string, addOnly bool) error {
userGroups, err := a.GetUserGroupMappings(ctx, realmName, userID)
if err != nil {
return err
}
groupsToAdd := make([]string, 0, len(groups))
for _, gn := range groups {
if !slices.ContainsFunc(userGroups, func(mapping UserGroupMapping) bool {
return mapping.Name == gn
}) {
groupsToAdd = append(groupsToAdd, gn)
}
}
if len(groupsToAdd) > 0 {
var kcGroups map[string]gocloak.Group
kcGroups, err = a.getGroupsByNames(
ctx,
realmName,
groupsToAdd,
)
if err != nil {
return fmt.Errorf("unable to get groups: %w", err)
}
for _, gr := range kcGroups {
if err = a.AddUserToGroup(ctx, realmName, userID, *gr.ID); err != nil {
return fmt.Errorf("failed to add user to group %v: %w", gr.Name, err)
}
}
}
if !addOnly {
for _, gr := range userGroups {
if !slices.Contains(groups, gr.Name) {
if err = a.RemoveUserFromGroup(ctx, realmName, userID, gr.ID); err != nil {
return fmt.Errorf("unable to remove user from group: %w", err)
}
}
}
}
return nil
}
func (a GoCloakAdapter) syncUserRoles(ctx context.Context, realmName string, userID string, roles []string, addOnly bool) error {
if !addOnly {
if err := a.clearUserRealmRoles(ctx, realmName, userID); err != nil {
return errors.Wrap(err, "unable to clear realm roles")
}
}
realmRoles, err := a.client.GetRealmRoles(ctx, a.token.AccessToken, realmName, gocloak.GetRoleParams{
Max: gocloak.IntP(100),
BriefRepresentation: gocloak.BoolP(true),
})
if err != nil {
return fmt.Errorf("unable to get realm roles: %w", err)
}
realmRolesDict := make(map[string]gocloak.Role, len(realmRoles))
for _, role := range realmRoles {
realmRolesDict[*role.Name] = *role
}
kcRoles := make([]gocloak.Role, 0, len(roles))
for _, roleName := range roles {
role, ok := realmRolesDict[roleName]
if !ok {
return errors.Errorf("realm role %s not found", roleName)
}
kcRoles = append(kcRoles, role)
}
if err = a.client.AddRealmRoleToUser(ctx, a.token.AccessToken, realmName, userID, kcRoles); err != nil {
return fmt.Errorf("unable to add realm roles to user: %w", err)
}
return nil
}
func (a GoCloakAdapter) GetUserRealmRoleMappings(ctx context.Context, realmName string, userID string) ([]UserRealmRoleMapping, error) {
var roles []UserRealmRoleMapping
rsp, err := a.startRestyRequest().
SetContext(ctx).
SetPathParams(map[string]string{
keycloakApiParamRealm: realmName,
keycloakApiParamId: userID,
}).
SetResult(&roles).
Get(a.buildPath(getUserRealmRoleMappings))
if err = a.checkError(err, rsp); err != nil {
return nil, errors.Wrap(err, "unable to get realm role mappings")
}
return roles, nil
}
func (a GoCloakAdapter) GetUserGroupMappings(ctx context.Context, realmName string, userID string) ([]UserGroupMapping, error) {
var groups []UserGroupMapping
rsp, err := a.startRestyRequest().
SetContext(ctx).
SetPathParams(map[string]string{
keycloakApiParamRealm: realmName,
keycloakApiParamId: userID,
}).
SetResult(&groups).
Get(a.buildPath(getUserGroupMappings))
if err = a.checkError(err, rsp); err != nil {
return nil, errors.Wrap(err, "unable to get group mappings")
}
return groups, nil
}
func (a GoCloakAdapter) RemoveUserFromGroup(ctx context.Context, realmName, userID, groupID string) error {
rsp, err := a.startRestyRequest().
SetContext(ctx).
SetPathParams(map[string]string{
keycloakApiParamRealm: realmName,
"userID": userID,
"groupID": groupID,
}).
Delete(a.buildPath(manageUserGroups))
if err = a.checkError(err, rsp); err != nil {
return errors.Wrap(err, "unable to remove user from group")
}
return nil
}
func (a GoCloakAdapter) AddUserToGroup(ctx context.Context, realmName, userID, groupID string) error {
rsp, err := a.startRestyRequest().
SetContext(ctx).
SetPathParams(map[string]string{
keycloakApiParamRealm: realmName,
"userID": userID,
"groupID": groupID,
}).
SetBody(map[string]string{
"groupId": groupID,
keycloakApiParamRealm: realmName,
"userId": userID,
}).
Put(a.buildPath(manageUserGroups))
if err = a.checkError(err, rsp); err != nil {
return errors.Wrap(err, "unable to add user to group")
}
return nil
}
func (a GoCloakAdapter) UpdateUsersProfile(
ctx context.Context,
realm string,
userProfile keycloak_go_client.UserProfileConfig,
) (*keycloak_go_client.UserProfileConfig, error) {
cl, err := keycloak_go_client.NewClient(a.basePath, keycloak_go_client.WithToken(a.token.AccessToken))
if err != nil {
return nil, fmt.Errorf("failed to create keycloak_go_client client: %w", err)
}
profile, res, err := cl.Users.UpdateUsersProfile(ctx, realm, userProfile)
if err = checkHttpResp(res, err); err != nil {
return nil, err
}
return profile, nil
}
func (a GoCloakAdapter) GetUsersProfile(
ctx context.Context,
realm string,
) (*keycloak_go_client.UserProfileConfig, error) {
cl, err := keycloak_go_client.NewClient(a.basePath, keycloak_go_client.WithToken(a.token.AccessToken))
if err != nil {
return nil, fmt.Errorf("failed to create keycloak_go_client client: %w", err)
}
profile, res, err := cl.Users.GetUsersProfile(ctx, realm)
if err = checkHttpResp(res, err); err != nil {
return nil, err
}
return profile, nil
}
func checkHttpResp(res *keycloak_go_client.Response, err error) error {
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
if res == nil || res.HTTPResponse == nil {
return errors.New("empty response")
}
const maxStatusCodesSuccess = 399
if res.HTTPResponse.StatusCode > maxStatusCodesSuccess {
return errors.Errorf("status: %s, body: %s", res.HTTPResponse.Status, res.Body)
}
return nil
}
func (a GoCloakAdapter) clearUserRealmRoles(ctx context.Context, realmName string, userID string) error {
roles, err := a.GetUserRealmRoleMappings(ctx, realmName, userID)
if err != nil {
return errors.Wrap(err, "unable to get user realm role map")
}
goRoles := make([]gocloak.Role, 0, len(roles))
for i := range roles {
goRoles = append(goRoles, gocloak.Role{ID: &roles[i].ID, Name: &roles[i].Name})
}
if err := a.client.DeleteRealmRoleFromUser(ctx, a.token.AccessToken, realmName, userID, goRoles); err != nil {
return errors.Wrap(err, "unable to delete realm role from user")
}
return nil
}
func (a GoCloakAdapter) setUserPassword(realmName, userID, password string) error {
rsp, err := a.startRestyRequest().
SetPathParams(map[string]string{
keycloakApiParamRealm: realmName,
keycloakApiParamId: userID,
}).
SetBody(map[string]interface{}{
"temporary": false,
"type": "password",
"value": password,
}).
Put(a.buildPath(setRealmUserPassword))
if err = a.checkError(err, rsp); err != nil {
return errors.Wrap(err, "unable to set user password")
}
return nil
}
func (a GoCloakAdapter) makeUserAttributes(keycloakUser *gocloak.User, userCR *KeycloakUser, addOnly bool) *map[string][]string {
attrs := make(map[string][]string)
for k, v := range userCR.Attributes {
attrs[k] = []string{v}
}
if addOnly && keycloakUser.Attributes != nil && len(*keycloakUser.Attributes) > 0 {
for k, v := range *keycloakUser.Attributes {
attrs[k] = v
}
}
return &attrs
}