diff --git a/pkg/common/client.go b/pkg/common/client.go index 79f494e..98cfd87 100644 --- a/pkg/common/client.go +++ b/pkg/common/client.go @@ -796,6 +796,48 @@ func (c *Client) FindGroupByName(groupName string, realmName string) (*Group, er return findInList(groups.([]*Group)), nil } +func (c *Client) FindGroupByPath(groupPath, realmName string) (*Group, error) { + // Given a path "root/sub1/sub2", convert into ["root", "sub1", "sub2"] + paths := strings.Split(groupPath, "/") + + // Find the "root" group + rootGroup, err := c.FindGroupByName(paths[0], realmName) + if err != nil { + return nil, err + } + + // If the path is "root", there's no need to keep looking, return the + // group + if len(paths) == 1 { + return rootGroup, nil + } + + // Iterate through the subpaths ["sub1", "sub2"] and look for the next + // path in the current group level + currentGroup := rootGroup + for level := 1; level < len(paths); level++ { + currentPath := paths[level] + foundInLevel := false + for _, subGroup := range currentGroup.SubGroups { + if subGroup.Name != currentPath { + continue + } + + currentGroup = subGroup + foundInLevel = true + } + + // We iterated through all the subgroups and didn't find + // the subpath, return nil as the group is not in the expected + // hierarchy + if !foundInLevel { + return nil, nil + } + } + + return currentGroup, nil +} + func (c *Client) CreateGroup(groupName string, realmName string) (string, error) { group := Group{ Name: groupName, @@ -1132,6 +1174,7 @@ type KeycloakInterface interface { DeleteUserFromGroup(realmName, userID, groupID string) error FindGroupByName(groupName string, realmName string) (*Group, error) + FindGroupByPath(groupPath, realmName string) (*Group, error) CreateGroup(group string, realmName string) (string, error) MakeGroupDefault(groupID string, realmName string) error ListDefaultGroups(realmName string) ([]*Group, error) diff --git a/pkg/common/client_test.go b/pkg/common/client_test.go index 40f9157..cf2efbb 100644 --- a/pkg/common/client_test.go +++ b/pkg/common/client_test.go @@ -401,6 +401,60 @@ func TestClient_FindGroupByName(t *testing.T) { testClientHTTPRequest(handle, request) } +func TestClient_FindGroupByPath(t *testing.T) { + const ( + existingGroupName string = "gotcha" + existingGroupID string = "12348" + ) + realm := getDummyRealm() + + handle := withPathAssertionBody( + t, + 200, + fmt.Sprintf(GroupListPath, realm.Spec.Realm.Realm), + []*Group{ + { + ID: "12345", + Name: "root", + SubGroups: []*Group{ + { + Name: "skip-me", + ID: "12346", + }, + { + Name: "almost-there", + ID: "12347", + SubGroups: []*Group{ + { + Name: "gotcha", + ID: "12348", + }, + }, + }, + }, + }, + }, + ) + + request := func(c *Client) { + // when the group exists + foundGroup, err := c.FindGroupByPath("root/almost-there/gotcha", "dummy") + // then return the group instance + assert.NoError(t, err) + assert.NotNil(t, foundGroup) + assert.Equal(t, existingGroupID, foundGroup.ID) + assert.Equal(t, existingGroupName, foundGroup.Name) + + // when the group doesn't exist + notFoundGroup, err := c.FindGroupByName("root/oops/gotcha", "dummy") + // then return `nil` + assert.NoError(t, err) + assert.Nil(t, notFoundGroup) + } + + testClientHTTPRequest(handle, request) +} + func TestClient_CreateGroup(t *testing.T) { realm := getDummyRealm() const ( diff --git a/pkg/common/keycloakClient_moq.go b/pkg/common/keycloakClient_moq.go index 988593c..d6735d1 100644 --- a/pkg/common/keycloakClient_moq.go +++ b/pkg/common/keycloakClient_moq.go @@ -96,6 +96,9 @@ var _ KeycloakInterface = &KeycloakInterfaceMock{} // FindGroupByNameFunc: func(groupName string, realmName string) (*Group, error) { // panic("mock out the FindGroupByName method") // }, +// FindGroupByPathFunc: func(groupPath string, realmName string) (*Group, error) { +// panic("mock out the FindGroupByPath method") +// }, // FindGroupClientRoleFunc: func(realmName string, clientID string, groupID string, predicate func(*v1alpha1.KeycloakUserRole) bool) (*v1alpha1.KeycloakUserRole, error) { // panic("mock out the FindGroupClientRole method") // }, @@ -301,6 +304,9 @@ type KeycloakInterfaceMock struct { // FindGroupByNameFunc mocks the FindGroupByName method. FindGroupByNameFunc func(groupName string, realmName string) (*Group, error) + // FindGroupByPathFunc mocks the FindGroupByPath method. + FindGroupByPathFunc func(groupPath string, realmName string) (*Group, error) + // FindGroupClientRoleFunc mocks the FindGroupClientRole method. FindGroupClientRoleFunc func(realmName string, clientID string, groupID string, predicate func(*v1alpha1.KeycloakUserRole) bool) (*v1alpha1.KeycloakUserRole, error) @@ -637,6 +643,13 @@ type KeycloakInterfaceMock struct { // RealmName is the realmName argument value. RealmName string } + // FindGroupByPath holds details about calls to the FindGroupByPath method. + FindGroupByPath []struct { + // GroupPath is the groupPath argument value. + GroupPath string + // RealmName is the realmName argument value. + RealmName string + } // FindGroupClientRole holds details about calls to the FindGroupClientRole method. FindGroupClientRole []struct { // RealmName is the realmName argument value. @@ -946,6 +959,7 @@ type KeycloakInterfaceMock struct { lockFindAuthenticationFlowByAlias sync.RWMutex lockFindAvailableGroupClientRole sync.RWMutex lockFindGroupByName sync.RWMutex + lockFindGroupByPath sync.RWMutex lockFindGroupClientRole sync.RWMutex lockFindUserByEmail sync.RWMutex lockFindUserByUsername sync.RWMutex @@ -1962,6 +1976,41 @@ func (mock *KeycloakInterfaceMock) FindGroupByNameCalls() []struct { return calls } +// FindGroupByPath calls FindGroupByPathFunc. +func (mock *KeycloakInterfaceMock) FindGroupByPath(groupPath string, realmName string) (*Group, error) { + if mock.FindGroupByPathFunc == nil { + panic("KeycloakInterfaceMock.FindGroupByPathFunc: method is nil but KeycloakInterface.FindGroupByPath was just called") + } + callInfo := struct { + GroupPath string + RealmName string + }{ + GroupPath: groupPath, + RealmName: realmName, + } + mock.lockFindGroupByPath.Lock() + mock.calls.FindGroupByPath = append(mock.calls.FindGroupByPath, callInfo) + mock.lockFindGroupByPath.Unlock() + return mock.FindGroupByPathFunc(groupPath, realmName) +} + +// FindGroupByPathCalls gets all the calls that were made to FindGroupByPath. +// Check the length with: +// len(mockedKeycloakInterface.FindGroupByPathCalls()) +func (mock *KeycloakInterfaceMock) FindGroupByPathCalls() []struct { + GroupPath string + RealmName string +} { + var calls []struct { + GroupPath string + RealmName string + } + mock.lockFindGroupByPath.RLock() + calls = mock.calls.FindGroupByPath + mock.lockFindGroupByPath.RUnlock() + return calls +} + // FindGroupClientRole calls FindGroupClientRoleFunc. func (mock *KeycloakInterfaceMock) FindGroupClientRole(realmName string, clientID string, groupID string, predicate func(*v1alpha1.KeycloakUserRole) bool) (*v1alpha1.KeycloakUserRole, error) { if mock.FindGroupClientRoleFunc == nil {