From 1f29073c4d9aa644f96b7404127b5e357611c2ad Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Mon, 19 Aug 2024 19:52:11 +0700 Subject: [PATCH 01/10] chore: update keycloak image version and fix docker compose --- Dockerfile | 4 ++-- docker-compose.yml | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1590f824..91d5a03a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -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 @@ -6,6 +6,6 @@ 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_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"] diff --git a/docker-compose.yml b/docker-compose.yml index b422ddbb..99298794 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,5 @@ services: 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"] - \ No newline at end of file + - ./testdata/gocloak-realm.json:/opt/keycloak/data/import/gocloak-realm.json + entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"] From 28abdb61b208c23cb0189083d1584a6a6b6e2ba1 Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Mon, 19 Aug 2024 19:53:29 +0700 Subject: [PATCH 02/10] feat: get child groups --- client.go | 17 +++++++++++++++++ client_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/client.go b/client.go index ccff45fc..6a9f6e91 100644 --- a/client.go +++ b/client.go @@ -1700,6 +1700,23 @@ 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) ([]*Group, error) { + const errMessage = "could not get child groups" + + var result []*Group + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetResult(&result). + 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" diff --git a/client_test.go b/client_test.go index 0cda97c3..475d8223 100644 --- a/client_test.go +++ b/client_test.go @@ -2427,6 +2427,36 @@ 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() + + childGroupID, err := client.CreateChildGroup(context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + groupID, + gocloak.Group{ + Name: GetRandomNameP("Group"), + }, + ) + require.NoError(t, err, "CreateChildGroup failed") + + childGroups, err := client.GetChildGroups( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + groupID, + ) + require.NoError(t, err, "GetChildGroup failed") + require.Len(t, childGroups, 1) + require.Equal(t, childGroupID, *childGroups[0].ID) +} + func Test_GetGroupMembers(t *testing.T) { t.Parallel() cfg := GetConfig(t) From d1d0ad506fc1d2616b169fd1a928a39e588af9fa Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Mon, 19 Aug 2024 19:54:30 +0700 Subject: [PATCH 03/10] docs: update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6f4d7c09..6f993729 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,7 @@ type GoCloak interface { GetGroups(ctx context.Context, accessToken, realm string, params GetGroupsParams) ([]*Group, error) GetGroupsCount(ctx context.Context, token, realm string, params GetGroupsParams) (int, error) GetGroup(ctx context.Context, accessToken, realm, groupID string) (*Group, error) + GetChildGroups(ctx context.Context, token, realm, groupID string) ([]*Group, error) GetDefaultGroups(ctx context.Context, accessToken, realm string) ([]*Group, error) AddDefaultGroup(ctx context.Context, accessToken, realm, groupID string) error RemoveDefaultGroup(ctx context.Context, accessToken, realm, groupID string) error From 27c175a0576b61a23a561cf3bbdc5d4212b89411 Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Mon, 19 Aug 2024 20:57:21 +0700 Subject: [PATCH 04/10] chore: update health check endpoint --- Dockerfile | 1 + docker-compose.yml | 5 +++-- run-tests.sh | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 91d5a03a..6ae473d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ ENV KEYCLOAK_USER=admin ENV KEYCLOAK_PASSWORD=secret ENV KEYCLOAK_ADMIN=admin ENV KEYCLOAK_ADMIN_PASSWORD=secret +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"] diff --git a/docker-compose.yml b/docker-compose.yml index 99298794..3b31655a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3' +version: "3" services: keycloak: @@ -12,8 +12,9 @@ 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 diff --git a/run-tests.sh b/run-tests.sh index 57875f8e..aeac8cd7 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -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) @@ -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 From 7d531d9ba7eae8360eade5a6e40fb30ccce6444b Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Mon, 19 Aug 2024 23:46:53 +0700 Subject: [PATCH 05/10] chore: update docker compose --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3b31655a..6d8b45ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,4 +20,4 @@ services: retries: 5 volumes: - ./testdata/gocloak-realm.json:/opt/keycloak/data/import/gocloak-realm.json - entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"] + entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev", "--features=preview", "--import-realm"] From abbd4272f9b74078756a5d2d386e87697e61dce6 Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Mon, 19 Aug 2024 23:51:50 +0700 Subject: [PATCH 06/10] test: update tests to be compatible with KC 25 --- client_test.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/client_test.go b/client_test.go index 475d8223..9f6fedb6 100644 --- a/client_test.go +++ b/client_test.go @@ -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) { @@ -1410,8 +1410,9 @@ func CreateClientScope(t *testing.T, client *gocloak.GoCloak, scope *gocloak.Cli 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"), } } @@ -1422,10 +1423,10 @@ func CreateClientScope(t *testing.T, client *gocloak.GoCloak, scope *gocloak.Cli 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(), @@ -2634,10 +2635,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") } } @@ -4859,7 +4860,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") } @@ -5778,8 +5779,8 @@ func Test_GetAuthorizationPolicyScopes(t *testing.T) { require.Equal(t, *scopes[0].ID, scopeID) defer func() { - scope() policy() + scope() }() } @@ -6450,7 +6451,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, } @@ -6635,13 +6636,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( @@ -6716,6 +6718,7 @@ E8go1LcvbfHNyknHu2sptnRq55fHZSHr18vVsQRfDYMG "loginHint": "false", "enabledFromMetadata": "true", "idpEntityId": "https://accounts.google.com/o/saml2?idpid=C01unc9st", + "syncMode": "LEGACY", } require.Len( From 6544d8b010f2dd1672bec7d5e2797147d9147bf3 Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Tue, 20 Aug 2024 01:08:09 +0700 Subject: [PATCH 07/10] feat: get unregisterd required actions --- client.go | 17 +++++++++++++++++ models.go | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/client.go b/client.go index 6a9f6e91..cf06f5fa 100644 --- a/client.go +++ b/client.go @@ -4177,6 +4177,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" diff --git a/models.go b/models.go index a093b233..17f83e14 100644 --- a/models.go +++ b/models.go @@ -1411,6 +1411,11 @@ type RequiredActionProviderRepresentation struct { ProviderID *string `json:"providerId,omitempty"` } +type UnregisteredRequiredActionProviderRepresentation struct { + 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 { From 4f88a50ebdb8a0b74a1452adb22ea2584a8eb624 Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Tue, 20 Aug 2024 01:08:49 +0700 Subject: [PATCH 08/10] test: update test create and get required action --- client_test.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/client_test.go b/client_test.go index 9f6fedb6..5bda213d 100644 --- a/client_test.go +++ b/client_test.go @@ -6903,16 +6903,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) From 3f68398054f4a04bf5e88dff3de673eebb8617e0 Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Tue, 20 Aug 2024 01:28:19 +0700 Subject: [PATCH 09/10] feat: get child group params --- client.go | 7 ++++++- client_test.go | 28 ++++++++++++++++------------ models.go | 9 +++++++++ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/client.go b/client.go index cf06f5fa..27123bf3 100644 --- a/client.go +++ b/client.go @@ -1701,13 +1701,18 @@ func (g *GoCloak) GetGroup(ctx context.Context, token, realm, groupID string) (* } // GetChildGroups get child groups of group with id in realm -func (g *GoCloak) GetChildGroups(ctx context.Context, token, realm, groupID string) ([]*Group, error) { +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 { diff --git a/client_test.go b/client_test.go index 5bda213d..ad453da6 100644 --- a/client_test.go +++ b/client_test.go @@ -2437,25 +2437,29 @@ func Test_GetChildGroups(t *testing.T) { tearDown, groupID := CreateGroup(t, client) defer tearDown() - childGroupID, err := client.CreateChildGroup(context.Background(), - token.AccessToken, - cfg.GoCloak.Realm, - groupID, - gocloak.Group{ - Name: GetRandomNameP("Group"), - }, - ) - require.NoError(t, err, "CreateChildGroup failed") + 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, "GetChildGroup failed") - require.Len(t, childGroups, 1) - require.Equal(t, childGroupID, *childGroups[0].ID) + require.NoError(t, err, "GetChildGroups failed") + require.Len(t, childGroups, len(childGroupIDs)) } func Test_GetGroupMembers(t *testing.T) { diff --git a/models.go b/models.go index 17f83e14..61e71a43 100644 --- a/models.go +++ b/models.go @@ -351,6 +351,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) { From 50a8dfb237fe672f364784ee3cdd6351d0e4eb7b Mon Sep 17 00:00:00 2001 From: "surasith.kae" Date: Tue, 20 Aug 2024 01:44:21 +0700 Subject: [PATCH 10/10] docs: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f993729..b304ed20 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ type GoCloak interface { GetGroups(ctx context.Context, accessToken, realm string, params GetGroupsParams) ([]*Group, error) GetGroupsCount(ctx context.Context, token, realm string, params GetGroupsParams) (int, error) GetGroup(ctx context.Context, accessToken, realm, groupID string) (*Group, error) - GetChildGroups(ctx context.Context, token, realm, groupID string) ([]*Group, error) + GetChildGroups(ctx context.Context, token, realm, groupID string, params GetChildGroupsParams) ([]*Group, error) GetDefaultGroups(ctx context.Context, accessToken, realm string) ([]*Group, error) AddDefaultGroup(ctx context.Context, accessToken, realm, groupID string) error RemoveDefaultGroup(ctx context.Context, accessToken, realm, groupID string) error