Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: get child groups and get unregistered required actions #484

Merged
merged 11 commits into from
Nov 5, 2024
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 @@ -4179,6 +4201,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 @@ -351,6 +351,15 @@
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 @@ -1411,6 +1420,11 @@
ProviderID *string `json:"providerId,omitempty"`
}

type UnregisteredRequiredActionProviderRepresentation struct {

Check failure on line 1423 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
Loading