Repository: Nerzal/gocloak
Pull request #28 (2019-03-13 21:41:47)

Added composite roles support


<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/nerzal/gocloak/28)
<!-- Reviewable:end -->



Patch:
diff --git a/README.md b/README.md
index 34da07d..8554c7e 100644
--- a/README.md
+++ b/README.md
@@ -85,15 +85,16 @@ type GoCloak interface {
 	DecodeAccessToken(accessToken string, realm string) (*jwt.Token, *jwt.MapClaims, error)
 	DecodeAccessTokenCustomClaims(accessToken string, realm string, claims jwt.Claims) (*jwt.Token, error)
 	RetrospectToken(accessToken string, clientID, clientSecret string, realm string) (*RetrospecTokenResult, error)
+
 	GetIssuer(realm string) (*IssuerResponse, error)
 	GetCerts(realm string) (*CertResponse, error)
 	GetUserInfo(accessToken string, realm string) (*UserInfo, error)
+
 	SetPassword(token string, userID string, realm string, password string, temporary bool) error
 	ExecuteActionsEmail(token string, realm string, params ExecuteActionsEmail) error
-
 	CreateUser(token string, realm string, user User) (*string, error)
 	CreateGroup(accessToken string, realm string, group Group) error
-	CreateClientRole(accessToken string, realm string, clientID string, role Role) error
+	CreateRole(accessToken string, realm string, clientID string, role Role) error
 	CreateClient(accessToken string, realm string, clientID Client) error
 	CreateClientScope(accessToken string, realm string, scope ClientScope) error
 	CreateComponent(accessToken string, realm string, component Component) error
@@ -107,59 +108,37 @@ type GoCloak interface {
 	DeleteUser(accessToken string, realm, userID string) error
 	DeleteComponent(accessToken string, realm, componentID string) error
 	DeleteGroup(accessToken string, realm, groupID string) error
-	DeleteClientRole(accessToken string, realm, clientID, roleName string) error
+	DeleteRole(accessToken string, realm, clientID, roleName string) error
 	DeleteClient(accessToken string, realm, clientID string) error
 	DeleteClientScope(accessToken string, realm, scopeID string) error
 
-	GetClient(accessToken string, realm string, clientID string) (*Client, error)
-	GetClientSecret(token string, realm string, clientID string) (*CredentialRepresentation, error)
 	GetKeyStoreConfig(accessToken string, realm string) (*KeyStoreConfig, error)
 	GetUserByID(accessToken string, realm string, userID string) (*User, error)
 	GetUserCount(accessToken string, realm string) (int, error)
 	GetUsers(accessToken string, realm string, params GetUsersParams) (*[]User, error)
 	GetUserGroups(accessToken string, realm string, userID string) (*[]UserGroup, error)
 	GetComponents(accessToken string, realm string) (*[]Component, error)
-	GetGroups(accessToken string, realm string, params GetGroupsParams) (*[]Group, error)
-	GetGroup(accessToken string, realm, groupID string) (*Group, error)
-	GetRoleMappingByGroupID(accessToken string, realm string, groupID string) (*MappingsRepresentation, error)
-	GetRoleMappingByUserID(accessToken string, realm string, userID string) (*MappingsRepresentation, error)
-	GetClientRoles(accessToken string, realm string, clientID string) (*[]Role, error)
-	GetClientRole(token string, realm string, clientID string, roleName string) (*Role, error)
-	GetClients(accessToken string, realm string, params GetClientsParams) (*[]Client, error)
-	GetUsersByRoleName(token string, realm string, roleName string) (*[]User, error)
-	UserAttributeContains(attributes map[string][]string, attribute string, value string) bool
 
-	// *** Realm Roles ***
+	UserAttributeContains(attributes map[string][]string, attribute string, value string) bool
 
-	CreateRealmRole(token string, realm string, role Role) error
-	GetRealmRole(token string, realm string, roleName string) (*Role, error)
-	GetRealmRoles(accessToken string, realm string) (*[]Role, error)
+	GetGroups(accessToken string, realm string) (*[]Group, error)
+	GetGroup(accessToken string, realm, groupID string) (*Group, error)
+	GetRoles(accessToken string, realm string) (*[]Role, error)
+	GetRoleMappingByGroupID(accessToken string, realm string, groupID string) (*[]RoleMapping, error)
+	GetRoleMappingByUserID(accessToken string, realm string, userID string) (*[]RoleMapping, error)
+	GetRolesByClientID(accessToken string, realm string, clientID string) (*[]Role, error)
+	GetClients(accessToken string, realm string) (*[]Client, error)
 	GetRealmRolesByUserID(accessToken string, realm string, userID string) (*[]Role, error)
 	GetRealmRolesByGroupID(accessToken string, realm string, groupID string) (*[]Role, error)
-	UpdateRealmRole(token string, realm string, roleName string, role Role) error
-	DeleteRealmRole(token string, realm string, roleName string) error
-	AddRealmRoleToUser(token string, realm string, userID string, roles []Role) error
-	DeleteRealmRoleFromUser(token string, realm string, userID string, roles []Role) error
-
-	// *** Realm ***
-
-	GetRealm(token string, realm string) (*RealmRepresentation, error)
-	CreateRealm(token string, realm RealmRepresentation) error
+	GetUsersByRoleName(token string, realm string, roleName string) (*[]User, error)
+	
+    GetRealm(token string, realm string) (*RealmRepresentation, error)
 }
 ```
 
 ## developing & testing
-For local testing you need to start a docker container. Simply run following commands prior to starting the tests:
+For local testing you needa start a docker container. Simply run following commands prior to starting the tests:
 
-```bash
 docker pull jboss/keycloak
-docker run -d -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=secret -e KEYCLOAK_IMPORT=/tmp/gocloak-realm.json -v `pwd`/testdata/gocloak-realm.json:/tmp/gocloak-realm.json -p 8080:8080 --name keycloak jboss/keycloak
-go test
-```
-
-Or you can run the tests on you own keycloak:
-```bash
-export GOCLOAK_TEST_CONFIG=/path/to/gocloak/config.json
-```
 
-All resources created as a result of unit tests will be deleted, except for the test user defined in the configuration file.
+docker run -d -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=secret -e KEYCLOAK_IMPORT=/tmp/gocloak-realm.json -v `pwd`/testdata/gocloak-realm.json:/tmp/gocloak-realm.json -p 8080:8080 --name keycloak jboss/keycloak
\ No newline at end of file
diff --git a/client.go b/client.go
index 85531f1..8f89881 100644
--- a/client.go
+++ b/client.go
@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"errors"
 	"log"
+	"strconv"
 	"strings"
 	"time"
 
@@ -65,7 +66,7 @@ func checkForError(resp *resty.Response, err error) error {
 
 	if resp.IsError() {
 		if resp.StatusCode() == 409 {
-			return &ObjectAlreadyExists{}
+			return &ObjectAllreadyExists{}
 		}
 		log.Printf("Error: Request returned a response with status '%s' and body: %s", resp.Status(), string(resp.Body()))
 		return errors.New(resp.Status())
@@ -115,8 +116,8 @@ func (client *gocloak) GetUserInfo(accessToken string, realm string) (*UserInfo,
 	resp, err := getRequestWithBearerAuth(accessToken).
 		SetResult(&result).
 		Get(client.getRealmURL(realm, openIDConnect, "userinfo"))
-
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -128,8 +129,8 @@ func (client *gocloak) getNewCerts(realm string) (*CertResponse, error) {
 	resp, err := resty.R().
 		SetResult(&result).
 		Get(client.getRealmURL(realm, openIDConnect, "certs"))
-
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -160,8 +161,8 @@ func (client *gocloak) GetIssuer(realm string) (*IssuerResponse, error) {
 	resp, err := resty.R().
 		SetResult(&result).
 		Get(client.getRealmURL(realm))
-
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -178,8 +179,8 @@ func (client *gocloak) RetrospectToken(accessToken string, clientID, clientSecre
 		}).
 		SetResult(&result).
 		Post(client.getRealmURL(realm, tokenEndpoint, "introspect"))
-
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -234,8 +235,8 @@ func (client *gocloak) RefreshToken(refreshToken string, clientID, clientSecret,
 		}).
 		SetResult(&result).
 		Post(client.getRealmURL(realm, tokenEndpoint))
-
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -259,7 +260,8 @@ func (client *gocloak) LoginAdmin(username, password, realm string) (*JWT, error
 		SetResult(&result).
 		Post(client.getRealmURL(realm, tokenEndpoint))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -281,8 +283,8 @@ func (client *gocloak) LoginClient(clientID, clientSecret, realm string) (*JWT,
 		}).
 		SetResult(&result).
 		Post(client.getRealmURL(realm, tokenEndpoint))
-
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -304,8 +306,8 @@ func (client *gocloak) Login(clientID string, clientSecret string, realm string,
 		}).
 		SetResult(&result).
 		Post(client.getRealmURL(realm, tokenEndpoint))
-
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -340,8 +342,8 @@ func (client *gocloak) RequestPermission(clientID string, clientSecret string, r
 		}).
 		SetResult(&result).
 		Post(client.getRealmURL(realm, tokenEndpoint))
-
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -352,25 +354,35 @@ func (client *gocloak) RequestPermission(clientID string, clientSecret string, r
 	return &result, nil
 }
 
-// SetPassword sets a new password for the user with the given id. Needs elevated privileges
+// SetPassword sets a new password
 func (client *gocloak) SetPassword(token string, userID string, realm string, password string, temporary bool) error {
 	requestBody := SetPasswordRequest{Password: password, Temporary: temporary, Type: "password"}
 	resp, err := getRequestWithBearerAuth(token).
 		SetBody(requestBody).
 		Put(client.getAdminRealmURL(realm, "users", userID, "reset-password"))
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
 
-	return checkForError(resp, err)
+	return nil
 }
 
 // ExecuteActionsEmail executes an actions email
 func (client *gocloak) ExecuteActionsEmail(token string, realm string, params ExecuteActionsEmail) error {
-	queryParams, err := params.GetQueryParams()
-	if err != nil {
-		return err
+	q := map[string]string{}
+	if len(params.ClientID) > 0 {
+		q["client_id"] = params.ClientID
 	}
+	if len(params.RedirectURI) > 0 {
+		q["redirect_uri"] = params.RedirectURI
+	}
+	if params.Lifespan > 0 {
+		q["lifepsan"] = strconv.Itoa(params.Lifespan)
+	}
+
 	resp, err := getRequestWithBearerAuth(token).
-		SetBody(params.Actions).
-		SetQueryParams(queryParams).
+		SetBody(params.Actions).SetQueryParams(q).
 		Put(client.getAdminRealmURL(realm, "users", params.UserID, "execute-actions-email"))
 
 	return checkForError(resp, err)
@@ -382,7 +394,8 @@ func (client *gocloak) CreateUser(token string, realm string, user User) (*strin
 		SetBody(user).
 		Post(client.getAdminRealmURL(realm, "users"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -399,7 +412,12 @@ func (client *gocloak) CreateGroup(token string, realm string, group Group) erro
 		SetBody(group).
 		Post(client.getAdminRealmURL(realm, "groups"))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // CreateComponent creates a new user
@@ -408,7 +426,12 @@ func (client *gocloak) CreateComponent(token string, realm string, component Com
 		SetBody(component).
 		Post(client.getAdminRealmURL(realm, "components"))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // CreateUser creates a new user
@@ -417,7 +440,12 @@ func (client *gocloak) CreateClient(token string, realm string, newClient Client
 		SetBody(newClient).
 		Post(client.getAdminRealmURL(realm, "clients"))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // CreateClientRole creates a new role for a client
@@ -426,7 +454,11 @@ func (client *gocloak) CreateClientRole(token string, realm string, clientID str
 		SetBody(role).
 		Post(client.getAdminRealmURL(realm, "clients", clientID, "roles"))
 
-	return checkForError(resp, err)
+	if err := checkForError(resp, err); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // CreateClientScope creates a new client scope
@@ -435,7 +467,12 @@ func (client *gocloak) CreateClientScope(token string, realm string, scope Clien
 		SetBody(scope).
 		Post(client.getAdminRealmURL(realm, "client-scopes"))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UpdateUser creates a new user
@@ -444,7 +481,12 @@ func (client *gocloak) UpdateUser(token string, realm string, user User) error {
 		SetBody(user).
 		Put(client.getAdminRealmURL(realm, "users", user.ID))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UpdateUser creates a new user
@@ -453,7 +495,12 @@ func (client *gocloak) UpdateGroup(token string, realm string, group Group) erro
 		SetBody(group).
 		Put(client.getAdminRealmURL(realm, "groups", group.ID))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UpdateUser creates a new user
@@ -462,7 +509,12 @@ func (client *gocloak) UpdateClient(token string, realm string, newClient Client
 		SetBody(newClient).
 		Put(client.getAdminRealmURL(realm, "clients"))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UpdateUser creates a new user
@@ -471,7 +523,12 @@ func (client *gocloak) UpdateRole(token string, realm string, clientID string, r
 		SetBody(role).
 		Put(client.getAdminRealmURL(realm, "clients", clientID, "roles", role.Name))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // UpdateClientScope creates a new client scope
@@ -480,7 +537,12 @@ func (client *gocloak) UpdateClientScope(token string, realm string, scope Clien
 		SetBody(scope).
 		Put(client.getAdminRealmURL(realm, "client-scopes", scope.ID))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // DeleteUser creates a new user
@@ -488,7 +550,12 @@ func (client *gocloak) DeleteUser(token string, realm string, userID string) err
 	resp, err := getRequestWithBearerAuth(token).
 		Delete(client.getAdminRealmURL(realm, "users", userID))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // DeleteUser creates a new user
@@ -496,7 +563,12 @@ func (client *gocloak) DeleteGroup(token string, realm string, groupID string) e
 	resp, err := getRequestWithBearerAuth(token).
 		Delete(client.getAdminRealmURL(realm, "groups", groupID))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // DeleteClient deletes a given client
@@ -504,7 +576,12 @@ func (client *gocloak) DeleteClient(token string, realm string, clientID string)
 	resp, err := getRequestWithBearerAuth(token).
 		Delete(client.getAdminRealmURL(realm, "clients", clientID))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // DeleteComponent creates a new user
@@ -512,7 +589,12 @@ func (client *gocloak) DeleteComponent(token string, realm string, componentID s
 	resp, err := getRequestWithBearerAuth(token).
 		Delete(client.getAdminRealmURL(realm, "components", componentID))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // DeleteClientRole deletes a given role
@@ -520,7 +602,11 @@ func (client *gocloak) DeleteClientRole(token string, realm string, clientID, ro
 	resp, err := getRequestWithBearerAuth(token).
 		Delete(client.getAdminRealmURL(realm, "clients", clientID, "roles", roleName))
 
-	return checkForError(resp, err)
+	if err := checkForError(resp, err); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // DeleteClientScope creates a new client scope
@@ -528,7 +614,12 @@ func (client *gocloak) DeleteClientScope(token string, realm string, scopeID str
 	resp, err := getRequestWithBearerAuth(token).
 		Put(client.getAdminRealmURL(realm, "client-scopes", scopeID))
 
-	return checkForError(resp, err)
+	err = checkForError(resp, err)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // GetClient returns a client
@@ -567,14 +658,15 @@ func (client *gocloak) GetKeyStoreConfig(token string, realm string) (*KeyStoreC
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, "keys"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
 	return &result, nil
 }
 
-// GetUserByID fetches a user from the given realm with he given userID
+// GetUserByID fetches a user from the given realm witht he given userID
 func (client *gocloak) GetUserByID(accessToken string, realm string, userID string) (*User, error) {
 	if userID == "" {
 		return nil, errors.New("UserID shall not be empty")
@@ -585,7 +677,8 @@ func (client *gocloak) GetUserByID(accessToken string, realm string, userID stri
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, "users", userID))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -599,7 +692,8 @@ func (client *gocloak) GetComponents(token string, realm string) (*[]Component,
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, "components"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -609,17 +703,39 @@ func (client *gocloak) GetComponents(token string, realm string) (*[]Component,
 // GetUsers get all users in realm
 func (client *gocloak) GetUsers(token string, realm string, params GetUsersParams) (*[]User, error) {
 	var result []User
-	queryParams, err := params.GetQueryParams()
-	if err != nil {
-		return nil, err
+
+	q := map[string]string{}
+	if params.BriefRepresentation != nil {
+		q["briefRepresentation"] = strconv.FormatBool(*params.BriefRepresentation)
+	}
+	if len(params.Email) > 0 {
+		q["email"] = params.Email
+	}
+	if params.First > 0 {
+		q["first"] = strconv.Itoa(params.First)
+	}
+	if len(params.FirstName) > 0 {
+		q["firstName"] = params.FirstName
+	}
+	if len(params.LastName) > 0 {
+		q["lastName"] = params.LastName
+	}
+	if params.Max > 0 {
+		q["max"] = strconv.Itoa(params.Max)
+	}
+	if len(params.Search) > 0 {
+		q["search"] = params.Search
+	}
+	if len(params.Username) > 0 {
+		q["username"] = params.Username
 	}
 
 	resp, err := getRequestWithBearerAuth(token).
-		SetResult(&result).
-		SetQueryParams(queryParams).
+		SetResult(&result).SetQueryParams(q).
 		Get(client.getAdminRealmURL(realm, "users"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -633,7 +749,8 @@ func (client *gocloak) GetUserCount(token string, realm string) (int, error) {
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, "users", "count"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return -1, err
 	}
 
@@ -647,7 +764,8 @@ func (client *gocloak) GetUserGroups(token string, realm string, userID string)
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, "users", userID, "groups"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -660,7 +778,8 @@ func (client *gocloak) getRoleMappings(token string, realm string, path string,
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, path, objectID, "role-mappings"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -684,7 +803,8 @@ func (client *gocloak) GetGroup(token string, realm string, groupID string) (*Gr
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, "group", groupID))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -694,17 +814,23 @@ func (client *gocloak) GetGroup(token string, realm string, groupID string) (*Gr
 // GetGroups get all groups in realm
 func (client *gocloak) GetGroups(token string, realm string, params GetGroupsParams) (*[]Group, error) {
 	var result []Group
-	queryParams, err := params.GetQueryParams()
-	if err != nil {
-		return nil, err
+	q := map[string]string{}
+	if params.First > 0 {
+		q["first"] = strconv.Itoa(params.First)
+	}
+	if params.Max > 0 {
+		q["max"] = strconv.Itoa(params.Max)
+	}
+	if len(params.Search) > 0 {
+		q["search"] = params.Search
 	}
 
 	resp, err := getRequestWithBearerAuth(token).
-		SetResult(&result).
-		SetQueryParams(queryParams).
+		SetResult(&result).SetQueryParams(q).
 		Get(client.getAdminRealmURL(realm, "groups"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -742,16 +868,19 @@ func (client *gocloak) GetClientRole(token string, realm string, clientID string
 // GetClients gets all clients in realm
 func (client *gocloak) GetClients(token string, realm string, params GetClientsParams) (*[]Client, error) {
 	var result []Client
-	queryParams, err := params.GetQueryParams()
-	if err != nil {
-		return nil, err
+	q := map[string]string{}
+	if params.ViewableOnly {
+		q["viewableOnly"] = strconv.FormatBool(params.ViewableOnly)
+	}
+	if len(params.ClientID) != 0 {
+		q["clientId"] = params.ClientID
 	}
 	resp, err := getRequestWithBearerAuth(token).
-		SetResult(&result).
-		SetQueryParams(queryParams).
+		SetResult(&result).SetQueryParams(q).
 		Get(client.getAdminRealmURL(realm, "clients"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -777,7 +906,8 @@ func (client *gocloak) GetUsersByRoleName(token string, realm string, roleName s
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, "roles", roleName, "users"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -818,7 +948,8 @@ func (client *gocloak) GetRealmRoles(token string, realm string) (*[]Role, error
 		SetResult(&result).
 		Get(client.getAdminRealmURL(realm, "roles"))
 
-	if err := checkForError(resp, err); err != nil {
+	err = checkForError(resp, err)
+	if err != nil {
 		return nil, err
 	}
 
@@ -887,6 +1018,22 @@ func (client *gocloak) DeleteRealmRoleFromUser(token string, realm string, userI
 	return checkForError(resp, err)
 }
 
+func (client *gocloak) AddRealmRoleComposite(token string, realm string, roleName string, roles []Role) error {
+	resp, err := getRequestWithBearerAuth(token).
+		SetBody(roles).
+		Post(client.getAdminRealmURL(realm, "roles", roleName, "composites"))
+
+	return checkForError(resp, err)
+}
+
+func (client *gocloak) DeleteRealmRoleComposite(token string, realm string, roleName string, roles []Role) error {
+	resp, err := getRequestWithBearerAuth(token).
+		SetBody(roles).
+		Delete(client.getAdminRealmURL(realm, "roles", roleName, "composites"))
+
+	return checkForError(resp, err)
+}
+
 // -----
 // Realm
 // -----
diff --git a/errors.go b/errors.go
index a12b2e9..65f971a 100644
--- a/errors.go
+++ b/errors.go
@@ -1,8 +1,8 @@
 package gocloak
 
 // ObjectAllreadyExists is used when keycloak answers with 409
-type ObjectAlreadyExists struct{}
+type ObjectAllreadyExists struct{}
 
-func (o *ObjectAlreadyExists) Error() string {
-	return "Conflict: Object already exists"
+func (o *ObjectAllreadyExists) Error() string {
+	return "Conflict: Object allready exists"
 }
diff --git a/gocloak.go b/gocloak.go
index a9ec60d..57af4e7 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -4,7 +4,7 @@ import (
 	"github.com/dgrijalva/jwt-go"
 )
 
-// GoCloak holds all methods a client should fulfill
+// GoCloak holds all methods a client should fullfill
 type GoCloak interface {
 	// Login sends a request to the token endpoint using user and client credentials
 	Login(clientID string, clientSecret string, realm string, username string, password string) (*JWT, error)
@@ -31,7 +31,7 @@ type GoCloak interface {
 	// GetUserInfo gets the user info for the given realm
 	GetUserInfo(accessToken string, realm string) (*UserInfo, error)
 
-	// SetPassword sets a new password for the user with the given id. Needs elevated privileges
+	// SetPassword sets a new password for the user with the given id. Needs elevated priviliges
 	SetPassword(token string, userID string, realm string, password string, temporary bool) error
 
 	// ExecuteActionsEmail executes an actions email
@@ -130,6 +130,10 @@ type GoCloak interface {
 	AddRealmRoleToUser(token string, realm string, userID string, roles []Role) error
 	// DeleteRealmRoleFromUser deletes realm-level role mappings
 	DeleteRealmRoleFromUser(token string, realm string, userID string, roles []Role) error
+	// AddRealmRoleComposite adds roles as composite
+	AddRealmRoleComposite(token string, realm string, roleName string, roles []Role) error
+	// AddRealmRoleComposite adds roles as composite
+	DeleteRealmRoleComposite(token string, realm string, roleName string, roles []Role) error
 
 	// *** Realm ***
 
diff --git a/models.go b/models.go
index 543b787..6ea02a8 100644
--- a/models.go
+++ b/models.go
@@ -1,27 +1,5 @@
 package gocloak
 
-import "encoding/json"
-
-// BaseParams provides basic functionality for all QueryParams structures.
-// The fields tags must have `json:"<name>,string,omitempty"` format.
-// "string" tag allows to convert the structure to map[string]string.
-// "omitempty" allows to skip the fields with default values.
-type BaseParams struct{}
-
-// GetQueryParams converts the struct to map[string]string
-func (s BaseParams) GetQueryParams() (map[string]string, error) {
-	b, err := json.Marshal(s)
-	if err != nil {
-		return nil, err
-	}
-	var res map[string]string
-	err = json.Unmarshal(b, &res)
-	if err != nil {
-		return nil, err
-	}
-	return res, nil
-}
-
 // APIError represents an api error
 type APIError struct {
 	Code    int
@@ -161,25 +139,23 @@ type UserGroup struct {
 
 // GetUsersParams represents the optional parameters for getting users
 type GetUsersParams struct {
-	BaseParams
-	BriefRepresentation *bool  `json:"briefRepresentation,string,omitempty"`
-	Email               string `json:"email,string,omitempty"`
-	First               int    `json:"first,string,omitempty"`
-	FirstName           string `json:"firstName,string,omitempty"`
-	LastName            string `json:"lastName,string,omitempty"`
-	Max                 int    `json:"max,string,omitempty"`
-	Search              string `json:"search,string,omitempty"`
-	Username            string `json:"username,string,omitempty"`
+	BriefRepresentation *bool  `query:"briefRepresentation,omitempty"`
+	Email               string `query:"email,omitempty"`
+	First               int    `query:"first,omitempty"`
+	FirstName           string `query:"firstName,omitempty"`
+	LastName            string `query:"lastName,omitempty"`
+	Max                 int    `query:"max,omitempty"`
+	Search              string `query:"search,omitempty"`
+	Username            string `query:"username,omitempty"`
 }
 
 // ExecuteActionsEmail represents parameters for executing action emails
 type ExecuteActionsEmail struct {
-	BaseParams
-	UserID      string   `json:"-"`
-	ClientID    string   `json:"clientId,string,omitempty"`
-	Lifespan    int      `json:"lifespan,string,omitempty"`
-	RedirectURI string   `json:"redirect_uri,string,omitempty"`
-	Actions     []string `json:"-"`
+	UserID      string   `path:"userId"`
+	ClientID    string   `query:"clientId,omitempty"`
+	Lifespan    int      `query:"lifespan,omitempty"`
+	RedirectURI string   `query:"redirect_uri,omitempty"`
+	Actions     []string `body:"actions"`
 }
 
 // Group is a Group
@@ -192,22 +168,21 @@ type Group struct {
 
 // GetGroupsParams represents the optional parameters for getting groups
 type GetGroupsParams struct {
-	BaseParams
-	First  int    `json:"first,string,omitempty"`
-	Max    int    `json:"max,string,omitempty"`
-	Search string `json:"search,string,omitempty"`
+	First  int    `query:"first,omitempty"`
+	Max    int    `query:"max,omitempty"`
+	Search string `query:"search,omitempty"`
 }
 
 // Role is a role
 type Role struct {
-	ID                 string            `json:"id,omitempty"`
-	Name               string            `json:"name,omitempty"`
-	ScopeParamRequired bool              `json:"scopeParamRequired,omitempty"`
-	Composite          bool              `json:"composite,omitempty"`
-	ClientRole         bool              `json:"clientRole,omitempty"`
-	ContainerID        string            `json:"containerId,omitempty"`
-	Description        string            `json:"description,omitempty"`
-	Attributes         map[string]string `json:"attributes,omitempty"`
+	ID                 string              `json:"id,omitempty"`
+	Name               string              `json:"name,omitempty"`
+	ScopeParamRequired bool                `json:"scopeParamRequired,omitempty"`
+	Composite          bool                `json:"composite,omitempty"`
+	ClientRole         bool                `json:"clientRole,omitempty"`
+	ContainerID        string              `json:"containerId,omitempty"`
+	Description        string              `json:"description,omitempty"`
+	Attributes         map[string][]string `json:"attributes,omitempty"`
 }
 
 // ClientMappingsRepresentation is a client role mappings
@@ -267,9 +242,8 @@ type Client struct {
 
 // GetClientsParams represents the query parameters
 type GetClientsParams struct {
-	BaseParams
-	ClientID     string `json:"clientId,string,omitempty"`
-	ViewableOnly bool   `json:"viewableOnly,string,omitempty"`
+	ClientID     string `json:"clientId,omitempty"`
+	ViewableOnly bool   `json:"viewableOnly,omitempty"`
 }
 
 // UserInfo is returned by the userinfo endpoint
diff --git a/pkg/jwx/models.go b/pkg/jwx/models.go
index 1f0e5b9..d844f6b 100644
--- a/pkg/jwx/models.go
+++ b/pkg/jwx/models.go
@@ -42,18 +42,15 @@ type RealmAccess struct {
 	Roles []string `json:"roles,omitempty"`
 }
 
-// ResourceAccess holds TODO: What does it hold?
 type ResourceAccess struct {
 	RealmManagement RealmManagement `json:"realm-management,omitempty"`
 	Account         Account         `json:"account,omitempty"`
 }
 
-// RealmManagement holds TODO: What does it hold?
 type RealmManagement struct {
 	Roles []string `json:"roles,omitempty"`
 }
 
-// Account holds TODO: What does it hold?
 type Account struct {
 	Roles []string `json:"roles,omitempty"`
 }


Test patch:
diff --git a/client_test.go b/client_test.go
index 01e9e31..9b6b1c3 100644
--- a/client_test.go
+++ b/client_test.go
@@ -66,11 +66,7 @@ func FailIf(t *testing.T, cond bool, msg string, args ...interface{}) {
 
 func GetConfig(t *testing.T) *Config {
 	configOnce.Do(func() {
-		configFileName, ok := os.LookupEnv("GOCLOAK_TEST_CONFIG")
-		if !ok {
-			configFileName = filepath.Join("testdata", "config.json")
-		}
-		configFile, err := os.Open(configFileName)
+		configFile, err := os.Open(filepath.Join("testdata", "config.json"))
 		FailIfErr(t, err, "cannot open config.json")
 		defer configFile.Close()
 		data, err := ioutil.ReadAll(configFile)
@@ -231,7 +227,7 @@ func SetUpTestUser(t *testing.T, client GoCloak) {
 			cfg.GoCloak.Realm,
 			user)
 		var userID string
-		if err != nil && err.Error() == "Conflict: Object already exists" {
+		if err != nil && err.Error() == "Conflict: Object allready exists" {
 			err = nil
 			users, err := client.GetUsers(
 				token.AccessToken,
@@ -851,7 +847,6 @@ func TestGocloak_UpdateRealmRole(t *testing.T) {
 		Role{
 			Name: newRoleName,
 		})
-	FailIfErr(t, err, "UpdateRealmRole failed")
 	err = client.DeleteRealmRole(
 		token.AccessToken,
 		cfg.GoCloak.Realm,
@@ -859,7 +854,7 @@ func TestGocloak_UpdateRealmRole(t *testing.T) {
 	FailIf(
 		t,
 		err == nil,
-		"Role with old name was deleted successfully, but it shouldn't. Old role: %s; Updated role: %s",
+		"Role with old name was deleted sucessfully, but it shouldn't. Old role: %s; Updated role: %s",
 		oldRoleName, newRoleName)
 	err = client.DeleteRealmRole(
 		token.AccessToken,
@@ -995,7 +990,6 @@ func TestGocloak_DeleteRealmRoleFromUser(t *testing.T) {
 		[]Role{
 			*role,
 		})
-	FailIfErr(t, err, "DeleteRealmRoleFromUser failed")
 
 	roles, err := client.GetRealmRolesByUserID(
 		token.AccessToken,
@@ -1009,3 +1003,47 @@ func TestGocloak_DeleteRealmRoleFromUser(t *testing.T) {
 			"The role has been found in asigned roles. Role: %+v", role)
 	}
 }
+
+func TestGocloak_AddRealmRoleComposite(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClient(cfg.HostName)
+	token := GetAdminToken(t, client)
+
+	tearDown, compositeRole := CreateRealmRole(t, client)
+	defer tearDown()
+
+	tearDown, role := CreateRealmRole(t, client)
+	defer tearDown()
+
+	roleModel, err := client.GetRealmRole(token.AccessToken, cfg.GoCloak.Realm, role)
+	FailIfErr(t, err, "Can't get just created role with GetRealmRole")
+
+	err = client.AddRealmRoleComposite(token.AccessToken,
+		cfg.GoCloak.Realm, compositeRole, []Role{*roleModel})
+	FailIfErr(t, err, "AddRealmRoleComposite failed")
+}
+
+func TestGocloak_DeleteRealmRoleComposite(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClient(cfg.HostName)
+	token := GetAdminToken(t, client)
+
+	tearDown, compositeRole := CreateRealmRole(t, client)
+	defer tearDown()
+
+	tearDown, role := CreateRealmRole(t, client)
+	defer tearDown()
+
+	roleModel, err := client.GetRealmRole(token.AccessToken, cfg.GoCloak.Realm, role)
+	FailIfErr(t, err, "Can't get just created role with GetRealmRole")
+
+	err = client.AddRealmRoleComposite(token.AccessToken,
+		cfg.GoCloak.Realm, compositeRole, []Role{*roleModel})
+	FailIfErr(t, err, "AddRealmRoleComposite failed")
+
+	err = client.DeleteRealmRoleComposite(token.AccessToken,
+		cfg.GoCloak.Realm, compositeRole, []Role{*roleModel})
+	FailIfErr(t, err, "DeleteRealmRoleComposite failed")
+}