Repository: Nerzal/gocloak
Pull request #230 (2020-11-24 03:54:16)

Add client methods for Get, Update resource
This is an initial proof of concept to show the proposed approach of
adding additional methods that use the protection API rather than the
admin endpoints. This is needed for production where admin credentials
won't be available to clients. GetResourceClient(), GetResourcesClient()
and UpdateResourceClient() have been added, and tested with TestGocloak_CreateListGetUpdateDeleteResourceClient()

[Issue: #229]




Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index cd7ade2..0f8105b 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -7,15 +7,15 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v2.1.3
         with:
           go-version: 1.15
       - name: Checkout code
         uses: actions/checkout@v2
       - name: Run golangci-lint
-        uses: golangci/golangci-lint-action@v1
+        uses: golangci/golangci-lint-action@v2.3.0
         with:
-          version: v1.27
+          version: v1.28.3
       - name: WriteGoList
         run: go list -json -m all > go.list
       - name: nancy
diff --git a/client.go b/client.go
index 1a2db02..97f8f2b 100644
--- a/client.go
+++ b/client.go
@@ -1363,6 +1363,22 @@ func (client *gocloak) GetClientRoles(ctx context.Context, token, realm, clientI
 	return result, nil
 }
 
+// GetClientRoleById gets role for the given client in realm using role ID
+func (client *gocloak) GetClientRoleByID(ctx context.Context, token, realm, roleID string) (*Role, error) {
+	const errMessage = "could not get client role"
+
+	var result Role
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getAdminRealmURL(realm, "roles-by-id", roleID))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
 // GetRealmRolesByUserID returns all client roles assigned to the given user
 func (client *gocloak) GetClientRolesByUserID(ctx context.Context, token, realm, clientID, userID string) ([]*Role, error) {
 	const errMessage = "could not client roles by user id"
@@ -2291,6 +2307,69 @@ 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) {
+	const errMessage = "could not get resource"
+
+	var result ResourceRepresentation
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getRealmURL(realm, "authz", "protection", "resource_set", resourceID))
+
+	//http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set/{resource_id}
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	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) {
+	const errMessage = "could not get resources"
+
+	queryParams, err := GetQueryParams(params)
+	if err != nil {
+		return nil, err
+	}
+
+	var result []*ResourceRepresentation
+	var resourceIDs []string
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&resourceIDs).
+		SetQueryParams(queryParams).
+		Get(client.getRealmURL(realm, "authz", "protection", "resource_set"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	for _, resourceID := range resourceIDs {
+		resource, err := client.GetResourceClient(ctx, token, realm, clientID, resourceID)
+		if err == nil {
+			result = append(result, resource)
+		}
+	}
+
+	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 {
+	const errMessage = "could not update resource"
+
+	if NilOrEmpty(resource.ID) {
+		return errors.New("ID of a resource required")
+	}
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetBody(resource).
+		Put(client.getRealmURL(realm, "authz", "protection", "resource_set", *(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"
diff --git a/gocloak.go b/gocloak.go
index 48c5428..4986b72 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -226,6 +226,8 @@ type GoCloak interface {
 	DeleteClientRoleFromGroup(ctx context.Context, token, realm, clientID, groupID string, roles []Role) error
 	// GetClientRoles gets roles for the given client
 	GetClientRoles(ctx context.Context, accessToken, realm, clientID string) ([]*Role, error)
+	// GetClientRoleById gets role for the given client using role id
+	GetClientRoleByID(ctx context.Context, accessToken, realm, roleID string) (*Role, error)
 	// GetRealmRolesByUserID returns all client roles assigned to the given user
 	GetClientRolesByUserID(ctx context.Context, token, realm, clientID, userID string) ([]*Role, error)
 	// GetClientRolesByGroupID returns all client roles assigned to the given group
@@ -316,9 +318,16 @@ type GoCloak interface {
 	DeleteIdentityProvider(ctx context.Context, token, realm, alias string) error
 
 	// *** Protection API ***
-	// GetResource returns a client's resource with the given id
+	// 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 Admin
 	GetResource(ctx context.Context, token, realm, clientID, resourceID string) (*ResourceRepresentation, error)
-	// GetResources a returns resources associated with the client
+	// 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(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) (*ResourceRepresentation, error)


Test patch:
diff --git a/client_test.go b/client_test.go
index daf27b1..cdca950 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1070,6 +1070,16 @@ func TestGocloak_GetClientRole(t *testing.T) {
 	)
 	require.NoError(t, err, "GetClientRoleI failed")
 	require.NotNil(t, role)
+
+	role, err = client.GetClientRoleByID(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		*role.ID,
+	)
+	require.NoError(t, err, "GetClientRoleI failed")
+	require.NotNil(t, role)
+
 	token = GetAdminToken(t, client)
 	role, err = client.GetClientRole(
 		context.Background(),
@@ -4027,6 +4037,78 @@ func TestGocloak_CreateProvider(t *testing.T) {
 // Protection API
 // -----------------
 
+func TestGocloak_CreateListGetUpdateDeleteResourceClient(t *testing.T) {
+
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetClientToken(t, client)
+
+	// Create
+	tearDown, resourceID := CreateResource(t, client, gocloakClientID)
+	// Delete
+	defer tearDown()
+
+	// List
+	createdResource, err := client.GetResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		resourceID,
+	)
+
+	require.NoError(t, err, "GetResource failed")
+	t.Logf("Created Resource: %+v", *(createdResource.ID))
+	require.Equal(t, resourceID, *(createdResource.ID))
+
+	// Looking for a created resource
+	resources, err := client.GetResourcesClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		gocloak.GetResourceParams{
+			Name: createdResource.Name,
+		},
+	)
+	require.NoError(t, err, "GetResources failed")
+	require.Len(t, resources, 1, "GetResources should return exact 1 resource")
+	require.Equal(t, *(createdResource.ID), *(resources[0].ID))
+	t.Logf("Resources: %+v", resources)
+
+	err = client.UpdateResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		gocloak.ResourceRepresentation{},
+	)
+	require.Error(t, err, "Should fail because of missing ID of the resource")
+
+	createdResource.Name = GetRandomNameP("ResourceName")
+
+	err = client.UpdateResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		*createdResource,
+	)
+	require.NoError(t, err, "UpdateResource failed")
+
+	updatedResource, err := client.GetResourceClient(
+		context.Background(),
+		token.AccessToken,
+		cfg.GoCloak.Realm,
+		gocloakClientID,
+		resourceID,
+	)
+	require.NoError(t, err, "GetResource failed")
+	require.Equal(t, *(createdResource.Name), *(updatedResource.Name))
+
+}
+
 func TestGocloak_CreateListGetUpdateDeleteResource(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)