Skip to content

Commit

Permalink
feat: get child groups and get unregistered required actions (#484)
Browse files Browse the repository at this point in the history
* chore: update keycloak image version and fix docker compose

* feat: get child groups

* docs: update readme

* chore: update health check endpoint

* chore: update docker compose

* test: update tests to be compatible with KC 25

* feat: get unregisterd required actions

* test: update test create and get required action

* feat: get child group params

* docs: update readme

---------

Co-authored-by: Nerzal <[email protected]>
  • Loading branch information
surasithaof and Nerzal authored Nov 5, 2024
1 parent d77c788 commit 8867dd1
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 33 deletions.
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
FROM quay.io/keycloak/keycloak:19.0
FROM quay.io/keycloak/keycloak:25.0.1
COPY testdata data/import
WORKDIR /opt/keycloak
ENV KC_HOSTNAME=localhost
ENV KEYCLOAK_USER=admin
ENV KEYCLOAK_PASSWORD=secret
ENV KEYCLOAK_ADMIN=admin
ENV KEYCLOAK_ADMIN_PASSWORD=secret
ENV KC_FEATURES=account-api,account2,authorization,client-policies,impersonation,docker,scripts,upload_scripts,admin-fine-grained-authz
ENV KC_HEALTH_ENABLED=true
ENV KC_FEATURES=account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz
RUN /opt/keycloak/bin/kc.sh import --file /data/import/gocloak-realm.json
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ To get the `clientId` from `id`, use `GetClients` method with `GetClientsParams{

[GoCloakIface](gocloak_iface.go) holds all methods a client should fulfil.


## Configure gocloak to skip TLS Insecure Verification

```go
Expand Down
39 changes: 39 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1719,6 +1719,28 @@ func (g *GoCloak) GetGroup(ctx context.Context, token, realm, groupID string) (*
return &result, nil
}

// GetChildGroups get child groups of group with id in realm
func (g *GoCloak) GetChildGroups(ctx context.Context, token, realm, groupID string, params GetChildGroupsParams) ([]*Group, error) {
const errMessage = "could not get child groups"

var result []*Group
queryParams, err := GetQueryParams(params)
if err != nil {
return nil, errors.Wrap(err, errMessage)
}

resp, err := g.GetRequestWithBearerAuth(ctx, token).
SetResult(&result).
SetQueryParams(queryParams).
Get(g.getAdminRealmURL(realm, "groups", groupID, "children"))

if err := checkForError(resp, err, errMessage); err != nil {
return nil, err
}

return result, nil
}

// GetGroupByPath get group with path in realm
func (g *GoCloak) GetGroupByPath(ctx context.Context, token, realm, groupPath string) (*Group, error) {
const errMessage = "could not get group"
Expand Down Expand Up @@ -4194,6 +4216,23 @@ func (g *GoCloak) RegisterRequiredAction(ctx context.Context, token string, real
return err
}

// GetUnregisteredRequiredActions gets a list of unregistered required actions for a given realm
func (g *GoCloak) GetUnregisteredRequiredActions(ctx context.Context, token string, realm string) ([]*UnregisteredRequiredActionProviderRepresentation, error) {
const errMessage = "could not get unregistered required actions"

var result []*UnregisteredRequiredActionProviderRepresentation

resp, err := g.GetRequestWithBearerAuth(ctx, token).
SetResult(&result).
Get(g.getAdminRealmURL(realm, "authentication", "unregistered-required-actions"))

if err := checkForError(resp, err, errMessage); err != nil {
return nil, err
}

return result, nil
}

// GetRequiredActions gets a list of required actions for a given realm
func (g *GoCloak) GetRequiredActions(ctx context.Context, token string, realm string) ([]*RequiredActionProviderRepresentation, error) {
const errMessage = "could not get required actions"
Expand Down
91 changes: 67 additions & 24 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,7 @@ func Test_LoginClient_UnknownRealm(t *testing.T) {
cfg.GoCloak.ClientSecret,
"ThisRealmDoesNotExist")
require.Error(t, err, "Login shouldn't be successful")
require.EqualError(t, err, "404 Not Found: Realm does not exist")
require.EqualError(t, err, "404 Not Found: Realm does not exist: For more on this error consult the server log at the debug level.")
}

func Test_GetIssuer(t *testing.T) {
Expand Down Expand Up @@ -1410,8 +1410,9 @@ func CreateClientScope(t *testing.T, client gocloak.GoCloakIface, scope *gocloak

if scope == nil {
scope = &gocloak.ClientScope{
ID: GetRandomNameP("client-scope-id-"),
Name: GetRandomNameP("client-scope-name-"),
ID: GetRandomNameP("client-scope-id-"),
Name: GetRandomNameP("client-scope-name-"),
Protocol: gocloak.StringP("openid-connect"),
}
}

Expand All @@ -1422,10 +1423,10 @@ func CreateClientScope(t *testing.T, client gocloak.GoCloakIface, scope *gocloak
cfg.GoCloak.Realm,
*scope,
)
require.NoError(t, err, "CreateClientScope failed")
if !gocloak.NilOrEmpty(scope.ID) {
require.Equal(t, clientScopeID, *scope.ID)
}
require.NoError(t, err, "CreateClientScope failed")
tearDown := func() {
err := client.DeleteClientScope(
context.Background(),
Expand Down Expand Up @@ -2427,6 +2428,40 @@ func Test_GetGroupFull(t *testing.T) {
require.True(t, ok, "UserAttributeContains")
}

func Test_GetChildGroups(t *testing.T) {
t.Parallel()
cfg := GetConfig(t)
client := NewClientWithDebug(t)
token := GetAdminToken(t, client)

tearDown, groupID := CreateGroup(t, client)
defer tearDown()

childGroupIDs := []string{}
for i := 0; i < 3; i++ {
childGroupID, err := client.CreateChildGroup(context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
groupID,
gocloak.Group{
Name: GetRandomNameP("Group"),
},
)
require.NoError(t, err, "CreateChildGroup failed")
childGroupIDs = append(childGroupIDs, childGroupID)
}

childGroups, err := client.GetChildGroups(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
groupID,
gocloak.GetChildGroupsParams{},
)
require.NoError(t, err, "GetChildGroups failed")
require.Len(t, childGroups, len(childGroupIDs))
}

func Test_GetGroupMembers(t *testing.T) {
t.Parallel()
cfg := GetConfig(t)
Expand Down Expand Up @@ -2604,10 +2639,10 @@ func Test_SendVerifyEmail(t *testing.T) {
cfg.GoCloak.Realm,
params)
if err != nil {
if err.Error() == "500 Internal Server Error: Failed to send execute actions email" {
if err.Error() == "500 Internal Server Error: Failed to send verify email" {
return
}
require.NoError(t, err, "ExecuteActionsEmail failed")
require.NoError(t, err, "SendVerifyEmail failed")
}
}

Expand Down Expand Up @@ -4830,7 +4865,7 @@ func Test_CreateDeleteClientScopeWithMappers(t *testing.T) {
cfg.GoCloak.Realm,
id,
)
require.EqualError(t, err, "404 Not Found: Could not find client scope")
require.EqualError(t, err, "404 Not Found: Could not find client scope: For more on this error consult the server log at the debug level.")
require.Nil(t, clientScopeActual, "client scope has not been deleted")
}

Expand Down Expand Up @@ -5749,8 +5784,8 @@ func Test_GetAuthorizationPolicyScopes(t *testing.T) {
require.Equal(t, *scopes[0].ID, scopeID)

defer func() {
scope()
policy()
scope()
}()
}

Expand Down Expand Up @@ -6421,7 +6456,7 @@ func Test_CheckError(t *testing.T) {

expectedError := &gocloak.APIError{
Code: http.StatusNotFound,
Message: "404 Not Found: Could not find client",
Message: "404 Not Found: Could not find client: For more on this error consult the server log at the debug level.",
Type: gocloak.APIErrTypeUnknown,
}

Expand Down Expand Up @@ -6606,13 +6641,14 @@ func Test_ImportIdentityProviderConfig(t *testing.T) {
require.NoError(t, err, "ImportIdentityProviderConfig failed")

expected := map[string]string{
"userInfoUrl": "https://openidconnect.googleapis.com/v1/userinfo",
"validateSignature": "true",
"tokenUrl": "https://oauth2.googleapis.com/token",
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"jwksUrl": "https://www.googleapis.com/oauth2/v3/certs",
"issuer": "https://accounts.google.com",
"useJwksUrl": "true",
"userInfoUrl": "https://openidconnect.googleapis.com/v1/userinfo",
"validateSignature": "true",
"tokenUrl": "https://oauth2.googleapis.com/token",
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"jwksUrl": "https://www.googleapis.com/oauth2/v3/certs",
"issuer": "https://accounts.google.com",
"useJwksUrl": "true",
"metadataDescriptorUrl": "https://accounts.google.com/.well-known/openid-configuration",
}

require.Len(
Expand Down Expand Up @@ -6687,6 +6723,7 @@ E8go1LcvbfHNyknHu2sptnRq55fHZSHr18vVsQRfDYMG</ds:X509Certificate>
"loginHint": "false",
"enabledFromMetadata": "true",
"idpEntityId": "https://accounts.google.com/o/saml2?idpid=C01unc9st",
"syncMode": "LEGACY",
}

require.Len(
Expand Down Expand Up @@ -6871,16 +6908,22 @@ func TestGocloak_CreateAndGetRequiredAction(t *testing.T) {
cfg := GetConfig(t)
client := NewClientWithDebug(t)
token := GetAdminToken(t, client)

// need to get unregistered required actions first
// refer to test suit of Keycloak for more details
// https://github.com/keycloak/keycloak/blob/main/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java#L93
unregisteredRequiredActions, err := client.GetUnregisteredRequiredActions(context.Background(), token.AccessToken, cfg.GoCloak.Realm)
require.NoError(t, err, "Failed to get required actions")
require.NotEmpty(t, unregisteredRequiredActions, "Required actions must not be empty")
require.NotNil(t, unregisteredRequiredActions[0].Name, "Required action name must not be nil")
require.NotNil(t, unregisteredRequiredActions[0].ProviderID, "Required action alias must not be nil")

requiredAction := gocloak.RequiredActionProviderRepresentation{
Alias: gocloak.StringP("VERIFY_EMAIL_NEW"),
Config: nil,
DefaultAction: gocloak.BoolP(false),
Enabled: gocloak.BoolP(true),
Name: gocloak.StringP("Verify Email new"),
Priority: gocloak.Int32P(50),
ProviderID: gocloak.StringP("VERIFY_EMAIL_NEW"),
Alias: unregisteredRequiredActions[0].ProviderID,
Name: unregisteredRequiredActions[0].Name,
ProviderID: unregisteredRequiredActions[0].ProviderID,
}
err := client.RegisterRequiredAction(context.Background(), token.AccessToken, cfg.GoCloak.Realm, requiredAction)
err = client.RegisterRequiredAction(context.Background(), token.AccessToken, cfg.GoCloak.Realm, requiredAction)
require.NoError(t, err, "Failed to register required action")

ra, err := client.GetRequiredAction(context.Background(), token.AccessToken, cfg.GoCloak.Realm, *requiredAction.Alias)
Expand Down
9 changes: 5 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
version: "3"
services:
keycloak:
build: .
Expand All @@ -10,12 +11,12 @@ services:
KC_HEALTH_ENABLED: "true"
ports:
- "8080:8080"
- "9000:9000"
healthcheck:
test: curl --fail --silent http://localhost:8080/health/ready 2>&1 || exit 1
test: curl --fail --silent http://localhost:9000/health/ready 2>&1 || exit 1
interval: 10s
timeout: 10s
retries: 5
volumes:
- ./testdata/gocloak-realm.json:/opt/keycloak/data/import/gocloak-realm.json
entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev --features=preview --import-realm"]

- ./testdata/gocloak-realm.json:/opt/keycloak/data/import/gocloak-realm.json
entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev", "--features=preview", "--import-realm"]
14 changes: 14 additions & 0 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,15 @@ type GetGroupsParams struct {
Search *string `json:"search,omitempty"`
}

// GetChildGroupsParams represents the optional parameters for getting child groups
type GetChildGroupsParams struct {
BriefRepresentation *bool `json:"briefRepresentation,string,omitempty"`
Exact *bool `json:"exact,string,omitempty"`
First *int `json:"first,string,omitempty"`
Max *int `json:"max,string,omitempty"`
Search *string `json:"search,omitempty"`
}

// MarshalJSON is a custom json marshaling function to automatically set the Full and BriefRepresentation properties
// for backward compatibility
func (obj GetGroupsParams) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -1416,6 +1425,11 @@ type RequiredActionProviderRepresentation struct {
ProviderID *string `json:"providerId,omitempty"`
}

type UnregisteredRequiredActionProviderRepresentation struct {

Check failure on line 1428 in models.go

View workflow job for this annotation

GitHub Actions / tests

exported: exported type UnregisteredRequiredActionProviderRepresentation should have comment or be unexported (revive)
Name *string `json:"name,omitempty"`
ProviderID *string `json:"providerId,omitempty"`
}

// ManagementPermissionRepresentation is a representation of management permissions
// v18: https://www.keycloak.org/docs-api/18.0/rest-api/#_managementpermissionreference
type ManagementPermissionRepresentation struct {
Expand Down
6 changes: 3 additions & 3 deletions run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
docker compose down
docker compose up -d

keycloakServer=http://localhost:8080
url="${keycloakServer}/health"
keycloakServer=http://localhost
url="${keycloakServer}:9000/health"
echo "Checking service availability at $url (CTRL+C to exit)"
while true; do
response=$(curl -s -o /dev/null -w "%{http_code}" $url)
Expand All @@ -13,7 +13,7 @@ while true; do
fi
sleep 1
done
echo "Service is now available at ${keycloakServer}"
echo "Service is now available at ${keycloakServer}:8080"

ARGS=()
if [ $# -gt 0 ]; then
Expand Down

0 comments on commit 8867dd1

Please sign in to comment.