pkg/client/keycloak/adapter/gocloak_adapter_groups.go (242 lines of code) (raw):

package adapter import ( "context" "fmt" "net/http" "strings" "sync" "github.com/Nerzal/gocloak/v12" "github.com/pkg/errors" "golang.org/x/sync/errgroup" keycloakApi "github.com/epam/edp-keycloak-operator/api/v1" ) type NotFoundError string func (e NotFoundError) Error() string { return string(e) } func IsErrNotFound(err error) bool { errNotFound := NotFoundError("") if errors.As(err, &errNotFound) { return true } apiErr := gocloak.APIError{} if errors.As(err, &apiErr) { return apiErr.Code == http.StatusNotFound } apiErrp := &gocloak.APIError{} if errors.As(err, &apiErrp) { return apiErrp.Code == http.StatusNotFound } return false } // GetGroups return top-level groups for a realm. func (a GoCloakAdapter) GetGroups(ctx context.Context, realm string) (map[string]*gocloak.Group, error) { groups, err := a.client.GetGroups( ctx, a.token.AccessToken, realm, gocloak.GetGroupsParams{ Max: gocloak.IntP(100), }, ) if err != nil { return nil, fmt.Errorf("failed to get groups: %w", err) } groupMap := make(map[string]*gocloak.Group, len(groups)) for _, g := range groups { if g != nil && g.Name != nil { groupMap[*g.Name] = g } } return groupMap, nil } func (a GoCloakAdapter) getGroup(ctx context.Context, realm, groupName string) (*gocloak.Group, error) { groups, err := a.client.GetGroups(ctx, a.token.AccessToken, realm, gocloak.GetGroupsParams{ Search: gocloak.StringP(groupName), }) if err != nil { return nil, errors.Wrap(err, "unable to search groups") } gr := make([]gocloak.Group, len(groups)) for i, g := range groups { if g != nil { gr[i] = *g } } group := getGroupByName(gr, groupName) if group != nil { return group, nil } return nil, NotFoundError("group not found") } func (a GoCloakAdapter) getGroupsByNames(ctx context.Context, realm string, groupNames []string) (map[string]gocloak.Group, error) { groups := make(map[string]gocloak.Group, len(groupNames)) eg := errgroup.Group{} m := sync.Mutex{} for _, groupName := range groupNames { eg.Go(func() error { group, err := a.getGroup(ctx, realm, groupName) if err != nil { return err } m.Lock() defer m.Unlock() groups[groupName] = *group return nil }) } if err := eg.Wait(); err != nil { return nil, fmt.Errorf("failed to get groups by names: %w", err) } return groups, nil } func getGroupByName(groups []gocloak.Group, groupName string) *gocloak.Group { for _, g := range groups { if *g.Name == groupName { return &g } if g.SubGroups != nil { return getGroupByName(*g.SubGroups, groupName) } } return nil } func (a GoCloakAdapter) getChildGroups(ctx context.Context, realm string, parentGroup *gocloak.Group) ([]gocloak.Group, error) { var result []gocloak.Group resp, err := a.client.RestyClient().R(). SetContext(ctx). SetAuthToken(a.token.AccessToken). SetHeader(contentTypeHeader, contentTypeJson). SetPathParams(map[string]string{ keycloakApiParamRealm: realm, "groupID": *parentGroup.ID, }). SetResult(&result). Get(a.buildPath(getChildGroups)) if err = a.checkError(err, resp); err != nil { // Use workaround for Keycloak versions < 23.0.0 for backward compatibility. if strings.Contains(err.Error(), "No resource method found for GET, return 405 with Allow header") { r, err := a.getChildGroupsKCVersionUnder23(ctx, realm, parentGroup) if err != nil { return nil, fmt.Errorf("unable to get child groups: %w", err) } return r, nil } return nil, fmt.Errorf("unable to get child groups, rsp: %s", resp.String()) } return result, nil } // getChildGroupsKCVersionUnder23 is a workaround for Keycloak versions < 23.0.0. // Group model in Keycloak < 23.0.0 contains subgroups. // In Keycloak >= 23.0.0 to get subgroups we need to use dedicated endpoint. func (a GoCloakAdapter) getChildGroupsKCVersionUnder23(ctx context.Context, realm string, parentGroup *gocloak.Group) ([]gocloak.Group, error) { result := &gocloak.Group{} resp, err := a.client.RestyClient().R(). SetContext(ctx). SetAuthToken(a.token.AccessToken). SetHeader(contentTypeHeader, contentTypeJson). SetPathParams(map[string]string{ keycloakApiParamRealm: realm, "groupID": *parentGroup.ID, }). SetResult(result). Get(a.buildPath(getGroup)) if err = a.checkError(err, resp); err != nil { return nil, fmt.Errorf("unable to get group: %s", resp.String()) } if result.SubGroups == nil { return nil, nil } return *result.SubGroups, nil } func (a GoCloakAdapter) syncGroupRoles(realmName, groupID string, spec *keycloakApi.KeycloakRealmGroupSpec) error { roleMap, err := a.client.GetRoleMappingByGroupID(context.Background(), a.token.AccessToken, realmName, groupID) if err != nil { return errors.Wrapf(err, "unable to get role mappings for group spec %+v", spec) } if err := a.syncEntityRealmRoles(groupID, realmName, spec.RealmRoles, roleMap.RealmMappings, a.client.AddRealmRoleToGroup, a.client.DeleteRealmRoleFromGroup); err != nil { return errors.Wrapf(err, "unable to sync group realm roles, groupID: %s with spec %+v", groupID, spec) } claimedClientRoles := make(map[string][]string) for _, cr := range spec.ClientRoles { claimedClientRoles[cr.ClientID] = cr.Roles } if err := a.syncEntityClientRoles(realmName, groupID, claimedClientRoles, roleMap.ClientMappings, a.client.AddClientRoleToGroup, a.client.DeleteClientRoleFromGroup); err != nil { return errors.Wrapf(err, "unable to sync client roles for group: %+v", spec) } return nil } func (a GoCloakAdapter) syncSubGroups(ctx context.Context, realm string, group *gocloak.Group, subGroups []string) error { currentGroups, err := a.makeCurrentGroups(ctx, realm, group) if err != nil { return err } claimedGroups := make(map[string]struct{}, len(subGroups)) for _, g := range subGroups { claimedGroups[g] = struct{}{} } for _, claimed := range subGroups { if _, ok := currentGroups[claimed]; !ok { gr, err := a.getGroup(ctx, realm, claimed) if err != nil { return errors.Wrapf(err, "unable to get group, realm: %s, group: %s", realm, claimed) } if _, err := a.client.CreateChildGroup(ctx, a.token.AccessToken, realm, *group.ID, *gr); err != nil { return errors.Wrapf(err, "unable to create child group, realm: %s, group: %s", realm, claimed) } } } for name, current := range currentGroups { if _, ok := claimedGroups[name]; !ok { // this is strange but if we call create group on subgroup it will be detached from parent group %) if _, err := a.client.CreateGroup(ctx, a.token.AccessToken, realm, current); err != nil { return errors.Wrapf(err, "unable to detach subgroup from group, realm: %s, subgroup: %s, group: %+v", realm, name, group) } } } return nil } func (a GoCloakAdapter) SyncRealmGroup(ctx context.Context, realmName string, spec *keycloakApi.KeycloakRealmGroupSpec) (string, error) { group, err := a.getGroup(ctx, realmName, spec.Name) if err != nil { if !IsErrNotFound(err) { return "", errors.Wrapf(err, "unable to get group with spec %+v", spec) } group = &gocloak.Group{Name: &spec.Name, Path: &spec.Path, Attributes: &spec.Attributes, Access: &spec.Access} groupID, err := a.client.CreateGroup(ctx, a.token.AccessToken, realmName, *group) if err != nil { return "", errors.Wrapf(err, "unable to create group with spec %+v", spec) } group.ID = &groupID } else { group.Path, group.Access, group.Attributes = &spec.Path, &spec.Access, &spec.Attributes if err := a.client.UpdateGroup(ctx, a.token.AccessToken, realmName, *group); err != nil { return "", errors.Wrapf(err, "unable to update group, realm: %s, group spec: %+v", realmName, spec) } } if err := a.syncGroupRoles(realmName, *group.ID, spec); err != nil { return "", errors.Wrapf(err, "unable to sync group realm roles, group: %+v with spec %+v", group, spec) } if err := a.syncSubGroups(ctx, realmName, group, spec.SubGroups); err != nil { return "", errors.Wrapf(err, "unable to sync subgroups, group: %+v with spec: %+v", group, spec) } return *group.ID, nil } func (a GoCloakAdapter) DeleteGroup(ctx context.Context, realm, groupName string) error { group, err := a.getGroup(ctx, realm, groupName) if err != nil { return errors.Wrapf(err, "unable to get group, realm: %s, group: %s", realm, groupName) } if err := a.client.DeleteGroup(ctx, a.token.AccessToken, realm, *group.ID); err != nil { return errors.Wrapf(err, "unable to delete group, realm: %s, group: %s", realm, groupName) } return nil } func (a GoCloakAdapter) makeCurrentGroups(ctx context.Context, realm string, group *gocloak.Group) (map[string]gocloak.Group, error) { child, err := a.getChildGroups(ctx, realm, group) if err != nil { return nil, err } currentGroups := make(map[string]gocloak.Group, len(child)) for _, c := range child { currentGroups[*c.Name] = c } return currentGroups, nil }