From c2afc5e5f25f27cccf0ade60cfe3b9acea72f14a Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Mon, 30 Dec 2024 19:12:49 +0200 Subject: [PATCH 01/18] Added optional parameters for getting organizations --- models.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/models.go b/models.go index 4d526aeb..42989526 100644 --- a/models.go +++ b/models.go @@ -1445,6 +1445,16 @@ type GetClientUserSessionsParams struct { Max *int `json:"max,string,omitempty"` } +// GetOrganizationsParams represents the optional parameters for getting organizations +type GetOrganizationsParams 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"` + Q *string `json:"q,omitempty"` + Search *string `json:"search,omitempty"` +} + // prettyStringStruct returns struct formatted into pretty string func prettyStringStruct(t interface{}) string { json, err := json.MarshalIndent(t, "", "\t") From bbfdfea0ebc225586f1d15adb73ac9c4fd0f0e20 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Mon, 30 Dec 2024 19:17:21 +0200 Subject: [PATCH 02/18] Added Organization representations --- models.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/models.go b/models.go index 42989526..58fd8137 100644 --- a/models.go +++ b/models.go @@ -1455,6 +1455,28 @@ type GetOrganizationsParams struct { Search *string `json:"search,omitempty"` } +// OrganizationDomainRepresentation is a representation of an organization's domain +// v26: https://www.keycloak.org/docs-api/latest/rest-api/index.html#OrganizationOrganizationDomainRepresentation +type OrganizationDomainRepresentation struct { + Name *string `json:"name,omitempty"` + Verified *bool `json:"verified,omitempty"` +} + +// OrganizationRepresentation is a representation of an organization +// v26: https://www.keycloak.org/docs-api/latest/rest-api/index.html#OrganizationRepresentation +type OrganizationRepresentation struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Alias *string `json:"alias,omitempty"` + Enable *bool `json:"enabled,omitempty"` + Description *string `json:"description,omitempty"` + RedirectURL *string `json:"redirectUrl,omitempty"` + Attributes *map[string][]string `json:"attributes,omitempty"` + Domains *[]OrganizationDomainRepresentation `json:"domains,omitempty"` + Members *[]string `json:"members,omitempty"` + IdentityProviders *[]string `json:"identityProviders,omitempty"` +} + // prettyStringStruct returns struct formatted into pretty string func prettyStringStruct(t interface{}) string { json, err := json.MarshalIndent(t, "", "\t") @@ -1549,3 +1571,6 @@ func (v *CredentialRepresentation) String() string { return pre func (v *RequiredActionProviderRepresentation) String() string { return prettyStringStruct(v) } func (v *BruteForceStatus) String() string { return prettyStringStruct(v) } func (v *GetClientUserSessionsParams) String() string { return prettyStringStruct(v) } +func (v *GetOrganizationsParams) String() string { return prettyStringStruct(v) } +func (v *OrganizationDomainRepresentation) String() string { return prettyStringStruct(v) } +func (v *OrganizationRepresentation) String() string { return prettyStringStruct(v) } From 45067d3fc3da18b80a8e4e9041a716ccaa1e34c4 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Mon, 30 Dec 2024 20:36:30 +0200 Subject: [PATCH 03/18] Add organizations api for create, delete and get organization(s) --- client.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ gocloak_iface.go | 6 ++++++ 2 files changed, 53 insertions(+) diff --git a/client.go b/client.go index c06063be..990a61f3 100644 --- a/client.go +++ b/client.go @@ -4507,3 +4507,50 @@ func (g *GoCloak) GetUsersManagementPermissions(ctx context.Context, accessToken return &result, nil } + +// CreateOrganization creates a new Organization +func (g *GoCloak) CreateOrganization(ctx context.Context, token, realm string, organization OrganizationRepresentation) (string, error) { + const errMessage = "could not create organization" + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetBody(organization). + Post(g.getAdminRealmURL(realm, "organizations")) + + if err := checkForError(resp, err, errMessage); err != nil { + return "", err + } + + return getID(resp), nil +} + +// GetOrganizations returns a paginated list of organizations filtered according to the specified parameters +func (g *GoCloak) GetOrganizations(ctx context.Context, token, realm string, params GetOrganizationsParams) ([]*OrganizationRepresentation, error) { + const errMessage = "could not get organizations" + + queryParams, err := GetQueryParams(params) + if err != nil { + return nil, errors.Wrap(err, errMessage) + } + + var result []*OrganizationRepresentation + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetQueryParams(queryParams). + SetResult(&result). + Get(g.getAdminRealmURL(realm, "organizations")) + if err := checkForError(resp, err, errMessage); err != nil { + return nil, err + } + + return result, nil +} + +// DeleteOrganization deletes the organization +func (g *GoCloak) DeleteOrganization(ctx context.Context, token, realm, idOfOrganization string) error { + const errMessage = "could not delete organization" + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + Delete(g.getAdminRealmURL(realm, "organizations", idOfOrganization)) + + return checkForError(resp, err, errMessage) +} diff --git a/gocloak_iface.go b/gocloak_iface.go index 32dbf9b3..cc64e9ee 100644 --- a/gocloak_iface.go +++ b/gocloak_iface.go @@ -578,4 +578,10 @@ type GoCloakIface interface { UpdateUsersManagementPermissions(ctx context.Context, accessToken, realm string, managementPermissions ManagementPermissionRepresentation) (*ManagementPermissionRepresentation, error) // GetUsersManagementPermissions returns the management permissions for users GetUsersManagementPermissions(ctx context.Context, accessToken, realm string) (*ManagementPermissionRepresentation, error) + // CreateOrganization creates a new Organization + CreateOrganization(ctx context.Context, token, realm string, organization OrganizationRepresentation) (string, error) + // GetOrganizations returns a paginated list of organizations filtered according to the specified parameters + GetOrganizations(ctx context.Context, token, realm string, params GetOrganizationsParams) ([]*OrganizationRepresentation, error) + // DeleteOrganization deletes the organization + DeleteOrganization(ctx context.Context, token, realm, idOfOrganization string) error } From 444cdcb03e647beb575dcf51e966dc261e54373d Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Mon, 30 Dec 2024 20:38:53 +0200 Subject: [PATCH 04/18] Add tests for the organizations API - create, delete and get (All, ByName, ByDomain) --- client_test.go | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ model_test.go | 3 ++ 2 files changed, 117 insertions(+) diff --git a/client_test.go b/client_test.go index 9a39059a..6c86948d 100644 --- a/client_test.go +++ b/client_test.go @@ -7112,3 +7112,117 @@ func Test_RevokeToken(t *testing.T) { ) require.NoError(t, err, "Revoke failed") } + +// ----------- +// Organizations +// ----------- + +func CreateOrganization(t *testing.T, client gocloak.GoCloakIface, name, alias, domain string) (func(), string) { + cfg := GetConfig(t) + token := GetAdminToken(t, client) + + org := gocloak.OrganizationRepresentation{ + Name: gocloak.StringP(name), + Alias: gocloak.StringP(alias), + Enable: gocloak.BoolP(true), + Description: gocloak.StringP("Just a test organization"), + Domains: &[]gocloak.OrganizationDomainRepresentation{ + { + Name: gocloak.StringP(domain), + Verified: gocloak.BoolP(true), + }, + }, + } + + orgID, err := client.CreateOrganization( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + org) + + require.NoError(t, err, "CreateOrganization failed") + + org.ID = &orgID + t.Logf("Created Organization: %+v", org) + tearDown := func() { + err := client.DeleteOrganization( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + orgID) + require.NoError(t, err, "DeleteOrganization") + } + + return tearDown, orgID +} + +func Test_CreateOrganization(t *testing.T) { + t.Parallel() + client := NewClientWithDebug(t) + + tearDown, _ := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() +} + +func Test_GetOrganizations(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + // Create two organizations + tearDown1, _ := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown1() + + tearDown2, _ := CreateOrganization(t, client, "Another Inc", "another-inc", "another.com") + defer tearDown2() + + organizations, err := client.GetOrganizations( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + gocloak.GetOrganizationsParams{}) + require.NoError(t, err, "GetOrganizations failed") + t.Log(organizations) +} + +func Test_GetOrganizationByName(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + tearDown, _ := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + organization, err := client.GetOrganizations( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + gocloak.GetOrganizationsParams{ + Search: gocloak.StringP("Test Inc"), + }) + require.NoError(t, err, "GetOrganizationByName failed") + t.Log(organization) +} + +func Test_GetOrganizationByDomain(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + tearDown, _ := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + organization, err := client.GetOrganizations( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + gocloak.GetOrganizationsParams{ + Search: gocloak.StringP("test-inc.org"), + }) + require.NoError(t, err, "GetOrganizationByDomain failed") + fmt.Printf("%+v", organization) + t.Log(organization) +} diff --git a/model_test.go b/model_test.go index 1a5affcc..e0a988ff 100644 --- a/model_test.go +++ b/model_test.go @@ -336,6 +336,9 @@ func TestStringerOmitEmpty(t *testing.T) { &gocloak.RequestingPartyTokenOptions{}, &gocloak.RequestingPartyPermission{}, &gocloak.GetClientUserSessionsParams{}, + &gocloak.GetOrganizationsParams{}, + &gocloak.OrganizationDomainRepresentation{}, + &gocloak.OrganizationRepresentation{}, } for _, custom := range customs { From 4ac7b61c16434a1ec3b008f149b3fed1f72ab0c1 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Mon, 30 Dec 2024 21:51:34 +0200 Subject: [PATCH 05/18] Add organizations api for GetOrganizationByID and UpdateOrganization --- client.go | 31 +++++++++++++++++++++++++++++++ gocloak_iface.go | 4 ++++ 2 files changed, 35 insertions(+) diff --git a/client.go b/client.go index 990a61f3..e9bbe44f 100644 --- a/client.go +++ b/client.go @@ -4554,3 +4554,34 @@ func (g *GoCloak) DeleteOrganization(ctx context.Context, token, realm, idOfOrga return checkForError(resp, err, errMessage) } + +// GetOrganization returns the organization representation of the organization with provided ID +func (g *GoCloak) GetOrganizationByID(ctx context.Context, token, realm, idOfOrganization string) (*OrganizationRepresentation, error) { + const errMessage = "could not find organization" + var result *OrganizationRepresentation + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetResult(&result). + Get(g.getAdminRealmURL(realm, "organizations", idOfOrganization)) + + if err := checkForError(resp, err, errMessage); err != nil { + return nil, err + } + + return result, nil +} + +// UpdateOrganization updates the given organization +func (g *GoCloak) UpdateOrganization(ctx context.Context, token, realm string, organization OrganizationRepresentation) error { + const errMessage = "could not update organization" + + if NilOrEmpty(organization.ID) { + return errors.Wrap(errors.New("ID of an organization required"), errMessage) + } + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetBody(organization). + Put(g.getAdminRealmURL(realm, "organizations", PString(organization.ID))) + + return checkForError(resp, err, errMessage) +} diff --git a/gocloak_iface.go b/gocloak_iface.go index cc64e9ee..192d749c 100644 --- a/gocloak_iface.go +++ b/gocloak_iface.go @@ -584,4 +584,8 @@ type GoCloakIface interface { GetOrganizations(ctx context.Context, token, realm string, params GetOrganizationsParams) ([]*OrganizationRepresentation, error) // DeleteOrganization deletes the organization DeleteOrganization(ctx context.Context, token, realm, idOfOrganization string) error + // GetOrganization returns the organization representation of the organization with provided ID + GetOrganizationByID(ctx context.Context, token, realm, idOfOrganization string) (*OrganizationRepresentation, error) + // UpdateOrganization updates the given organization + UpdateOrganization(ctx context.Context, token, realm string, organization OrganizationRepresentation) error } From fc6feb5643e8b3d5a50fe30aa772fa8a51e5667a Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Mon, 30 Dec 2024 21:52:48 +0200 Subject: [PATCH 06/18] Add tests for the organizations API - GetOrganizationByID and UpdateOrganization --- client_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/client_test.go b/client_test.go index 6c86948d..798a35f0 100644 --- a/client_test.go +++ b/client_test.go @@ -7186,7 +7186,7 @@ func Test_GetOrganizations(t *testing.T) { t.Log(organizations) } -func Test_GetOrganizationByName(t *testing.T) { +func Test_GetOrganizationsByName(t *testing.T) { t.Parallel() cfg := GetConfig(t) client := NewClientWithDebug(t) @@ -7206,7 +7206,7 @@ func Test_GetOrganizationByName(t *testing.T) { t.Log(organization) } -func Test_GetOrganizationByDomain(t *testing.T) { +func Test_GetOrganizationsByDomain(t *testing.T) { t.Parallel() cfg := GetConfig(t) client := NewClientWithDebug(t) @@ -7223,6 +7223,50 @@ func Test_GetOrganizationByDomain(t *testing.T) { Search: gocloak.StringP("test-inc.org"), }) require.NoError(t, err, "GetOrganizationByDomain failed") - fmt.Printf("%+v", organization) + t.Log(organization) +} + +func Test_GetOrganizationByID(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + organization, err := client.GetOrganizationByID( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + orgID) + require.NoError(t, err, "GetOrganization failed") + t.Log(organization) +} + +func Test_UpdateOrganization(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + organization, err := client.GetOrganizationByID( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + orgID) + require.NoError(t, err, "GetOrganizationByID failed") + + organization.Enable = gocloak.BoolP(false) + + err = client.UpdateOrganization( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + *organization) + require.NoError(t, err, "UpdateOrganization failed") t.Log(organization) } From 29430e73a112aaa414281148fed350cb7e66b0ac Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 12:23:20 +0200 Subject: [PATCH 07/18] Add form params for invitee --- models.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/models.go b/models.go index 58fd8137..5e693a35 100644 --- a/models.go +++ b/models.go @@ -1445,6 +1445,13 @@ type GetClientUserSessionsParams struct { Max *int `json:"max,string,omitempty"` } +// InviteeFormParams represents the form parameters used to invite a user to an organization +type InviteeFormParams struct { + Email *string `json:"email,omitempty"` + FirstName *string `json:"firstname,omitempty"` + LastName *string `json:"lastname,omitempty"` +} + // GetOrganizationsParams represents the optional parameters for getting organizations type GetOrganizationsParams struct { BriefRepresentation *bool `json:"briefRepresentation,string,omitempty"` @@ -1572,5 +1579,6 @@ func (v *RequiredActionProviderRepresentation) String() string { return pre func (v *BruteForceStatus) String() string { return prettyStringStruct(v) } func (v *GetClientUserSessionsParams) String() string { return prettyStringStruct(v) } func (v *GetOrganizationsParams) String() string { return prettyStringStruct(v) } +func (v *InviteeFormParams) String() string { return prettyStringStruct(v) } func (v *OrganizationDomainRepresentation) String() string { return prettyStringStruct(v) } func (v *OrganizationRepresentation) String() string { return prettyStringStruct(v) } From cc9bc4150627273d9afabbacd1c37579b0daca36 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 12:25:51 +0200 Subject: [PATCH 08/18] feature: implement APIs for user invitation or addition to an organization --- client.go | 50 +++++++++++++++++++++++++++++++++++++++++++++++- gocloak_iface.go | 13 ++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index e9bbe44f..21b7056c 100644 --- a/client.go +++ b/client.go @@ -4555,7 +4555,7 @@ func (g *GoCloak) DeleteOrganization(ctx context.Context, token, realm, idOfOrga return checkForError(resp, err, errMessage) } -// GetOrganization returns the organization representation of the organization with provided ID +// GetOrganizationByID returns the organization representation of the organization with provided ID func (g *GoCloak) GetOrganizationByID(ctx context.Context, token, realm, idOfOrganization string) (*OrganizationRepresentation, error) { const errMessage = "could not find organization" var result *OrganizationRepresentation @@ -4585,3 +4585,51 @@ func (g *GoCloak) UpdateOrganization(ctx context.Context, token, realm string, o return checkForError(resp, err, errMessage) } + +// InviteUserToOrganizationByID invites an existing user to the organization, using the specified user id +// An invitation email will be sent to the user so SMTP settings are required in keycloak +func (g *GoCloak) InviteUserToOrganizationByID(ctx context.Context, token, realm, idOfOrganization, userID string) error { + const errMessage = "could not invite user to organization" + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetFormData(map[string]string{ + "id": userID, + }). + Post(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members", "invite-existing-user")) + + return checkForError(resp, err, errMessage) +} + +// InviteUserToOrganizationByEmail invites an existing user or sends a registration link to a new user, based on the provided e-mail address. +// If the user with the given e-mail address exists, it sends an invitation link, otherwise it sends a registration link. +// An invitation email will be sent to the user so SMTP settings are required in keycloak +func (g *GoCloak) InviteUserToOrganizationByEmail(ctx context.Context, token, realm, idOfOrganization string, userParams InviteeFormParams) error { + const errMessage = "could not invite user to organization" + + if NilOrEmpty(userParams.Email) { + return errors.Wrap(errors.New("Email of invitee required"), errMessage) + } + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetFormData(map[string]string{ + "email": *userParams.Email, + "firstName": *userParams.FirstName, + "lastName": *userParams.LastName, + }). + Post(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members", "invite-user")) + + return checkForError(resp, err, errMessage) +} + +// AddUserToOrganization adds the user with the specified id as a member of the organization +// Adds, or associates, an existing user with the organization. If no user is found, or if it is already associated with the organization, an error response is returned +// No invitation email is sent to the user +func (g *GoCloak) AddUserToOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error { + const errMessage = "could not add user to organization" + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetBody(idOfUser). + Post(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members")) + + return checkForError(resp, err, errMessage) +} diff --git a/gocloak_iface.go b/gocloak_iface.go index 192d749c..81269e73 100644 --- a/gocloak_iface.go +++ b/gocloak_iface.go @@ -584,8 +584,19 @@ type GoCloakIface interface { GetOrganizations(ctx context.Context, token, realm string, params GetOrganizationsParams) ([]*OrganizationRepresentation, error) // DeleteOrganization deletes the organization DeleteOrganization(ctx context.Context, token, realm, idOfOrganization string) error - // GetOrganization returns the organization representation of the organization with provided ID + // GetOrganizationByID returns the organization representation of the organization with provided ID GetOrganizationByID(ctx context.Context, token, realm, idOfOrganization string) (*OrganizationRepresentation, error) // UpdateOrganization updates the given organization UpdateOrganization(ctx context.Context, token, realm string, organization OrganizationRepresentation) error + // InviteUserToOrganizationByID invites an existing user to the organization, using the specified user id + // An invitation email will be sent to the user so SMTP settings are required in keycloak + InviteUserToOrganizationByID(ctx context.Context, token, realm, idOfOrganization, userID string) error + // InviteUserToOrganizationByEmail invites an existing user or sends a registration link to a new user, based on the provided e-mail address. + // If the user with the given e-mail address exists, it sends an invitation link, otherwise it sends a registration link. + // An invitation email will be sent to the user so SMTP settings are required in keycloak + InviteUserToOrganizationByEmail(ctx context.Context, token, realm, idOfOrganization string, userParams InviteeFormParams) error + // AddUserToOrganization adds the user with the specified id as a member of the organization + // Adds, or associates, an existing user with the organization. If no user is found, or if it is already associated with the organization, an error response is returned + // No invitation email is sent to the user + AddUserToOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error } From 3caa2d54f1292ef992a4358cca84c12307c1ee56 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 12:27:18 +0200 Subject: [PATCH 09/18] Add tests for user addition or invitation --- client_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/client_test.go b/client_test.go index 798a35f0..277221f7 100644 --- a/client_test.go +++ b/client_test.go @@ -7270,3 +7270,78 @@ func Test_UpdateOrganization(t *testing.T) { require.NoError(t, err, "UpdateOrganization failed") t.Log(organization) } + +func Test_InviteUserToOrganizationByID(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + td, userID := CreateUser(t, client) + defer td() + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + err := client.InviteUserToOrganizationByID( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID) + require.NoError(t, err, "InviteUserToOrganizationByID failed") +} + +func Test_InviteUserToOrganizationByEmail(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + td, userID := CreateUser(t, client) + defer td() + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + ctx := context.Background() + user, err := client.GetUserByID( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + userID) + require.NoError(t, err, "GetUserByID failed") + + err = client.InviteUserToOrganizationByEmail( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + gocloak.InviteeFormParams{ + Email: user.Email, + FirstName: GetRandomNameP("FirstName"), + LastName: GetRandomNameP("LastName"), + }) + require.NoError(t, err, "InviteUserToOrganizationByEmail failed") +} + +func Test_AddUserToOrganization(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + td, userID := CreateUser(t, client) + defer td() + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + err := client.AddUserToOrganization( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID) + require.NoError(t, err, "AddUserToOrganization failed") +} From cf16820a8f7cfd75c48a96269fa7ff2d9c5314e6 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 13:15:27 +0200 Subject: [PATCH 10/18] Add API to remove user from an organization --- client.go | 10 ++++++++++ client_test.go | 31 +++++++++++++++++++++++++++++++ gocloak_iface.go | 2 ++ 3 files changed, 43 insertions(+) diff --git a/client.go b/client.go index 21b7056c..085ee116 100644 --- a/client.go +++ b/client.go @@ -4633,3 +4633,13 @@ func (g *GoCloak) AddUserToOrganization(ctx context.Context, token, realm, idOfO return checkForError(resp, err, errMessage) } + +// RemoveUserFromOrganization removes the user with the specified id from the organization +func (g *GoCloak) RemoveUserFromOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error { + const errMessage = "could not delete organization" + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + Delete(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members", idOfUser)) + + return checkForError(resp, err, errMessage) +} diff --git a/client_test.go b/client_test.go index 277221f7..a995fd45 100644 --- a/client_test.go +++ b/client_test.go @@ -7345,3 +7345,34 @@ func Test_AddUserToOrganization(t *testing.T) { userID) require.NoError(t, err, "AddUserToOrganization failed") } + +func Test_RemoveUserFromOrganization(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + td, userID := CreateUser(t, client) + defer td() + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + ctx := context.Background() + + err := client.AddUserToOrganization( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID) + require.NoError(t, err, "AddUserToOrganization failed") + + err = client.RemoveUserFromOrganization( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID) + require.NoError(t, err, "RemoveUserFromOrganization failed") +} diff --git a/gocloak_iface.go b/gocloak_iface.go index 81269e73..208ceba1 100644 --- a/gocloak_iface.go +++ b/gocloak_iface.go @@ -599,4 +599,6 @@ type GoCloakIface interface { // Adds, or associates, an existing user with the organization. If no user is found, or if it is already associated with the organization, an error response is returned // No invitation email is sent to the user AddUserToOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error + // RemoveUserFromOrganization removes the user with the specified id from the organization + RemoveUserFromOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error } From ca3dd9c2e634852fe5e737882a5ba275d7d63270 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 13:55:46 +0200 Subject: [PATCH 11/18] Added API to count members of an organization --- client.go | 23 ++++++++++++++++++++--- client_test.go | 32 ++++++++++++++++++++++++++++++++ gocloak_iface.go | 2 ++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 085ee116..a6ea6f86 100644 --- a/client.go +++ b/client.go @@ -4589,7 +4589,7 @@ func (g *GoCloak) UpdateOrganization(ctx context.Context, token, realm string, o // InviteUserToOrganizationByID invites an existing user to the organization, using the specified user id // An invitation email will be sent to the user so SMTP settings are required in keycloak func (g *GoCloak) InviteUserToOrganizationByID(ctx context.Context, token, realm, idOfOrganization, userID string) error { - const errMessage = "could not invite user to organization" + const errMessage = "could not invite user to organization by id" resp, err := g.GetRequestWithBearerAuth(ctx, token). SetFormData(map[string]string{ @@ -4604,7 +4604,7 @@ func (g *GoCloak) InviteUserToOrganizationByID(ctx context.Context, token, realm // If the user with the given e-mail address exists, it sends an invitation link, otherwise it sends a registration link. // An invitation email will be sent to the user so SMTP settings are required in keycloak func (g *GoCloak) InviteUserToOrganizationByEmail(ctx context.Context, token, realm, idOfOrganization string, userParams InviteeFormParams) error { - const errMessage = "could not invite user to organization" + const errMessage = "could not invite user to organization by email" if NilOrEmpty(userParams.Email) { return errors.Wrap(errors.New("Email of invitee required"), errMessage) @@ -4636,10 +4636,27 @@ func (g *GoCloak) AddUserToOrganization(ctx context.Context, token, realm, idOfO // RemoveUserFromOrganization removes the user with the specified id from the organization func (g *GoCloak) RemoveUserFromOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error { - const errMessage = "could not delete organization" + const errMessage = "could not remove user from organization" resp, err := g.GetRequestWithBearerAuth(ctx, token). Delete(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members", idOfUser)) return checkForError(resp, err, errMessage) } + +// GetOrganizationMemberCount returns number of members in the organization. +func (g *GoCloak) GetOrganizationMemberCount(ctx context.Context, token, realm, idOfOrganization string) (int, error) { + const errMessage = "could not get organization members count" + + var result int + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetResult(&result). + Get(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members", "count")) + + if err := checkForError(resp, err, errMessage); err != nil { + return -1, errors.Wrap(err, errMessage) + } + + return result, err +} diff --git a/client_test.go b/client_test.go index a995fd45..fd7f170f 100644 --- a/client_test.go +++ b/client_test.go @@ -7376,3 +7376,35 @@ func Test_RemoveUserFromOrganization(t *testing.T) { userID) require.NoError(t, err, "RemoveUserFromOrganization failed") } + +func Test_GetOrganizationMemberCount(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + td, userID := CreateUser(t, client) + defer td() + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + ctx := context.Background() + err := client.AddUserToOrganization( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID) + require.NoError(t, err, "AddUserToOrganization failed") + + count, err := client.GetOrganizationMemberCount( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID) + + t.Logf("Members in Organization: %d", count) + require.Equal(t, 1, count) + require.NoError(t, err, "GetOrganizationMemberCount failed") +} diff --git a/gocloak_iface.go b/gocloak_iface.go index 208ceba1..c6facc6b 100644 --- a/gocloak_iface.go +++ b/gocloak_iface.go @@ -601,4 +601,6 @@ type GoCloakIface interface { AddUserToOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error // RemoveUserFromOrganization removes the user with the specified id from the organization RemoveUserFromOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error + // GetOrganizationMemberCount returns number of members in the organization. + GetOrganizationMemberCount(ctx context.Context, token, realm, idOfOrganization string) (int, error) } From dd9d08b626818af13201ae5f38f01b904e9f6fcc Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 14:33:40 +0200 Subject: [PATCH 12/18] Add MemberRepresentation type --- models.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/models.go b/models.go index 5e693a35..3848ad20 100644 --- a/models.go +++ b/models.go @@ -1452,6 +1452,18 @@ type InviteeFormParams struct { LastName *string `json:"lastname,omitempty"` } +// MembershipType represent the membership type of an organization member. +// v26: https://www.keycloak.org/docs-api/latest/rest-api/index.html#MembershipType +type MembershipType struct{} + +// MemberRepresentation represents a member of an organization +// v26: https://www.keycloak.org/docs-api/latest/rest-api/index.html#MemberRepresentation +type MemberRepresentation struct { + User + // Type not defined in the Keycloak doc so I left it unexported. Help if you have more information + membershipType MembershipType +} + // GetOrganizationsParams represents the optional parameters for getting organizations type GetOrganizationsParams struct { BriefRepresentation *bool `json:"briefRepresentation,string,omitempty"` @@ -1580,5 +1592,7 @@ func (v *BruteForceStatus) String() string { return pre func (v *GetClientUserSessionsParams) String() string { return prettyStringStruct(v) } func (v *GetOrganizationsParams) String() string { return prettyStringStruct(v) } func (v *InviteeFormParams) String() string { return prettyStringStruct(v) } +func (v *MembershipType) String() string { return prettyStringStruct(v) } +func (v *MemberRepresentation) String() string { return prettyStringStruct(v) } func (v *OrganizationDomainRepresentation) String() string { return prettyStringStruct(v) } func (v *OrganizationRepresentation) String() string { return prettyStringStruct(v) } From a5c69c177d1b330df54db1eca85b67bb2489ec84 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 14:34:59 +0200 Subject: [PATCH 13/18] feature: get user in a specified org by their user ID --- client.go | 19 +++++++++++++++++++ client_test.go | 33 +++++++++++++++++++++++++++++++++ gocloak_iface.go | 4 ++++ 3 files changed, 56 insertions(+) diff --git a/client.go b/client.go index a6ea6f86..01a18262 100644 --- a/client.go +++ b/client.go @@ -4660,3 +4660,22 @@ func (g *GoCloak) GetOrganizationMemberCount(ctx context.Context, token, realm, return result, err } + +// GetOrganizationMemberByID returns the member of the organization with the specified id +// Searches for auser with the given id. If one is found, and is currently a member of the organization, returns it. +// Otherwise,an error response with status NOT_FOUND is returned +func (g *GoCloak) GetOrganizationMemberByID(ctx context.Context, token, realm, idOfOrganization, idOfUser string) (*MemberRepresentation, error) { + const errMessage = "could not get organization member by ID" + + var result *MemberRepresentation + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetResult(&result). + Get(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members", idOfUser)) + + if err := checkForError(resp, err, errMessage); err != nil { + return nil, errors.Wrap(err, errMessage) + } + + return result, err +} diff --git a/client_test.go b/client_test.go index fd7f170f..d41f6966 100644 --- a/client_test.go +++ b/client_test.go @@ -7183,6 +7183,7 @@ func Test_GetOrganizations(t *testing.T) { cfg.GoCloak.Realm, gocloak.GetOrganizationsParams{}) require.NoError(t, err, "GetOrganizations failed") + require.Equal(t, 2, len(organizations)) t.Log(organizations) } @@ -7408,3 +7409,35 @@ func Test_GetOrganizationMemberCount(t *testing.T) { require.Equal(t, 1, count) require.NoError(t, err, "GetOrganizationMemberCount failed") } + +func Test_GetOrganizationMemberByID(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + td, userID := CreateUser(t, client) + defer td() + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + ctx := context.Background() + err := client.AddUserToOrganization( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID) + require.NoError(t, err, "AddUserToOrganization failed") + + member, err := client.GetOrganizationMemberByID( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID) + + require.Equal(t, *member.ID, userID) + require.NoError(t, err, "GetOrganizationMemberByID failed") +} diff --git a/gocloak_iface.go b/gocloak_iface.go index c6facc6b..4b191c93 100644 --- a/gocloak_iface.go +++ b/gocloak_iface.go @@ -603,4 +603,8 @@ type GoCloakIface interface { RemoveUserFromOrganization(ctx context.Context, token, realm, idOfOrganization, idOfUser string) error // GetOrganizationMemberCount returns number of members in the organization. GetOrganizationMemberCount(ctx context.Context, token, realm, idOfOrganization string) (int, error) + // GetOrganizationMemberByID returns the member of the organization with the specified id + // Searches for auser with the given id. If one is found, and is currently a member of the organization, returns it. + // Otherwise,an error response with status NOT_FOUND is returned + GetOrganizationMemberByID(ctx context.Context, token, realm, idOfOrganization, idOfUser string) (*MemberRepresentation, error) } From e96f5e211fe0dc5b5c4fccaba98c191e2f7a653a Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 17:26:46 +0200 Subject: [PATCH 14/18] Add optional params for getting organization members --- models.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/models.go b/models.go index 3848ad20..363fca37 100644 --- a/models.go +++ b/models.go @@ -1452,6 +1452,15 @@ type InviteeFormParams struct { LastName *string `json:"lastname,omitempty"` } +// GetMembersParams represents the optional parameters for getting members of an organization +type GetMembersParams struct { + Exact *bool `json:"exact,string,omitempty"` + First *int `json:"first,string,omitempty"` + Max *int `json:"max,string,omitempty"` + MembershipType *MembershipType `json:"membershipetype,omitempty"` + Search *string `json:"search,omitempty"` +} + // MembershipType represent the membership type of an organization member. // v26: https://www.keycloak.org/docs-api/latest/rest-api/index.html#MembershipType type MembershipType struct{} @@ -1461,7 +1470,7 @@ type MembershipType struct{} type MemberRepresentation struct { User // Type not defined in the Keycloak doc so I left it unexported. Help if you have more information - membershipType MembershipType + MembershipType *MembershipType `json:"membershipetype,omitempty"` } // GetOrganizationsParams represents the optional parameters for getting organizations @@ -1592,6 +1601,7 @@ func (v *BruteForceStatus) String() string { return pre func (v *GetClientUserSessionsParams) String() string { return prettyStringStruct(v) } func (v *GetOrganizationsParams) String() string { return prettyStringStruct(v) } func (v *InviteeFormParams) String() string { return prettyStringStruct(v) } +func (v *GetMembersParams) String() string { return prettyStringStruct(v) } func (v *MembershipType) String() string { return prettyStringStruct(v) } func (v *MemberRepresentation) String() string { return prettyStringStruct(v) } func (v *OrganizationDomainRepresentation) String() string { return prettyStringStruct(v) } From 7dc2ccdcfd27e4d57fb7e64a308cd226b2393cbc Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 17:29:31 +0200 Subject: [PATCH 15/18] Add API to get organization members and member associated organizations --- client.go | 38 +++++++++++++++++++++ client_test.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ gocloak_iface.go | 4 +++ 3 files changed, 130 insertions(+) diff --git a/client.go b/client.go index 01a18262..f6302e58 100644 --- a/client.go +++ b/client.go @@ -4679,3 +4679,41 @@ func (g *GoCloak) GetOrganizationMemberByID(ctx context.Context, token, realm, i return result, err } + +// GetOrganizationMembers returns a paginated list of organization members filtered according to the specified parameters +func (g *GoCloak) GetOrganizationMembers(ctx context.Context, token, realm, idOfOrganization string, params GetMembersParams) ([]*MemberRepresentation, error) { + const errMessage = "could not get organization members" + + queryParams, err := GetQueryParams(params) + if err != nil { + return nil, errors.Wrap(err, errMessage) + } + + var result []*MemberRepresentation + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetResult(&result). + SetQueryParams(queryParams). + Get(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members")) + if err := checkForError(resp, err, errMessage); err != nil { + return nil, errors.Wrap(err, errMessage) + } + + return result, err +} + +// GetMemberAssociatedOrganizations returns the organizations associated with the user that has the specified id +func (g *GoCloak) GetMemberAssociatedOrganizations(ctx context.Context, token, realm, idOfUser string) ([]*OrganizationRepresentation, error) { + const errMessage = "could not get member's associated organizations" + + var result []*OrganizationRepresentation + + resp, err := g.GetRequestWithBearerAuth(ctx, token). + SetResult(&result). + Get(g.getAdminRealmURL(realm, "organizations", "members", idOfUser, "organizations")) + if err := checkForError(resp, err, errMessage); err != nil { + return nil, errors.Wrap(err, errMessage) + } + + return result, err +} diff --git a/client_test.go b/client_test.go index d41f6966..f4df4e59 100644 --- a/client_test.go +++ b/client_test.go @@ -7441,3 +7441,91 @@ func Test_GetOrganizationMemberByID(t *testing.T) { require.Equal(t, *member.ID, userID) require.NoError(t, err, "GetOrganizationMemberByID failed") } + +func Test_GetOrganizationMembers(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + td, userID := CreateUser(t, client) + defer td() + + td2, userID2 := CreateUser(t, client) + defer td2() + + tearDown, orgID := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown() + + ctx := context.Background() + err := client.AddUserToOrganization( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID) + require.NoError(t, err, "AddUserToOrganization failed") + + err = client.AddUserToOrganization( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + userID2) + require.NoError(t, err, "AddUserToOrganization failed") + + members, err := client.GetOrganizationMembers( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID, + gocloak.GetMembersParams{}) + require.NoError(t, err, "GetOrganizationMembers failed") + + fmt.Println(members) + require.GreaterOrEqual(t, len(members), 1) +} + +func Test_GetMemberAssociatedOrganizations(t *testing.T) { + t.Parallel() + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + td, userID := CreateUser(t, client) + defer td() + + // Create two organizations + tearDown1, orgID1 := CreateOrganization(t, client, "Test Inc", "test-inc", "test.com") + defer tearDown1() + + tearDown2, orgID2 := CreateOrganization(t, client, "Another Inc", "another-inc", "another.com") + defer tearDown2() + + // Add user to both organizations + ctx := context.Background() + err := client.AddUserToOrganization( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID1, + userID) + require.NoError(t, err, "AddUserToOrganization failed") + + err = client.AddUserToOrganization( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + orgID2, + userID) + require.NoError(t, err, "AddUserToOrganization failed") + + organizations, err := client.GetMemberAssociatedOrganizations( + ctx, + token.AccessToken, + cfg.GoCloak.Realm, + userID) + require.NoError(t, err, "GetMemberAssociatedOrganizations failed") + + require.GreaterOrEqual(t, len(organizations), 1) +} diff --git a/gocloak_iface.go b/gocloak_iface.go index 4b191c93..f625d7bd 100644 --- a/gocloak_iface.go +++ b/gocloak_iface.go @@ -607,4 +607,8 @@ type GoCloakIface interface { // Searches for auser with the given id. If one is found, and is currently a member of the organization, returns it. // Otherwise,an error response with status NOT_FOUND is returned GetOrganizationMemberByID(ctx context.Context, token, realm, idOfOrganization, idOfUser string) (*MemberRepresentation, error) + // GetOrganizationMembers returns a paginated list of organization members filtered according to the specified parameters + GetOrganizationMembers(ctx context.Context, token, realm, idOfOrganization string, params GetMembersParams) ([]*MemberRepresentation, error) + // GetMemberAssociatedOrganizations returns the organizations associated with the user that has the specified id + GetMemberAssociatedOrganizations(ctx context.Context, token, realm, idOfUser string) ([]*OrganizationRepresentation, error) } From 2eae0a7073913773622888ad502efe121e403478 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 17:41:06 +0200 Subject: [PATCH 16/18] Clean up --- client.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client.go b/client.go index f6302e58..8af6a5fc 100644 --- a/client.go +++ b/client.go @@ -4683,14 +4683,13 @@ func (g *GoCloak) GetOrganizationMemberByID(ctx context.Context, token, realm, i // GetOrganizationMembers returns a paginated list of organization members filtered according to the specified parameters func (g *GoCloak) GetOrganizationMembers(ctx context.Context, token, realm, idOfOrganization string, params GetMembersParams) ([]*MemberRepresentation, error) { const errMessage = "could not get organization members" + var result []*MemberRepresentation queryParams, err := GetQueryParams(params) if err != nil { return nil, errors.Wrap(err, errMessage) } - var result []*MemberRepresentation - resp, err := g.GetRequestWithBearerAuth(ctx, token). SetResult(&result). SetQueryParams(queryParams). From 5c223cada6ea812dbbc0cfea4db203c0fc68f36b Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 31 Dec 2024 18:31:59 +0200 Subject: [PATCH 17/18] Fix codebeat issues --- client.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index 8af6a5fc..e54aad17 100644 --- a/client.go +++ b/client.go @@ -4647,7 +4647,6 @@ func (g *GoCloak) RemoveUserFromOrganization(ctx context.Context, token, realm, // GetOrganizationMemberCount returns number of members in the organization. func (g *GoCloak) GetOrganizationMemberCount(ctx context.Context, token, realm, idOfOrganization string) (int, error) { const errMessage = "could not get organization members count" - var result int resp, err := g.GetRequestWithBearerAuth(ctx, token). @@ -4666,7 +4665,6 @@ func (g *GoCloak) GetOrganizationMemberCount(ctx context.Context, token, realm, // Otherwise,an error response with status NOT_FOUND is returned func (g *GoCloak) GetOrganizationMemberByID(ctx context.Context, token, realm, idOfOrganization, idOfUser string) (*MemberRepresentation, error) { const errMessage = "could not get organization member by ID" - var result *MemberRepresentation resp, err := g.GetRequestWithBearerAuth(ctx, token). @@ -4674,7 +4672,7 @@ func (g *GoCloak) GetOrganizationMemberByID(ctx context.Context, token, realm, i Get(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members", idOfUser)) if err := checkForError(resp, err, errMessage); err != nil { - return nil, errors.Wrap(err, errMessage) + return nil, err } return result, err @@ -4683,8 +4681,8 @@ func (g *GoCloak) GetOrganizationMemberByID(ctx context.Context, token, realm, i // GetOrganizationMembers returns a paginated list of organization members filtered according to the specified parameters func (g *GoCloak) GetOrganizationMembers(ctx context.Context, token, realm, idOfOrganization string, params GetMembersParams) ([]*MemberRepresentation, error) { const errMessage = "could not get organization members" - var result []*MemberRepresentation + var result []*MemberRepresentation queryParams, err := GetQueryParams(params) if err != nil { return nil, errors.Wrap(err, errMessage) @@ -4694,8 +4692,9 @@ func (g *GoCloak) GetOrganizationMembers(ctx context.Context, token, realm, idOf SetResult(&result). SetQueryParams(queryParams). Get(g.getAdminRealmURL(realm, "organizations", idOfOrganization, "members")) + if err := checkForError(resp, err, errMessage); err != nil { - return nil, errors.Wrap(err, errMessage) + return nil, err } return result, err @@ -4704,14 +4703,13 @@ func (g *GoCloak) GetOrganizationMembers(ctx context.Context, token, realm, idOf // GetMemberAssociatedOrganizations returns the organizations associated with the user that has the specified id func (g *GoCloak) GetMemberAssociatedOrganizations(ctx context.Context, token, realm, idOfUser string) ([]*OrganizationRepresentation, error) { const errMessage = "could not get member's associated organizations" - var result []*OrganizationRepresentation resp, err := g.GetRequestWithBearerAuth(ctx, token). SetResult(&result). Get(g.getAdminRealmURL(realm, "organizations", "members", idOfUser, "organizations")) if err := checkForError(resp, err, errMessage); err != nil { - return nil, errors.Wrap(err, errMessage) + return nil, err } return result, err From 772bc7887e8690c37fd866b55e9bbbad05a7c8d5 Mon Sep 17 00:00:00 2001 From: Wilfred Dube Date: Tue, 7 Jan 2025 16:31:36 +0200 Subject: [PATCH 18/18] Added Organizations and OrganizationsEnabled fields to RealmPresentation --- docker-compose.yml | 8 +++++++- models.go | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c467aad6..35f80da6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,4 +19,10 @@ services: 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"] + entrypoint: + [ + "/opt/keycloak/bin/kc.sh", + "start-dev", + "--features=preview", + "--import-realm", + ] diff --git a/models.go b/models.go index 363fca37..8e438d7d 100644 --- a/models.go +++ b/models.go @@ -796,6 +796,8 @@ type RealmRepresentation struct { OfflineSessionIdleTimeout *int `json:"offlineSessionIdleTimeout,omitempty"` OfflineSessionMaxLifespan *int `json:"offlineSessionMaxLifespan,omitempty"` OfflineSessionMaxLifespanEnabled *bool `json:"offlineSessionMaxLifespanEnabled,omitempty"` + OrganizationsEnabled bool `json:"organizationsEnabled,omitempty"` + Organizations *[]interface{} `json:"organizations,omitempty"` OtpPolicyAlgorithm *string `json:"otpPolicyAlgorithm,omitempty"` OtpPolicyDigits *int `json:"otpPolicyDigits,omitempty"` OtpPolicyInitialCounter *int `json:"otpPolicyInitialCounter,omitempty"`