Repository: Nerzal/gocloak
Pull request #233 (2020-12-01 03:47:22)

Client resources and user permissions
This is a continuation of work to add non-admin methods for resources/permissions (as mentioned in issue #229)
- add the methods for creating and deleting resources by a client (ClientCreateResource, ClientDeleteResource). These complement the methods GetResourceClient, GetResourcesClient, and UpdateResourceClient which were added in PR#230.
- add methods for a client (resource server typically) to create permission ticket, as well as grant, update, get, and delete user permissions for specific resources (CreatePermissionTicket, GrantUserPermission, UpdateUserPermission, GetUserPermissions, DeleteUserPermission).

The new permissions methods are not an exact parallel to the existing admin versions. There is no obvious exact parallel between what the admin user can do with the permission methods already implemented, and what is available to resource servers via client API so I have implemented methods to offer the functionality described in the Keycloak documentation in [Authorization Services - Managing Permission Requests](https://www.keycloak.org/docs/latest/authorization_services/#_service_protection_permission_api_papi). For naming, I've used the format ```<Verb>UserPermission``` for these because they relate to users more than they do clients. I'm happy to revisit the naming of these to better suit the overall scheme. Creating a permission ticket is not granting permission to a user, ([it's an input to the RPT](https://www.keycloak.org/docs/latest/authorization_services/#_overview_terminology_permission_ticket)) so I just named it CreatePermissionTicket.

Other smaller points:

- I've added new types in ```model.go``` to support these methods, because the keycloak API has small differences in the query parameters between the API used for these newly implemented methods, and the admin API for previously implemented methods in gocloak. It would be messy to try and force these slightly differently-named fields into pre-existing models and break compartmentalisation. 

- I've left the return of the CreatePermissionsTicket as a pointer to a struct because it might be possible to get a description of the ticket from keycloak which could be returned as well, and then it would be similar to the way id token, access token, and some descriptive fields are returned when you log in.

- All methods have tests (passing). In the testing, I'm in some cases decoding the returned JWT without checking the signature because trust is not an issue - just want to confirm that the right kind of response has come back (else any valid request could pass some of the tests). I don't _think_ this will make the tests too brittle, because they shouldn't break until there are major breaking changes in the keycloak API (which would need attention anyway). 

- UpdateUserPermission adds an explicit check for 204 (StatusNoContent) so it can return a nil pointer if the update has removed the permission - this makes it easier to interpret the result than having a non-nil pointer to a struct full of nil-pointers.

- Sorry - I moved some code from my previous PR around to organise it a bit better and it has made the diff logs a bit noisy on this commit because it looks at first glance like I have messed with the pre-existing  ```<verb>Resource``` methods - I haven't.

[Issue: #229]


Patch:
diff --git a/README.md b/README.md
index 131161c..c3f199f 100644
--- a/README.md
+++ b/README.md
@@ -104,7 +104,7 @@ go get github.com/Nerzal/gocloak/v7
 ## Features
 
 ```go
-// GoCloak holds all methods a client should fullfill
+// GoCloak holds all methods a client should fulfill
 type GoCloak interface {
 	GetRequestingPartyToken(ctx context.Context, token, realm string, options RequestingPartyTokenOptions) (*JWT, error)
 	GetRequestingPartyPermissions(ctx context.Context, token, realm string, options RequestingPartyTokenOptions) (*[]RequestingPartyPermission, error)
@@ -257,6 +257,12 @@ type GoCloak interface {
 	CreateResource(ctx context.Context, token, realm, clientID string, resource Resource) (*Resource, error)
 	UpdateResource(ctx context.Context, token, realm, clientID string, resource Resource) error
 	DeleteResource(ctx context.Context, token, realm, clientID, resourceID string) error
+	
+	GetResourceClient(ctx context.Context, token, realm, resourceID string) (*ResourceRepresentation, error)
+	GetResourcesClient(ctx context.Context, token, realm string, params GetResourceParams) ([]*ResourceRepresentation, error)
+	CreateResourceClient(ctx context.Context, token, realm string, resource ResourceRepresentation) (*ResourceRepresentation, error)
+	UpdateResourceClient(ctx context.Context, token, realm string, resource ResourceRepresentation) error
+	DeleteResourceClient(ctx context.Context, token, realm, resourceID string) error
 
 	GetScope(ctx context.Context, token, realm, clientID, scopeID string) (*ScopeRepresentation, error)
 	GetScopes(ctx context.Context, token, realm, clientID string, params GetScopeParams) ([]*ScopeRepresentation, error)
@@ -275,7 +281,13 @@ type GoCloak interface {
 	CreatePermission(ctx context.Context, token, realm, clientID string, permission PermissionRepresentation) (*PermissionRepresentation, error)
 	UpdatePermission(ctx context.Context, token, realm, clientID string, permission PermissionRepresentation) error
 	DeletePermission(ctx context.Context, token, realm, clientID, permissionID string) error
-
+	
+	CreatePermissionTicket(ctx context.Context, token, realm string, permissions []CreatePermissionTicketParams) (*PermissionTicketResponseRepresentation, error)
+	GrantUserPermission(ctx context.Context, token, realm string, permission PermissionGrantParams) (*PermissionGrantResponseRepresentation, error)
+	UpdateUserPermission(ctx context.Context, token, realm string, permission PermissionGrantParams) (*PermissionGrantResponseRepresentation, error)
+	GetUserPermissions(ctx context.Context, token, realm string, params GetUserPermissionParams) ([]*PermissionGrantResponseRepresentation, error)
+	DeleteUserPermission(ctx context.Context, token, realm, ticketID string) error
+	
 	// *** Credentials API ***
 
 	GetCredentialRegistrators(ctx context.Context, token, realm string) ([]string, error)
diff --git a/client.go b/client.go
index 97f8f2b..b40827e 100644
--- a/client.go
+++ b/client.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/base64"
 	"fmt"
+	"net/http"
 	"net/url"
 	"strings"
 	"sync"
@@ -48,6 +49,13 @@ func (client *gocloak) getRequest(ctx context.Context) *resty.Request {
 		SetError(&err)
 }
 
+func (client *gocloak) getRequestWithBearerAuthNoCache(ctx context.Context, token string) *resty.Request {
+	return client.getRequest(ctx).
+		SetAuthToken(token).
+		SetHeader("Content-Type", "application/json").
+		SetHeader("Cache-Control", "no-cache")
+}
+
 func (client *gocloak) getRequestWithBearerAuth(ctx context.Context, token string) *resty.Request {
 	return client.getRequest(ctx).
 		SetAuthToken(token).
@@ -2291,7 +2299,11 @@ func (client *gocloak) DeleteIdentityProvider(ctx context.Context, token, realm,
 	return checkForError(resp, err, errMessage)
 }
 
-// GetResource returns a client's resource with the given id
+// ------------------
+// Protection API
+// ------------------
+
+// GetResource returns a client's resource with the given id, using access token from admin
 func (client *gocloak) GetResource(ctx context.Context, token, realm, clientID, resourceID string) (*ResourceRepresentation, error) {
 	const errMessage = "could not get resource"
 
@@ -2307,8 +2319,8 @@ func (client *gocloak) GetResource(ctx context.Context, token, realm, clientID,
 	return &result, nil
 }
 
-// GetResource returns a client's resource with the given id
-func (client *gocloak) GetResourceClient(ctx context.Context, token, realm, clientID, resourceID string) (*ResourceRepresentation, error) {
+// GetResource returns a client's resource with the given id, using access token from client
+func (client *gocloak) GetResourceClient(ctx context.Context, token, realm, resourceID string) (*ResourceRepresentation, error) {
 	const errMessage = "could not get resource"
 
 	var result ResourceRepresentation
@@ -2325,8 +2337,30 @@ func (client *gocloak) GetResourceClient(ctx context.Context, token, realm, clie
 	return &result, nil
 }
 
-// GetResources returns resources associated with the client
-func (client *gocloak) GetResourcesClient(ctx context.Context, token, realm, clientID string, params GetResourceParams) ([]*ResourceRepresentation, error) {
+// GetResources returns resources associated with the client, using access token from admin
+func (client *gocloak) GetResources(ctx context.Context, token, realm, clientID string, params GetResourceParams) ([]*ResourceRepresentation, error) {
+	const errMessage = "could not get resources"
+
+	queryParams, err := GetQueryParams(params)
+	if err != nil {
+		return nil, err
+	}
+
+	var result []*ResourceRepresentation
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		SetQueryParams(queryParams).
+		Get(client.getAdminRealmURL(realm, "clients", clientID, "authz", "resource-server", "resource"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// GetResources returns resources associated with the client, using access token from client
+func (client *gocloak) GetResourcesClient(ctx context.Context, token, realm string, params GetResourceParams) ([]*ResourceRepresentation, error) {
 	const errMessage = "could not get resources"
 
 	queryParams, err := GetQueryParams(params)
@@ -2346,7 +2380,7 @@ func (client *gocloak) GetResourcesClient(ctx context.Context, token, realm, cli
 	}
 
 	for _, resourceID := range resourceIDs {
-		resource, err := client.GetResourceClient(ctx, token, realm, clientID, resourceID)
+		resource, err := client.GetResourceClient(ctx, token, realm, resourceID)
 		if err == nil {
 			result = append(result, resource)
 		}
@@ -2355,8 +2389,8 @@ func (client *gocloak) GetResourcesClient(ctx context.Context, token, realm, cli
 	return result, nil
 }
 
-// UpdateResource updates a resource associated with the client
-func (client *gocloak) UpdateResourceClient(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) error {
+// UpdateResource updates a resource associated with the client, using access token from admin
+func (client *gocloak) UpdateResource(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) error {
 	const errMessage = "could not update resource"
 
 	if NilOrEmpty(resource.ID) {
@@ -2365,42 +2399,52 @@ func (client *gocloak) UpdateResourceClient(ctx context.Context, token, realm, c
 
 	resp, err := client.getRequestWithBearerAuth(ctx, token).
 		SetBody(resource).
-		Put(client.getRealmURL(realm, "authz", "protection", "resource_set", *(resource.ID)))
+		Put(client.getAdminRealmURL(realm, "clients", clientID, "authz", "resource-server", "resource", *(resource.ID)))
 
 	return checkForError(resp, err, errMessage)
 }
 
-// GetResources returns resources associated with the client
-func (client *gocloak) GetResources(ctx context.Context, token, realm, clientID string, params GetResourceParams) ([]*ResourceRepresentation, error) {
-	const errMessage = "could not get resources"
+// UpdateResource updates a resource associated with the client, using access token from client
+func (client *gocloak) UpdateResourceClient(ctx context.Context, token, realm string, resource ResourceRepresentation) error {
+	const errMessage = "could not update resource"
 
-	queryParams, err := GetQueryParams(params)
-	if err != nil {
-		return nil, err
+	if NilOrEmpty(resource.ID) {
+		return errors.New("ID of a resource required")
 	}
 
-	var result []*ResourceRepresentation
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetBody(resource).
+		Put(client.getRealmURL(realm, "authz", "protection", "resource_set", *(resource.ID)))
+
+	return checkForError(resp, err, errMessage)
+}
+
+// CreateResource creates a resource associated with the client, using access token from admin
+func (client *gocloak) CreateResource(ctx context.Context, token, realm string, clientID string, resource ResourceRepresentation) (*ResourceRepresentation, error) {
+	const errMessage = "could not create resource"
+
+	var result ResourceRepresentation
 	resp, err := client.getRequestWithBearerAuth(ctx, token).
 		SetResult(&result).
-		SetQueryParams(queryParams).
-		Get(client.getAdminRealmURL(realm, "clients", clientID, "authz", "resource-server", "resource"))
+		SetBody(resource).
+		Post(client.getAdminRealmURL(realm, "clients", clientID, "authz", "resource-server", "resource"))
 
 	if err := checkForError(resp, err, errMessage); err != nil {
 		return nil, err
 	}
 
-	return result, nil
+	return &result, nil
 }
 
-// CreateResource creates a resource associated with the client
-func (client *gocloak) CreateResource(ctx context.Context, token, realm string, clientID string, resource ResourceRepresentation) (*ResourceRepresentation, error) {
+// CreateResource creates a resource associated with the client, using access token from client
+func (client *gocloak) CreateResourceClient(ctx context.Context, token, realm string, resource ResourceRepresentation) (*ResourceRepresentation, error) {
 	const errMessage = "could not create resource"
 
 	var result ResourceRepresentation
 	resp, err := client.getRequestWithBearerAuth(ctx, token).
 		SetResult(&result).
 		SetBody(resource).
-		Post(client.getAdminRealmURL(realm, "clients", clientID, "authz", "resource-server", "resource"))
+		Post(client.getRealmURL(realm, "authz", "protection", "resource_set"))
 
 	if err := checkForError(resp, err, errMessage); err != nil {
 		return nil, err
@@ -2409,27 +2453,22 @@ func (client *gocloak) CreateResource(ctx context.Context, token, realm string,
 	return &result, nil
 }
 
-// UpdateResource updates a resource associated with the client
-func (client *gocloak) UpdateResource(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) error {
-	const errMessage = "could not update resource"
-
-	if NilOrEmpty(resource.ID) {
-		return errors.New("ID of a resource required")
-	}
+// DeleteResource deletes a resource associated with the client (using an admin token)
+func (client *gocloak) DeleteResource(ctx context.Context, token, realm, clientID, resourceID string) error {
+	const errMessage = "could not delete resource"
 
 	resp, err := client.getRequestWithBearerAuth(ctx, token).
-		SetBody(resource).
-		Put(client.getAdminRealmURL(realm, "clients", clientID, "authz", "resource-server", "resource", *(resource.ID)))
+		Delete(client.getAdminRealmURL(realm, "clients", clientID, "authz", "resource-server", "resource", resourceID))
 
 	return checkForError(resp, err, errMessage)
 }
 
-// DeleteResource deletes a resource associated with the client
-func (client *gocloak) DeleteResource(ctx context.Context, token, realm, clientID, resourceID string) error {
+// DeleteResource deletes a resource associated with the client (using a client token)
+func (client *gocloak) DeleteResourceClient(ctx context.Context, token, realm, resourceID string) error {
 	const errMessage = "could not delete resource"
 
 	resp, err := client.getRequestWithBearerAuth(ctx, token).
-		Delete(client.getAdminRealmURL(realm, "clients", clientID, "authz", "resource-server", "resource", resourceID))
+		Delete(client.getRealmURL(realm, "authz", "protection", "resource_set", resourceID))
 
 	return checkForError(resp, err, errMessage)
 }
@@ -2602,6 +2641,82 @@ func (client *gocloak) DeletePolicy(ctx context.Context, token, realm, clientID,
 	return checkForError(resp, err, errMessage)
 }
 
+// GetResourcePolicy updates a permission for a specifc resource, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+func (client *gocloak) GetResourcePolicy(ctx context.Context, token, realm, permissionID string) (*ResourcePolicyRepresentation, error) {
+	const errMessage = "could not get resource policy"
+
+	var result ResourcePolicyRepresentation
+	resp, err := client.getRequestWithBearerAuthNoCache(ctx, token).
+		SetResult(&result).
+		Get(client.getRealmURL(realm, "authz", "protection", "uma-policy", permissionID))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// GetResourcePolicies returns resources associated with the client, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+func (client *gocloak) GetResourcePolicies(ctx context.Context, token, realm string, params GetResourcePoliciesParams) ([]*ResourcePolicyRepresentation, error) {
+	const errMessage = "could not get resource policies"
+
+	queryParams, err := GetQueryParams(params)
+	if err != nil {
+		return nil, err
+	}
+
+	var result []*ResourcePolicyRepresentation
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		SetQueryParams(queryParams).
+		Get(client.getRealmURL(realm, "authz", "protection", "uma-policy"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// CreateResourcePolicy associates a permission with a specifc resource, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+func (client *gocloak) CreateResourcePolicy(ctx context.Context, token, realm, resourceID string, policy ResourcePolicyRepresentation) (*ResourcePolicyRepresentation, error) {
+	const errMessage = "could not create resource policy"
+
+	var result ResourcePolicyRepresentation
+	resp, err := client.getRequestWithBearerAuthNoCache(ctx, token).
+		SetResult(&result).
+		SetBody(policy).
+		Post(client.getRealmURL(realm, "authz", "protection", "uma-policy", resourceID))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// UpdateResourcePolicy updates a permission for a specifc resource, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+func (client *gocloak) UpdateResourcePolicy(ctx context.Context, token, realm, permissionID string, policy ResourcePolicyRepresentation) error {
+	const errMessage = "could not update resource policy"
+
+	resp, err := client.getRequestWithBearerAuthNoCache(ctx, token).
+		SetBody(policy).
+		Put(client.getRealmURL(realm, "authz", "protection", "uma-policy", permissionID))
+
+	return checkForError(resp, err, errMessage)
+}
+
+// DeleteResourcePolicy deletes a permission for a specifc resource, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+func (client *gocloak) DeleteResourcePolicy(ctx context.Context, token, realm, permissionID string) error {
+	const errMessage = "could not  delete resource policy"
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		Delete(client.getRealmURL(realm, "authz", "protection", "uma-policy", permissionID))
+
+	return checkForError(resp, err, errMessage)
+}
+
 // GetPermission returns a client's permission with the given id
 func (client *gocloak) GetPermission(ctx context.Context, token, realm, clientID, permissionID string) (*PermissionRepresentation, error) {
 	const errMessage = "could not get permission"
@@ -2693,6 +2808,168 @@ func (client *gocloak) GetPermissions(ctx context.Context, token, realm, clientI
 	return result, nil
 }
 
+// checkPermissionTicketParams checks that mandatory fields are present
+func checkPermissionTicketParams(permissions []CreatePermissionTicketParams) error {
+
+	if len(permissions) == 0 {
+		return errors.New("at least one permission ticket must be requested")
+	}
+
+	for _, pt := range permissions {
+
+		if NilOrEmpty(pt.ResourceID) {
+			return errors.New("resourceID required for permission ticket")
+		}
+		if NilOrEmptyArray(pt.ResourceScopes) {
+			return errors.New("at least one resourceScope required for permission ticket")
+		}
+	}
+
+	return nil
+
+}
+
+// CreatePermissionTicket creates a permission ticket, using access token from client
+func (client *gocloak) CreatePermissionTicket(ctx context.Context, token, realm string, permissions []CreatePermissionTicketParams) (*PermissionTicketResponseRepresentation, error) {
+	const errMessage = "could not create permission ticket"
+
+	err := checkPermissionTicketParams(permissions)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var result PermissionTicketResponseRepresentation
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		SetBody(permissions).
+		Post(client.getRealmURL(realm, "authz", "protection", "permission"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// checkPermissionGrantParams checks for mandatory fields
+func checkPermissionGrantParams(permission PermissionGrantParams) error {
+
+	if NilOrEmpty(permission.RequesterID) {
+		return errors.New("requesterID required to grant user permission")
+	}
+	if NilOrEmpty(permission.ResourceID) {
+		return errors.New("resourceID required to grant user permission")
+	}
+	if NilOrEmpty(permission.ScopeName) {
+		return errors.New("scopeName required to grant user permission")
+	}
+
+	return nil
+}
+
+// GrantPermission lets resource owner grant permission for specific resource ID to specific user ID
+func (client *gocloak) GrantUserPermission(ctx context.Context, token, realm string, permission PermissionGrantParams) (*PermissionGrantResponseRepresentation, error) {
+	const errMessage = "could not grant user permission"
+
+	err := checkPermissionGrantParams(permission)
+
+	if err != nil {
+		return nil, err
+	}
+
+	permission.Granted = BoolP(true)
+
+	var result PermissionGrantResponseRepresentation
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		SetBody(permission).
+		Post(client.getRealmURL(realm, "authz", "protection", "permission", "ticket"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+
+}
+
+// checkPermissionUpdateParams
+func checkPermissionUpdateParams(permission PermissionGrantParams) error {
+
+	err := checkPermissionGrantParams(permission)
+
+	if err != nil {
+		return err
+	}
+
+	if permission.Granted == nil {
+		return errors.New("granted required to update user permission")
+	}
+	return nil
+}
+
+func (client *gocloak) UpdateUserPermission(ctx context.Context, token, realm string, permission PermissionGrantParams) (*PermissionGrantResponseRepresentation, error) {
+	const errMessage = "could not update user permission"
+
+	err := checkPermissionUpdateParams(permission)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var result PermissionGrantResponseRepresentation
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		SetBody(permission).
+		Put(client.getRealmURL(realm, "authz", "protection", "permission", "ticket"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode() == http.StatusNoContent { // permission updated to 'not granted' removes permission
+		return nil, nil
+	}
+	return &result, nil
+
+}
+
+// GetUserPermission gets granted permissions according query parameters
+func (client *gocloak) GetUserPermissions(ctx context.Context, token, realm string, params GetUserPermissionParams) ([]*PermissionGrantResponseRepresentation, error) {
+	const errMessage = "could not get user permissions"
+
+	queryParams, err := GetQueryParams(params)
+	if err != nil {
+		return nil, err
+	}
+
+	var result []*PermissionGrantResponseRepresentation
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		SetQueryParams(queryParams).
+		Get(client.getRealmURL(realm, "authz", "protection", "permission", "ticket"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+
+}
+
+func (client *gocloak) DeleteUserPermission(ctx context.Context, token, realm, ticketID string) error {
+	const errMessage = "could not delete user permission"
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		Delete(client.getRealmURL(realm, "authz", "protection", "permission", "ticket", ticketID))
+
+	return checkForError(resp, err, errMessage)
+
+}
+
 // CreatePermission creates a permission associated with the client
 func (client *gocloak) CreatePermission(ctx context.Context, token, realm, clientID string, permission PermissionRepresentation) (*PermissionRepresentation, error) {
 	const errMessage = "could not create permission"
diff --git a/go.mod b/go.mod
index 856a6d6..3c1b215 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,10 @@ module github.com/Nerzal/gocloak/v7
 go 1.15
 
 require (
+	dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363 // indirect
 	github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
 	github.com/go-resty/resty/v2 v2.3.0
+	github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063 // indirect
 	github.com/pkg/errors v0.9.1
 	github.com/segmentio/ksuid v1.0.3
 	github.com/stretchr/testify v1.6.1
diff --git a/go.sum b/go.sum
index 67f2253..73a8380 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,13 @@
+dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363 h1:o4lAkfETerCnr1kF9/qwkwjICnU+YLHNDCM8h2xj7as=
+dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363/go.mod h1:WG7q7swWsS2f9PYpt5DoEP/EBYWx8We5UoRltn9vJl8=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
 github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
 github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
 github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
+github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063 h1:dKprcOvlsvqfWn/iGvz+oYuC2axESeSMuF8dDrWMNsE=
+github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
diff --git a/gocloak.go b/gocloak.go
index 4986b72..66c9e5c 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -318,45 +318,49 @@ type GoCloak interface {
 	DeleteIdentityProvider(ctx context.Context, token, realm, alias string) error
 
 	// *** Protection API ***
-	// GetResource returns a client's resource with the given id, using access token from Client
-	GetResourceClient(ctx context.Context, token, realm, clientID, resourceID string) (*ResourceRepresentation, error)
-	// GetResources a returns resources associated with the client, using access token from Client
-	GetResourcesClient(ctx context.Context, token, realm, clientID string, params GetResourceParams) ([]*ResourceRepresentation, error)
-	// CreateResource creates a resource associated with the client
-	UpdateResourceClient(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) error
+	// GetResource returns a client's resource with the given id, using access token from client
+	GetResourceClient(ctx context.Context, token, realm, resourceID string) (*ResourceRepresentation, error)
+	// GetResources a returns resources associated with the client, using access token from client
+	GetResourcesClient(ctx context.Context, token, realm string, params GetResourceParams) ([]*ResourceRepresentation, error)
+	// CreateResource creates a resource associated with the client, using access token from client
+	CreateResourceClient(ctx context.Context, token, realm string, resource ResourceRepresentation) (*ResourceRepresentation, error)
+	// UpdateResource updates a resource associated with the client, using access token from client
+	UpdateResourceClient(ctx context.Context, token, realm string, resource ResourceRepresentation) error
+	// DeleteResource deletes a resource associated with the client, using access token from client
+	DeleteResourceClient(ctx context.Context, token, realm, resourceID string) error
 
-	// GetResource returns a client's resource with the given id, using access token from Admin
+	// GetResource returns a client's resource with the given id, using access token from admin
 	GetResource(ctx context.Context, token, realm, clientID, resourceID string) (*ResourceRepresentation, error)
-	// GetResources a returns resources associated with the client, using access token from Admin
+	// GetResources a returns resources associated with the client, using access token from admin
 	GetResources(ctx context.Context, token, realm, clientID string, params GetResourceParams) ([]*ResourceRepresentation, error)
-	// CreateResource creates a resource associated with the client
+	// CreateResource creates a resource associated with the client, using access token from admin
 	CreateResource(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) (*ResourceRepresentation, error)
-	// UpdateResource updates a resource associated with the client
+	// UpdateResource updates a resource associated with the client, using access token from admin
 	UpdateResource(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) error
-	// DeleteResource deletes a resource associated with the client
+	// DeleteResource deletes a resource associated with the client, using access token from admin
 	DeleteResource(ctx context.Context, token, realm, clientID, resourceID string) error
 
-	// GetScope returns a client's scope with the given id
+	// GetScope returns a client's scope with the given id, using access token from admin
 	GetScope(ctx context.Context, token, realm, clientID, scopeID string) (*ScopeRepresentation, error)
-	// GetScopes returns scopes associated with the client
+	// GetScopes returns scopes associated with the client, using access token from admin
 	GetScopes(ctx context.Context, token, realm, clientID string, params GetScopeParams) ([]*ScopeRepresentation, error)
-	// CreateScope creates a scope associated with the client
+	// CreateScope creates a scope associated with the client, using access token from admin
 	CreateScope(ctx context.Context, token, realm, clientID string, scope ScopeRepresentation) (*ScopeRepresentation, error)
-	// UpdateScope updates a scope associated with the client
+	// UpdateScope updates a scope associated with the client, using access token from admin
 	UpdateScope(ctx context.Context, token, realm, clientID string, resource ScopeRepresentation) error
-	// DeleteScope deletes a scope associated with the client
+	// DeleteScope deletes a scope associated with the client, using access token from admin
 	DeleteScope(ctx context.Context, token, realm, clientID, scopeID string) error
 
-	// GetPolicy returns a client's policy with the given id
-	GetPolicy(ctx context.Context, token, realm, clientID, policyID string) (*PolicyRepresentation, error)
-	// GetPolicies returns policies associated with the client
-	GetPolicies(ctx context.Context, token, realm, clientID string, params GetPolicyParams) ([]*PolicyRepresentation, error)
-	// CreatePolicy creates a policy associated with the client
-	CreatePolicy(ctx context.Context, token, realm, clientID string, policy PolicyRepresentation) (*PolicyRepresentation, error)
-	// UpdatePolicy updates a policy associated with the client
-	UpdatePolicy(ctx context.Context, token, realm, clientID string, policy PolicyRepresentation) error
-	// DeletePolicy deletes a policy associated with the client
-	DeletePolicy(ctx context.Context, token, realm, clientID, policyID string) error
+	// CreatePermissionTicket creates a permission ticket for a resource, using access token from client (typically a resource server)
+	CreatePermissionTicket(ctx context.Context, token, realm string, permissions []CreatePermissionTicketParams) (*PermissionTicketResponseRepresentation, error)
+	// GrantUserPermission lets resource owner grant permission for specific resource ID to specific user ID
+	GrantUserPermission(ctx context.Context, token, realm string, permission PermissionGrantParams) (*PermissionGrantResponseRepresentation, error)
+	// GrantPermission lets resource owner update permission for specific resource ID to specific user ID
+	UpdateUserPermission(ctx context.Context, token, realm string, permission PermissionGrantParams) (*PermissionGrantResponseRepresentation, error)
+	// GetUserPermission gets granted permissions according query parameters
+	GetUserPermissions(ctx context.Context, token, realm string, params GetUserPermissionParams) ([]*PermissionGrantResponseRepresentation, error)
+	// DeleteUserPermission lets resource owner delete permission for specific resource ID to specific user ID
+	DeleteUserPermission(ctx context.Context, token, realm, ticketID string) error
 
 	// GetPermission returns a client's permission with the given id
 	GetPermission(ctx context.Context, token, realm, clientID, permissionID string) (*PermissionRepresentation, error)
@@ -372,6 +376,29 @@ type GoCloak interface {
 	GetDependentPermissions(ctx context.Context, token, realm, clientID, policyID string) ([]*PermissionRepresentation, error)
 	GetPermissionResources(ctx context.Context, token, realm, clientID, permissionID string) ([]*PermissionResource, error)
 	GetPermissionScopes(ctx context.Context, token, realm, clientID, permissionID string) ([]*PermissionScope, error)
+
+	// GetPolicy returns a client's policy with the given id, using access token from admin
+	GetPolicy(ctx context.Context, token, realm, clientID, policyID string) (*PolicyRepresentation, error)
+	// GetPolicies returns policies associated with the client, using access token from admin
+	GetPolicies(ctx context.Context, token, realm, clientID string, params GetPolicyParams) ([]*PolicyRepresentation, error)
+	// CreatePolicy creates a policy associated with the client, using access token from admin
+	CreatePolicy(ctx context.Context, token, realm, clientID string, policy PolicyRepresentation) (*PolicyRepresentation, error)
+	// UpdatePolicy updates a policy associated with the client, using access token from admin
+	UpdatePolicy(ctx context.Context, token, realm, clientID string, policy PolicyRepresentation) error
+	// DeletePolicy deletes a policy associated with the client, using access token from admin
+	DeletePolicy(ctx context.Context, token, realm, clientID, policyID string) error
+
+	// GetResourcePolicy updates a permission for a specifc resource, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+	GetResourcePolicy(ctx context.Context, token, realm, permissionID string) (*ResourcePolicyRepresentation, error)
+	// GetResources returns resources associated with the client, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+	GetResourcePolicies(ctx context.Context, token, realm string, params GetResourcePoliciesParams) ([]*ResourcePolicyRepresentation, error)
+	// GetResources returns all resources associated with the client, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+	CreateResourcePolicy(ctx context.Context, token, realm, resourceID string, policy ResourcePolicyRepresentation) (*ResourcePolicyRepresentation, error)
+	// UpdateResourcePolicy updates a permission for a specifc resource, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+	UpdateResourcePolicy(ctx context.Context, token, realm, permissionID string, policy ResourcePolicyRepresentation) error
+	// DeleteResourcePolicy deletes a permission for a specifc resource, using token obtained by Resource Owner Password Credentials Grant or Token exchange
+	DeleteResourcePolicy(ctx context.Context, token, realm, permissionID string) error
+
 	// ---------------
 	// Credentials API
 	// ---------------
diff --git a/models.go b/models.go
index 5829e3c..f3dd4a9 100644
--- a/models.go
+++ b/models.go
@@ -3,6 +3,8 @@ package gocloak
 import (
 	"encoding/json"
 	"strings"
+
+	"github.com/dgrijalva/jwt-go/v4"
 )
 
 // GetQueryParams converts the struct to map[string]string
@@ -537,6 +539,7 @@ type ResourceRepresentation struct {
 	Name               *string                      `json:"name,omitempty"`
 	Owner              *ResourceOwnerRepresentation `json:"owner,omitempty"`
 	OwnerManagedAccess *bool                        `json:"ownerManagedAccess,omitempty"`
+	ResourceScopes     *[]ScopeRepresentation       `json:"resource_scopes,omitempty"`
 	Scopes             *[]ScopeRepresentation       `json:"scopes,omitempty"`
 	Type               *string                      `json:"type,omitempty"`
 	URIs               *[]string                    `json:"uris,omitempty"`
@@ -925,6 +928,113 @@ type PermissionRepresentation struct {
 	Type             *string           `json:"type,omitempty"`
 }
 
+// CreatePermissionTicketParams represents the optional parameters for getting a permission ticket
+type CreatePermissionTicketParams struct {
+	ResourceID     *string              `json:"resource_id,omitempty"`
+	ResourceScopes *[]string            `json:"resource_scopes,omitempty"`
+	Claims         *map[string][]string `json:"claims,omitempty"`
+}
+
+// PermissionTicketDescriptionRepresentation represents the parameters returned along with a permission ticket
+type PermissionTicketDescriptionRepresentation struct {
+	ID                     *string               `json:"id,omitempty"`
+	CreatedTimeStamp       *int64                `json:"createdTimestamp,omitempty"`
+	UserName               *string               `json:"username,omitempty"`
+	Enabled                *bool                 `json:"enabled,omitempty"`
+	TOTP                   *bool                 `json:"totp,omitempty"`
+	EmailVerified          *bool                 `json:"emailVerified,omitempty"`
+	FirstName              *string               `json:"firstName,omitempty"`
+	LastName               *string               `json:"lastName,omitempty"`
+	Email                  *string               `json:"email,omitempty"`
+	DisableCredentialTypes *[]string             `json:"disableCredentialTypes,omitempty"`
+	RequiredActions        *[]string             `json:"requiredActions,omitempty"`
+	NotBefore              *int64                `json:"notBefore,omitempty"`
+	Access                 *AccessRepresentation `json:"access,omitempty"`
+}
+
+// AccessRepresentation represents the access parameters returned in the permission ticket description
+type AccessRepresentation struct {
+	ManageGroupMembership *bool `json:"manageGroupMembership,omitempty"`
+	View                  *bool `json:"view,omitempty"`
+	MapRoles              *bool `json:"mapRoles,omitempty"`
+	Impersonate           *bool `json:"impersonate,omitempty"`
+	Manage                *bool `json:"manage,omitempty"`
+}
+
+// PermissionTicketResponseRepresentation represents the keycloak response containing the permission ticket
+type PermissionTicketResponseRepresentation struct {
+	Ticket *string `json:"ticket,omitempty"`
+}
+
+// PermissionTicketRepresentation represents the permission ticket contents
+type PermissionTicketRepresentation struct {
+	AZP         *string                                     `json:"azp,omitempty"`
+	Claims      *map[string][]string                        `json:"claims,omitempty"`
+	Permissions *[]PermissionTicketPermissionRepresentation `json:"permissions,omitempty"`
+	jwt.StandardClaims
+}
+
+// PermissionTicketPermissionRepresentation represents the individual permissions in a permission ticket
+type PermissionTicketPermissionRepresentation struct {
+	Scopes *[]string `json:"scopes,omitempty"`
+	RSID   *string   `json:"rsid,omitempty"`
+}
+
+// PermissionGrantParams represents the permission which the resource owner is granting to a specific user
+type PermissionGrantParams struct {
+	ResourceID  *string `json:"resource,omitempty"`
+	RequesterID *string `json:"requester,omitempty"`
+	Granted     *bool   `json:"granted,omitempty"`
+	ScopeName   *string `json:"scopeName,omitempty"`
+	TicketID    *string `json:"id,omitempty"`
+}
+
+// PermissionGrantResponseRepresentation represents the reply from Keycloack after granting permission
+type PermissionGrantResponseRepresentation struct {
+	ID          *string `json:"id,omitempty"`
+	Owner       *string `json:"owner,omitempty"`
+	ResourceID  *string `json:"resource,omitempty"`
+	Scope       *string `json:"scope,omitempty"`
+	Granted     *bool   `json:"granted,omitempty"`
+	RequesterID *string `json:"requester,omitempty"`
+}
+
+// GetUserPermissionParams represents the optional parameters for getting user permissions
+type GetUserPermissionParams struct {
+	ScopeID     *string `json:"scopeId,omitempty"`
+	ResourceID  *string `json:"resourceId,omitempty"`
+	Owner       *string `json:"owner,omitempty"`
+	Requester   *string `json:"requester,omitempty"`
+	Granted     *bool   `json:"granted,omitempty"`
+	ReturnNames *string `json:"returnNames,omitempty"`
+	First       *int    `json:"first,string,omitempty"`
+	Max         *int    `json:"max,string,omitempty"`
+}
+
+// ResourcePolicyRepresentation is a representation of a Policy applied to a resource
+type ResourcePolicyRepresentation struct {
+	Name             *string           `json:"name,omitempty"`
+	Description      *string           `json:"description,omitempty"`
+	Scopes           *[]string         `json:"scopes,omitempty"`
+	Roles            *[]string         `json:"roles,omitempty"`
+	Groups           *[]string         `json:"groups,omitempty"`
+	Clients          *[]string         `json:"clients,omitempty"`
+	ID               *string           `json:"id,omitempty"`
+	Logic            *Logic            `json:"logic,omitempty"`
+	DecisionStrategy *DecisionStrategy `json:"decisionStrategy,omitempty"`
+	Owner            *string           `json:"owner,omitempty"`
+	Type             *string           `json:"type,omitempty"`
+}
+
+// GetResourcePoliciesParams is a representation of the query params for getting policies
+type GetResourcePoliciesParams struct {
+	ResourceID *string `json:"resource_id,omitempty"`
+	Name       *string `json:"name,omitempty"`
+	Scope      *string `json:"scope,omitempty"`
+	First      *int    `json:"first,string,omitempty"`
+	Max        *int    `json:"max,string,omitempty"`
+}
+
 // CredentialRepresentation is a representations of the credentials
 // v7: https://www.keycloak.org/docs-api/7.0/rest-api/index.html#_credentialrepresentation
 // v8: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation
diff --git a/utils.go b/utils.go
index 14e3d9c..df012c3 100644
--- a/utils.go
+++ b/utils.go
@@ -78,6 +78,17 @@ func NilOrEmpty(value *string) bool {
 	return value == nil || len(*value) == 0
 }
 
+// NilOrEmptyArray returns true if string is empty or has a nil value
+func NilOrEmptyArray(value *[]string) bool {
+
+	if value == nil || len(*value) == 0 {
+		return true
+	}
+
+	return (*value)[0] == ""
+
+}
+
 // DecisionStrategyP returns a pointer for a DecisionStrategy value
 func DecisionStrategyP(value DecisionStrategy) *DecisionStrategy {
 	return &value


Test patch:
diff --git a/client_test.go b/client_test.go
index cdca950..2e436d3 100644
--- a/client_test.go
+++ b/client_test.go
@@ -230,6 +230,87 @@ func CreateResource(t *testing.T, client gocloak.GoCloak, clientID string) (func
 	return tearDown, *createdResource.ID
 }
 
+func CreateResourceClientWithScopes(t *testing.T, client gocloak.GoCloak) (func(), string) {
+	cfg := GetConfig(t)
+	token := GetClientToken(t, client)
+	resource := gocloak.ResourceRepresentation{
+		Name:        GetRandomNameP("ResourceName"),
+		DisplayName: gocloak.StringP("Resource Display Name"),
+		Type:        gocloak.StringP("urn:gocloak:resources:test"),
+		IconURI:     gocloak.StringP("/resource/test/icon"),
+		Attributes: &map[string][]string{
+			"foo": {"bar", "alice", "bob", "roflcopter"},
+			"bar": {"baz"},
+		},
+		URIs: &[]string{
+			"/resource/1",
+			"/resource/2",
+		},
+		OwnerManagedAccess: gocloak.BoolP(true),
+		ResourceScopes: &[]gocloak.ScopeRepresentation{
+			{Name: gocloak.StringP("read-public")},
+			{Name: gocloak.StringP("read-private")},
+			{Name: gocloak.StringP("post-update")},
+			{Name: gocloak.StringP("message-view")},
+			{Name: gocloak.StringP("message-post")},
+		},
+	}
+	createdResource, err := client.CreateResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		resource)
+	require.NoError(t, err, "CreateResource failed")
+	t.Logf("Created Resource ID: %s ", *(createdResource.ID))
+
+	tearDown := func() {
+		err := client.DeleteResourceClient(
+			context.Background(),
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			*createdResource.ID)
+		require.NoError(t, err, "DeleteResource failed")
+	}
+	return tearDown, *createdResource.ID
+}
+
+func CreateResourceClient(t *testing.T, client gocloak.GoCloak) (func(), string) {
+	cfg := GetConfig(t)
+	token := GetClientToken(t, client)
+	resource := gocloak.ResourceRepresentation{
+		Name:        GetRandomNameP("ResourceName"),
+		DisplayName: gocloak.StringP("Resource Display Name"),
+		Type:        gocloak.StringP("urn:gocloak:resources:test"),
+		IconURI:     gocloak.StringP("/resource/test/icon"),
+		Attributes: &map[string][]string{
+			"foo": {"bar", "alice", "bob", "roflcopter"},
+			"bar": {"baz"},
+		},
+		URIs: &[]string{
+			"/resource/1",
+			"/resource/2",
+		},
+		OwnerManagedAccess: gocloak.BoolP(true),
+	}
+	createdResource, err := client.CreateResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		resource)
+	require.NoError(t, err, "CreateResource failed")
+	t.Logf("Created Resource ID: %s ", *(createdResource.ID))
+
+	tearDown := func() {
+		err := client.DeleteResourceClient(
+			context.Background(),
+			token.AccessToken,
+			cfg.GoCloak.Realm,
+			*createdResource.ID)
+		require.NoError(t, err, "DeleteResource failed")
+	}
+	return tearDown, *createdResource.ID
+}
+
 func CreateScope(t *testing.T, client gocloak.GoCloak, clientID string) (func(), string) {
 	cfg := GetConfig(t)
 	token := GetAdminToken(t, client)
@@ -4037,6 +4118,58 @@ func TestGocloak_CreateProvider(t *testing.T) {
 // Protection API
 // -----------------
 
+func TestGocloak_ErrorsCreateListGetUpdateDeleteResourceClient(t *testing.T) {
+
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetClientToken(t, client)
+	token.AccessToken = "" // force unauthorized access attempts
+
+	// Create
+	tearDown, resourceID := CreateResourceClient(t, client)
+	// Delete
+	defer tearDown()
+
+	// List
+	_, err := client.GetResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		resourceID,
+	)
+
+	require.Error(t, err, "GetResource no error on unauthorized request")
+
+	// Looking for a created resource
+	_, err = client.GetResourcesClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloak.GetResourceParams{
+			Name: gocloak.StringP("nothing"),
+		},
+	)
+	require.Error(t, err, "GetResources no error on unauthorized request")
+
+	err = client.UpdateResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloak.ResourceRepresentation{},
+	)
+	require.Error(t, err, "UpdateResourceClient no error on missing ID of the resource")
+	emptyResource := gocloak.ResourceRepresentation{}
+	err = client.UpdateResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		emptyResource,
+	)
+	require.Error(t, err, "UpdateResourceClient no error on unauthorized request")
+
+}
+
 func TestGocloak_CreateListGetUpdateDeleteResourceClient(t *testing.T) {
 
 	t.Parallel()
@@ -4045,7 +4178,7 @@ func TestGocloak_CreateListGetUpdateDeleteResourceClient(t *testing.T) {
 	token := GetClientToken(t, client)
 
 	// Create
-	tearDown, resourceID := CreateResource(t, client, gocloakClientID)
+	tearDown, resourceID := CreateResourceClient(t, client)
 	// Delete
 	defer tearDown()
 
@@ -4054,7 +4187,6 @@ func TestGocloak_CreateListGetUpdateDeleteResourceClient(t *testing.T) {
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		gocloakClientID,
 		resourceID,
 	)
 
@@ -4067,7 +4199,6 @@ func TestGocloak_CreateListGetUpdateDeleteResourceClient(t *testing.T) {
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		gocloakClientID,
 		gocloak.GetResourceParams{
 			Name: createdResource.Name,
 		},
@@ -4081,7 +4212,6 @@ func TestGocloak_CreateListGetUpdateDeleteResourceClient(t *testing.T) {
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		gocloakClientID,
 		gocloak.ResourceRepresentation{},
 	)
 	require.Error(t, err, "Should fail because of missing ID of the resource")
@@ -4092,7 +4222,6 @@ func TestGocloak_CreateListGetUpdateDeleteResourceClient(t *testing.T) {
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		gocloakClientID,
 		*createdResource,
 	)
 	require.NoError(t, err, "UpdateResource failed")
@@ -4101,7 +4230,6 @@ func TestGocloak_CreateListGetUpdateDeleteResourceClient(t *testing.T) {
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
-		gocloakClientID,
 		resourceID,
 	)
 	require.NoError(t, err, "GetResource failed")
@@ -4338,6 +4466,95 @@ func TestGocloak_CreateListGetUpdateDeletePolicy(t *testing.T) {
 	require.Equal(t, *(createdPolicy.Name), *(updatedPolicy.Name))
 }
 
+func TestGocloak_CreateGetUpdateDeleteResourcePolicy(t *testing.T) {
+
+	// parallel is causing intermittent conflict with role-based test GetClientScopeMappingsClientRolesAvailable
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetClientToken(t, client)
+	adminToken := GetAdminToken(t, client)
+
+	tearDownResource, resourceID := CreateResourceClientWithScopes(t, client)
+	defer tearDownResource()
+
+	roleName := GetRandomName("editor")
+	role := gocloak.Role{
+		Name: &roleName,
+	}
+
+	roleID, err := client.CreateClientRole(context.Background(), adminToken.AccessToken, cfg.GoCloak.Realm, gocloakClientID, role)
+
+	defer func() {
+		err := client.DeleteClientRole(context.Background(), adminToken.AccessToken, cfg.GoCloak.Realm, gocloakClientID, roleName)
+		require.NoError(t, err, "could not delete client role")
+	}()
+
+	require.NoError(t, err, "could not create client role")
+	t.Logf("Created ClientRole: %+v", roleID)
+
+	scopes := []string{"message-post"}
+	policyNameP := GetRandomNameP("PolicyName")
+	policy := gocloak.ResourcePolicyRepresentation{
+		Name:        policyNameP,
+		Description: gocloak.StringP("Role Policy"),
+		Scopes:      &scopes,
+		Roles:       &[]string{roleName},
+	}
+
+	result, err := client.CreateResourcePolicy(context.Background(), token.AccessToken, cfg.GoCloak.Realm, resourceID, policy)
+	require.NoError(t, err, "could not create resource policy")
+	require.Equal(t, "Role Policy", *(result.Description))
+
+	result, err = client.GetResourcePolicy(context.Background(), token.AccessToken, cfg.GoCloak.Realm, *(result.ID))
+	require.NoError(t, err, "could not get resource policy")
+	require.Equal(t, scopes, *(result.Scopes))
+
+	newScopes := []string{"message-view"}
+	result.Scopes = &newScopes
+
+	err = client.UpdateResourcePolicy(context.Background(), token.AccessToken, cfg.GoCloak.Realm, *(result.ID), *result)
+	require.NoError(t, err, "could not get resource policy")
+
+	result, err = client.GetResourcePolicy(context.Background(), token.AccessToken, cfg.GoCloak.Realm, *(result.ID))
+	require.NoError(t, err, "could not get resource policy")
+	require.Equal(t, newScopes, *(result.Scopes))
+
+	params := gocloak.GetResourcePoliciesParams{
+		Scope: gocloak.StringP("message-view"),
+	}
+	policies, err := client.GetResourcePolicies(context.Background(), token.AccessToken, cfg.GoCloak.Realm, params)
+	require.NoError(t, err, "could not get resource policies")
+	require.Equal(t, 1, len(policies))
+	require.False(t, policies[0] == nil)
+
+	if len(policies) == 1 && policies[0] != nil {
+		require.Equal(t, *policyNameP, *(policies[0].Name))
+	}
+	err = client.DeleteResourcePolicy(context.Background(), token.AccessToken, cfg.GoCloak.Realm, *(result.ID))
+	require.NoError(t, err, "could not delete resource policies")
+
+	policies, err = client.GetResourcePolicies(context.Background(), token.AccessToken, cfg.GoCloak.Realm, params)
+	require.NoError(t, err, "could not get resource policies")
+	require.Equal(t, 0, len(policies))
+
+	// Test error handling
+	_, err = client.CreateResourcePolicy(context.Background(), token.AccessToken, cfg.GoCloak.Realm, "", policy)
+	require.Error(t, err, "should not create resource policy without resourceID")
+
+	_, err = client.GetResourcePolicy(context.Background(), "", cfg.GoCloak.Realm, "asdfasdfasdfasdf")
+	require.Error(t, err, "should not get resource policy without token")
+
+	err = client.UpdateResourcePolicy(context.Background(), token.AccessToken, cfg.GoCloak.Realm, "", policy)
+	require.Error(t, err, "should not update resource policy without token")
+
+	_, err = client.GetResourcePolicies(context.Background(), "", cfg.GoCloak.Realm, params)
+	require.Error(t, err, "should not get resource policies without token")
+
+	err = client.DeleteResourcePolicy(context.Background(), token.AccessToken, cfg.GoCloak.Realm, "")
+	require.Error(t, err, "should not delete resource policy without permission ID")
+
+}
+
 func TestGocloak_RolePolicy(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)
@@ -4525,6 +4742,252 @@ func TestGocloak_GroupPolicy(t *testing.T) {
 	defer tearDown()
 }
 
+func TestGocloak_ErrorsGrantGetUpdateDeleteUserPermission(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetClientToken(t, client)
+
+	tearDownResource, resourceID := CreateResourceClientWithScopes(t, client)
+	defer tearDownResource()
+
+	tearDownUser, userID := CreateUser(t, client)
+	defer tearDownUser()
+
+	// Grant
+	scope := "read-private"
+
+	permission := gocloak.PermissionGrantParams{
+		RequesterID: &userID,
+		ScopeName:   &scope,
+	}
+	_, err := client.GrantUserPermission(context.Background(), token.AccessToken, cfg.GoCloak.Realm, permission)
+	require.Error(t, err, "GrantUserPermission no error on missing ResourceID")
+
+	permission = gocloak.PermissionGrantParams{
+		ResourceID: &resourceID,
+		ScopeName:  &scope,
+	}
+	_, err = client.GrantUserPermission(context.Background(), token.AccessToken, cfg.GoCloak.Realm, permission)
+	require.Error(t, err, "GrantUserPermission no error on missing RequesterID")
+
+	permission = gocloak.PermissionGrantParams{
+		ScopeName: &scope,
+	}
+	_, err = client.GrantUserPermission(context.Background(), token.AccessToken, cfg.GoCloak.Realm, permission)
+	require.Error(t, err, "GrantUserPermission no error on missing Scope")
+
+	permission = gocloak.PermissionGrantParams{
+		ResourceID:  &resourceID,
+		RequesterID: &userID,
+		ScopeName:   &scope,
+	}
+	_, err = client.GrantUserPermission(context.Background(), "", cfg.GoCloak.Realm, permission)
+	require.Error(t, err, "GrantUserPermission no error on unauthorized request")
+
+	// Get
+	params := gocloak.GetUserPermissionParams{
+		ResourceID: &resourceID,
+	}
+	_, err = client.GetUserPermissions(context.Background(), "", cfg.GoCloak.Realm, params)
+	require.Error(t, err, "GetUserPermission no error on unauthorized request")
+
+	_, err = client.UpdateUserPermission(context.Background(), "", cfg.GoCloak.Realm, permission)
+	require.Error(t, err, "UpdateUserPermission no error on unauthorized request")
+
+	// Get (no permission expected to be returned)
+	params = gocloak.GetUserPermissionParams{
+		ResourceID: &resourceID,
+	}
+	_, err = client.GetUserPermissions(context.Background(), "", cfg.GoCloak.Realm, params)
+	require.Error(t, err, "UpdateUserPermission no error on unauthorized request")
+
+	// Delete
+	err = client.DeleteUserPermission(context.Background(), "", cfg.GoCloak.Realm, "someID")
+	require.Error(t, err, "DeleteUserPermission no error on unauthorized request")
+
+}
+
+func TestGocloak_GrantGetUpdateDeleteUserPermission(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetClientToken(t, client)
+
+	tearDownResource, resourceID := CreateResourceClientWithScopes(t, client)
+	defer tearDownResource()
+
+	tearDownUser, userID := CreateUser(t, client)
+	defer tearDownUser()
+
+	// Grant
+	scope := "read-private"
+
+	permission := gocloak.PermissionGrantParams{
+		ResourceID:  &resourceID,
+		RequesterID: &userID,
+		ScopeName:   &scope,
+	}
+	result, err := client.GrantUserPermission(context.Background(), token.AccessToken, cfg.GoCloak.Realm, permission)
+
+	require.NoError(t, err, "GrantUserPermission failed")
+	require.True(t, nil != result)
+	if result != nil {
+		require.False(t, result.ResourceID == nil)
+		require.False(t, result.RequesterID == nil)
+		require.False(t, result.Granted == nil)
+		if result.ResourceID != nil {
+			require.Equal(t, resourceID, *(result.ResourceID))
+		}
+		if result.RequesterID != nil {
+			require.Equal(t, userID, *(result.RequesterID))
+		}
+		if result.Granted != nil {
+			require.Equal(t, true, *(result.Granted))
+		}
+	}
+
+	// Get
+	params := gocloak.GetUserPermissionParams{
+		ResourceID: &resourceID,
+	}
+	queried, err := client.GetUserPermissions(context.Background(), token.AccessToken, cfg.GoCloak.Realm, params)
+	require.NoError(t, err, "GetUserPermissions failed")
+	require.Equal(t, 1, len(queried))
+	require.Equal(t, userID, *(queried[0].RequesterID))
+
+	// Update
+	permission.TicketID = gocloak.StringP(*(result.ID))
+	permission.Granted = gocloak.BoolP(false)
+
+	result, err = client.UpdateUserPermission(context.Background(), token.AccessToken, cfg.GoCloak.Realm, permission)
+
+	require.NoError(t, err, "UpdateUserPermission failed")
+	require.True(t, nil == result)
+
+	// Get (no permission expected to be returned)
+	params = gocloak.GetUserPermissionParams{
+		ResourceID: &resourceID,
+	}
+	queried, err = client.GetUserPermissions(context.Background(), token.AccessToken, cfg.GoCloak.Realm, params)
+	require.NoError(t, err, "GetUserPermissions failed")
+	require.Equal(t, 0, len(queried))
+
+	// Grant again
+	permission = gocloak.PermissionGrantParams{
+		ResourceID:  &resourceID,
+		RequesterID: &userID,
+		ScopeName:   &scope,
+	}
+	result, err = client.GrantUserPermission(context.Background(), token.AccessToken, cfg.GoCloak.Realm, permission)
+	require.NoError(t, err, "GrantUserPermissions failed")
+
+	// Get
+	params = gocloak.GetUserPermissionParams{
+		ResourceID: &resourceID,
+	}
+	queried, err = client.GetUserPermissions(context.Background(), token.AccessToken, cfg.GoCloak.Realm, params)
+	require.NoError(t, err, "GetUserPermissions failed")
+	require.Equal(t, 1, len(queried))
+	require.Equal(t, userID, *(queried[0].RequesterID))
+
+	// Delete
+	err = client.DeleteUserPermission(context.Background(), token.AccessToken, cfg.GoCloak.Realm, *(result.ID))
+	require.NoError(t, err, "DeleteUserPermissions failed")
+
+	// Get (no permission expected to be returned)
+
+	params = gocloak.GetUserPermissionParams{
+		ResourceID: &resourceID,
+	}
+	queried, err = client.GetUserPermissions(context.Background(), token.AccessToken, cfg.GoCloak.Realm, params)
+	require.NoError(t, err, "GetUserPermissions failed")
+	require.Equal(t, 0, len(queried))
+
+}
+
+func TestGocloak_BadCreatePermissionTicket(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetClientToken(t, client)
+
+	// Create
+	tearDownResource, resourceID := CreateResourceClientWithScopes(t, client)
+	// Delete
+	defer tearDownResource()
+
+	_, err := client.CreatePermissionTicket(context.Background(), token.AccessToken, cfg.GoCloak.Realm, []gocloak.CreatePermissionTicketParams{})
+	require.Error(t, err, "CreatePermissionTicket no error on empty params")
+
+	permissions := gocloak.CreatePermissionTicketParams{
+		ResourceID: &resourceID,
+	}
+
+	_, err = client.CreatePermissionTicket(context.Background(), token.AccessToken, cfg.GoCloak.Realm, []gocloak.CreatePermissionTicketParams{permissions})
+	require.Error(t, err, "CreatePermissionTicket no error on missing ResourceScopes in permission")
+
+	permissions = gocloak.CreatePermissionTicketParams{
+		ResourceScopes: &[]string{"read-private"},
+	}
+	_, err = client.CreatePermissionTicket(context.Background(), token.AccessToken, cfg.GoCloak.Realm, []gocloak.CreatePermissionTicketParams{permissions})
+	require.Error(t, err, "CreatePermissionTicket no error on missing ResourceID in permission")
+
+	permissions = gocloak.CreatePermissionTicketParams{
+		ResourceID:     &resourceID,
+		ResourceScopes: &[]string{"read-private"},
+	}
+
+	_, err = client.CreatePermissionTicket(context.Background(), "", cfg.GoCloak.Realm, []gocloak.CreatePermissionTicketParams{permissions})
+	require.Error(t, err, "CreatePermissionTicket no error on unauthorized access attempt")
+
+}
+
+func TestGocloak_CreatePermissionTicket(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetClientToken(t, client)
+
+	// Create
+	tearDownResource, resourceID := CreateResourceClientWithScopes(t, client)
+	// Delete
+	defer tearDownResource()
+
+	// Add additional claims
+	pushClaims := make(map[string][]string)
+
+	pushClaims["organization"] = []string{"acme", "somecorp"}
+
+	permissions := gocloak.CreatePermissionTicketParams{
+		ResourceID:     &resourceID,
+		ResourceScopes: &[]string{"read-private"},
+		Claims:         &pushClaims,
+	}
+
+	ticket, err := client.CreatePermissionTicket(context.Background(), token.AccessToken, cfg.GoCloak.Realm, []gocloak.CreatePermissionTicketParams{permissions})
+
+	require.NoError(t, err, "CreatePermissionTicket failed")
+	t.Logf("Created PermissionTicket: %+v", *(ticket.Ticket))
+
+	pt, err := jwt.ParseWithClaims(*(ticket.Ticket), &gocloak.PermissionTicketRepresentation{}, func(token *jwt.Token) (interface{}, error) {
+		return []byte(""), nil
+	})
+
+	//we're expecting validity error because we didn't supply secret
+	require.Equal(t, "token signature is invalid", err.Error())
+
+	claims, ok := pt.Claims.(*gocloak.PermissionTicketRepresentation) //ticketClaims)
+	require.Equal(t, true, ok)
+	require.Equal(t, cfg.GoCloak.Realm, *(claims.AZP))
+	require.Equal(t, 1, len(*(claims.Permissions)))
+	require.Equal(t, 1, len(*(claims.Permissions)))
+	require.Equal(t, 1, len(*(claims.Claims)))
+	require.Equal(t, pushClaims["organization"], (*(claims.Claims))["organization"])
+	require.Equal(t, *permissions.ResourceID, *((*(claims.Permissions))[0].RSID))
+
+}
+
 func TestGocloak_CreateListGetUpdateDeletePermission(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)
diff --git a/utils_test.go b/utils_test.go
index 6da1e96..68ba350 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -105,3 +105,12 @@ func TestPFloat64(t *testing.T) {
 	assert.Equal(t, p, v)
 	assert.IsType(t, p, v)
 }
+func TestNilOrEmptyArray(t *testing.T) {
+	a := gocloak.NilOrEmptyArray(&[]string{"c", "d"})
+	b := gocloak.NilOrEmptyArray(&[]string{"", "b"})
+	c := gocloak.NilOrEmptyArray(&[]string{})
+	assert.False(t, a)
+	assert.True(t, b)
+	assert.True(t, c)
+
+}