From 04f3ff43ffe1b5683374a05f8b83fb63ad601589 Mon Sep 17 00:00:00 2001 From: Philipp Nowak Date: Tue, 10 May 2022 10:51:56 +0200 Subject: [PATCH] Add missing endpoints for client scopes -> scope mappings -> client roles (#348) #348 Add missing endpoints for client scopes -> scope mappings -> client roles --- README.md | 4 ++ client.go | 62 +++++++++++++++++++++++++++ client_test.go | 112 ++++++++++++++++++++++++++++++++++++++++++++++++- gocloak.go | 8 ++++ 4 files changed, 184 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c7b2c6ac..7df6e9ab 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ type GoCloak interface { CreateClientScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfClient string, roles []Role) error CreateClientScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClient, idOfSelectedClient string, roles []Role) error CreateClientScopesScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfCLientScope string, roles []Role) error + CreateClientScopesScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClientScope, idOfClient string, roles []Role) error UpdateUser(ctx context.Context, accessToken, realm string, user User) error UpdateGroup(ctx context.Context, accessToken, realm string, updatedGroup Group) error @@ -177,6 +178,7 @@ type GoCloak interface { DeleteClientScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfClient string, roles []Role) error DeleteClientScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClient, idOfSelectedClient string, roles []Role) error DeleteClientScopesScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfCLientScope string, roles []Role) error + DeleteClientScopesScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClientScope, ifOfClient string, roles []Role) error GetClient(ctx context.Context, accessToken, realm, idOfClient string) (*Client, error) GetClientsDefaultScopes(ctx context.Context, token, realm, idOfClient string) ([]*ClientScope, error) @@ -193,8 +195,10 @@ type GoCloak interface { GetClientScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfClient string) ([]*Role, error) GetClientScopeMappingsRealmRolesAvailable(ctx context.Context, token, realm, idOfClient string) ([]*Role, error) GetClientScopesScopeMappingsRealmRolesAvailable(ctx context.Context, token, realm, idOfClientScope string) ([]*Role, error) + GetClientScopesScopeMappingsClientRolesAvailable(ctx context.Context, token, realm, idOfClientScope, idOfClient string) ([]*Role, error) GetClientScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClient, idOfSelectedClient string) ([]*Role, error) GetClientScopesScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfClientScope string) ([]*Role, error) + GetClientScopesScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClientScope, idOfClient string) ([]*Role, error) GetClientScopeMappingsClientRolesAvailable(ctx context.Context, token, realm, idOfClient, idOfSelectedClient string) ([]*Role, error) GetClientSecret(ctx context.Context, token, realm, idOfClient string) (*CredentialRepresentation, error) GetClientServiceAccount(ctx context.Context, token, realm, idOfClient string) (*User, error) diff --git a/client.go b/client.go index 98c7c1a4..6447f74f 100644 --- a/client.go +++ b/client.go @@ -3777,3 +3777,65 @@ func (client *gocloak) UpdateRequiredAction(ctx context.Context, token string, r return err } + +// CreateClientScopesScopeMappingsClientRoles attaches a client role to a client scope (not client's scope) +func (client *gocloak) CreateClientScopesScopeMappingsClientRoles( + ctx context.Context, token, realm, idOfClientScope, idOfClient string, roles []Role, +) error { + const errMessage = "could not create client-level roles to the client-scope" + + resp, err := client.getRequestWithBearerAuth(ctx, token). + SetBody(roles). + Post(client.getAdminRealmURL(realm, "client-scopes", idOfClientScope, "scope-mappings", "clients", idOfClient)) + + return checkForError(resp, err, errMessage) +} + +// GetClientScopesScopeMappingsClientRolesAvailable returns available (i.e. not attached via +// CreateClientScopesScopeMappingsClientRoles) client roles for a specific client, for a client scope +// (not client's scope). +func (client *gocloak) GetClientScopesScopeMappingsClientRolesAvailable(ctx context.Context, token, realm, idOfClientScope, idOfClient string) ([]*Role, error) { + const errMessage = "could not get available client-level roles with the client-scope" + + var result []*Role + + resp, err := client.getRequestWithBearerAuth(ctx, token). + SetResult(&result). + Get(client.getAdminRealmURL(realm, "client-scopes", idOfClientScope, "scope-mappings", "clients", idOfClient, "available")) + + if err := checkForError(resp, err, errMessage); err != nil { + return nil, err + } + + return result, nil +} + +// GetClientScopesScopeMappingsClientRoles returns attached client roles for a specific client, for a client scope +// (not client's scope). +func (client *gocloak) GetClientScopesScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClientScope, idOfClient string) ([]*Role, error) { + const errMessage = "could not get client-level roles with the client-scope" + + var result []*Role + + resp, err := client.getRequestWithBearerAuth(ctx, token). + SetResult(&result). + Get(client.getAdminRealmURL(realm, "client-scopes", idOfClientScope, "scope-mappings", "clients", idOfClient)) + + if err := checkForError(resp, err, errMessage); err != nil { + return nil, err + } + + return result, nil +} + +// DeleteClientScopesScopeMappingsClientRoles removes attachment of client roles from a client scope +// (not client's scope). +func (client *gocloak) DeleteClientScopesScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClientScope, idOfClient string, roles []Role) error { + const errMessage = "could not delete client-level roles from the client-scope" + + resp, err := client.getRequestWithBearerAuth(ctx, token). + SetBody(roles). + Delete(client.getAdminRealmURL(realm, "client-scopes", idOfClientScope, "scope-mappings", "clients", idOfClient)) + + return checkForError(resp, err, errMessage) +} diff --git a/client_test.go b/client_test.go index 6d94ff0d..6a4209d2 100644 --- a/client_test.go +++ b/client_test.go @@ -1665,7 +1665,7 @@ func Test_ClientScopeMappingsClientRoles(t *testing.T) { token := GetAdminToken(t, client) testClient := gocloak.Client{ ClientID: GetRandomNameP("ClientID"), - BaseURL: gocloak.StringP("http://example.com"), + BaseURL: gocloak.StringP("https://example.com"), FullScopeAllowed: gocloak.BoolP(false), } // Creating client @@ -1811,6 +1811,112 @@ func Test_ClientScopeMappingsRealmRoles(t *testing.T) { ) } +func CreateClientScopesMappingsClientRoles( + t *testing.T, client gocloak.GoCloak, scopeID, idOfClient string, roles []gocloak.Role, +) func() { + token := GetAdminToken(t, client) + cfg := GetConfig(t) + + // Creating client scope mappings + err := client.CreateClientScopesScopeMappingsClientRoles( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + scopeID, + idOfClient, + roles, + ) + require.NoError(t, err, "CreateClientScopesScopeMappingsClientRoles failed") + + tearDown := func() { + err = client.DeleteClientScopesScopeMappingsClientRoles( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + scopeID, + idOfClient, + roles, + ) + require.NoError(t, err, "DeleteClientScopesScopeMappingsClientRoles failed") + } + return tearDown +} + +// Test_ClientScopesMappingsClientRoles tests API calls related to client role attachment for a client scope. +func Test_ClientScopesMappingsClientRoles(t *testing.T) { + cfg := GetConfig(t) + client := NewClientWithDebug(t) + token := GetAdminToken(t, client) + + // Creating client roles (on shared client) + var roles []gocloak.Role + tearDownRole1, assignRoleName := CreateClientRole(t, client) + defer tearDownRole1() + role, err := client.GetClientRole( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + gocloakClientID, + assignRoleName, + ) + require.NoError(t, err, "CreateClientRole failed") + roles = append(roles, *role) + tearDownRole2, noAssignRoleName := CreateClientRole(t, client) + defer tearDownRole2() + role, err = client.GetClientRole( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + gocloakClientID, + noAssignRoleName, + ) + require.NoError(t, err, "GetClientRole after CreateClientRole failed") + roles = append(roles, *role) + + // Creating scope + tearDownScope, scopeID := CreateClientScope(t, client, nil) + defer tearDownScope() + + // Creating client roles for client scope mappings + onlyFirstRole := roles[:1] + tearDownMappings := CreateClientScopesMappingsClientRoles(t, client, scopeID, gocloakClientID, onlyFirstRole) + defer tearDownMappings() + + // Check client roles + mappedRoles, err := client.GetClientScopesScopeMappingsClientRoles( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + scopeID, + gocloakClientID, + ) + require.NoError(t, err, "GetClientScopesScopeMappingsClientRoles failed") + require.Len( + t, mappedRoles, len(onlyFirstRole), + "GetClientScopeMappingsClientRoles should return exact %s roles", len(onlyFirstRole), + ) + + clientRolesAvailable, err := client.GetClientScopesScopeMappingsClientRolesAvailable( + context.Background(), + token.AccessToken, + cfg.GoCloak.Realm, + scopeID, + gocloakClientID, + ) + require.NoError(t, err, "GetClientScopesScopeMappingsClientRolesAvailable failed") + foundUnassignedRole := false + for _, roleAvailable := range clientRolesAvailable { + require.NotEqual( + t, assignRoleName, roleAvailable.Name, + "assigned role %v should not be available", assignRoleName, + ) + if *roleAvailable.Name == noAssignRoleName { + foundUnassignedRole = true + } + } + require.True(t, foundUnassignedRole, "expected role %s to be available", noAssignRoleName) +} + func Test_CreateListGetUpdateDeleteClient(t *testing.T) { // t.Parallel() cfg := GetConfig(t) @@ -5306,7 +5412,9 @@ func Test_CreateGetUpdateDeleteResourcePolicy(t *testing.T) { Name: policyNameP, Description: gocloak.StringP("Role Policy"), Scopes: &scopes, - Roles: &[]string{roleName}, + // "gocloak" is the client name here, apparently it's necessary to scope client roles like that here. + // ref: https://github.com/keycloak/keycloak/blob/main/core/src/main/java/org/keycloak/representations/idm/authorization/UmaPermissionRepresentation.java#L53 + Roles: &[]string{fmt.Sprintf("gocloak/%v", roleName)}, }, { Name: policyNameP, diff --git a/gocloak.go b/gocloak.go index a4788548..91c4af32 100644 --- a/gocloak.go +++ b/gocloak.go @@ -86,6 +86,8 @@ type GoCloak interface { CreateClientScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClient, idOfSelectedClient string, roles []Role) error // CreateClientScopesScopeMappingsRealmRoles creates realm-level roles to the client-scope CreateClientScopesScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfClientScope string, roles []Role) error + // CreateClientScopesScopeMappingsClientRoles creates client-level roles to the client-scope + CreateClientScopesScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClientScope, idOfClient string, roles []Role) error // CreateClientRepresentation creates a new client representation CreateClientRepresentation(ctx context.Context, realm string) (*Client, error) @@ -118,6 +120,8 @@ type GoCloak interface { DeleteClientScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClient, idOfSelectedClient string, roles []Role) error // DeleteClientScopesScopeMappingsRealmRoles deletes realm-level roles from the client-scope DeleteClientScopesScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfClientScope string, roles []Role) error + // DeleteClientScopesScopeMappingsClientRoles deletes client-level roles from the client-scope + DeleteClientScopesScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClientScope, ifOfClient string, roles []Role) error // DeleteClientRepresentation deletes a given client representation DeleteClientRepresentation(ctx context.Context, accessToken, realm, clientID string) error @@ -155,10 +159,14 @@ type GoCloak interface { GetClientScopeMappingsRealmRolesAvailable(ctx context.Context, token, realm, idOfClient string) ([]*Role, error) // GetClientScopesScopeMappingsRealmRolesAvailable returns realm-level roles that are available to attach to this client-scope GetClientScopesScopeMappingsRealmRolesAvailable(ctx context.Context, token, realm, idOfClientScope string) ([]*Role, error) + // GetClientScopesScopeMappingsClientRolesAvailable returns client-level roles that are available to attach to this client-scope + GetClientScopesScopeMappingsClientRolesAvailable(ctx context.Context, token, realm, idOfClientScope, idOfClient string) ([]*Role, error) // GetClientScopeMappingsClientRoles returns roles associated with a client’s scope GetClientScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClient, idOfSelectedClient string) ([]*Role, error) // GetClientScopesScopeMappingsRealmRoles returns roles associated with a client-scope GetClientScopesScopeMappingsRealmRoles(ctx context.Context, token, realm, idOfClientScope string) ([]*Role, error) + // GetClientScopesScopeMappingsClientRoles returns client roles associated with a client-scope + GetClientScopesScopeMappingsClientRoles(ctx context.Context, token, realm, idOfClientScope, idOfClient string) ([]*Role, error) // GetClientScopeMappingsClientRolesAvailable returns available roles associated with a client’s scope GetClientScopeMappingsClientRolesAvailable(ctx context.Context, token, realm, idOfClient, idOfSelectedClient string) ([]*Role, error) // GetClientSecret returns a client's secret