Skip to content

Commit

Permalink
Added APIs to support fine-grained authorization (#432)
Browse files Browse the repository at this point in the history
* Added APIs to support fine-grained authorization

* fixed linting error

* Added test cases
Enabled admin-fine-grained-authz feature
Replaced sleep 10 with loop to wait for health endpoint availability

* Added test cases
Enabled admin-fine-grained-authz feature
Replaced sleep 10 with loop to wait for health endpoint availability
  • Loading branch information
dmartinol authored Jul 10, 2023
1 parent 166f442 commit f08754e
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ ENV KEYCLOAK_USER=admin
ENV KEYCLOAK_PASSWORD=secret
ENV KEYCLOAK_ADMIN=admin
ENV KEYCLOAK_ADMIN_PASSWORD=secret
ENV KC_FEATURES=account-api,account2,authorization,client-policies,impersonation,docker,scripts,upload_scripts
ENV KC_FEATURES=account-api,account2,authorization,client-policies,impersonation,docker,scripts,upload_scripts,admin-fine-grained-authz
RUN /opt/keycloak/bin/kc.sh import --file /data/import/gocloak-realm.json
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
98 changes: 98 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,24 @@ func (g *GoCloak) UpdateGroup(ctx context.Context, token, realm string, updatedG
return checkForError(resp, err, errMessage)
}

// UpdateGroupManagementPermissions updates the given group management permissions
func (g *GoCloak) UpdateGroupManagementPermissions(ctx context.Context, accessToken, realm string, idOfGroup string, managementPermissions ManagementPermissionRepresentation) (*ManagementPermissionRepresentation, error) {
const errMessage = "could not update group management permissions"

var result ManagementPermissionRepresentation

resp, err := g.GetRequestWithBearerAuth(ctx, accessToken).
SetResult(&result).
SetBody(managementPermissions).
Put(g.getAdminRealmURL(realm, "groups", idOfGroup, "management", "permissions"))

if err := checkForError(resp, err, errMessage); err != nil {
return nil, err
}

return &result, nil
}

// UpdateClient updates the given Client
func (g *GoCloak) UpdateClient(ctx context.Context, token, realm string, updatedClient Client) error {
const errMessage = "could not update client"
Expand Down Expand Up @@ -906,6 +924,24 @@ func (g *GoCloak) UpdateClientRepresentation(ctx context.Context, accessToken, r
return &result, nil
}

// UpdateClientManagementPermissions updates the given client management permissions
func (g *GoCloak) UpdateClientManagementPermissions(ctx context.Context, accessToken, realm string, idOfClient string, managementPermissions ManagementPermissionRepresentation) (*ManagementPermissionRepresentation, error) {
const errMessage = "could not update client management permissions"

var result ManagementPermissionRepresentation

resp, err := g.GetRequestWithBearerAuth(ctx, accessToken).
SetResult(&result).
SetBody(managementPermissions).
Put(g.getAdminRealmURL(realm, "clients", idOfClient, "management", "permissions"))

if err := checkForError(resp, err, errMessage); err != nil {
return nil, err
}

return &result, nil
}

// UpdateRole updates the given role.
func (g *GoCloak) UpdateRole(ctx context.Context, token, realm, idOfClient string, role Role) error {
const errMessage = "could not update role"
Expand Down Expand Up @@ -1682,6 +1718,23 @@ func (g *GoCloak) GetGroups(ctx context.Context, token, realm string, params Get
return result, nil
}

// GetGroupManagementPermissions returns whether group Authorization permissions have been initialized or not and a reference
// to the managed permissions
func (g *GoCloak) GetGroupManagementPermissions(ctx context.Context, token, realm string, idOfGroup string) (*ManagementPermissionRepresentation, error) {
const errMessage = "could not get management permissions"

var result ManagementPermissionRepresentation
resp, err := g.GetRequestWithBearerAuth(ctx, token).
SetResult(&result).
Get(g.getAdminRealmURL(realm, "groups", idOfGroup, "management", "permissions"))

if err := checkForError(resp, err, errMessage); err != nil {
return nil, err
}

return &result, nil
}

// GetGroupsByRole gets groups assigned with a specific role of a realm
func (g *GoCloak) GetGroupsByRole(ctx context.Context, token, realm string, roleName string) ([]*Group, error) {
const errMessage = "could not get groups"
Expand Down Expand Up @@ -1944,6 +1997,23 @@ func (g *GoCloak) GetClients(ctx context.Context, token, realm string, params Ge
return result, nil
}

// GetClientManagementPermissions returns whether client Authorization permissions have been initialized or not and a reference
// to the managed permissions
func (g *GoCloak) GetClientManagementPermissions(ctx context.Context, token, realm string, idOfClient string) (*ManagementPermissionRepresentation, error) {
const errMessage = "could not get management permissions"

var result ManagementPermissionRepresentation
resp, err := g.GetRequestWithBearerAuth(ctx, token).
SetResult(&result).
Get(g.getAdminRealmURL(realm, "clients", idOfClient, "management", "permissions"))

if err := checkForError(resp, err, errMessage); err != nil {
return nil, err
}

return &result, nil
}

// UserAttributeContains checks if the given attribute value is set
func UserAttributeContains(attributes map[string][]string, attribute, value string) bool {
for _, item := range attributes[attribute] {
Expand Down Expand Up @@ -3316,6 +3386,34 @@ func (g *GoCloak) CreateScope(ctx context.Context, token, realm, idOfClient stri
return &result, nil
}

// GetPermissionScope gets the permission scope associated with the client
func (g *GoCloak) GetPermissionScope(ctx context.Context, token, realm, idOfClient string, idOfScope string) (*PolicyRepresentation, error) {
const errMessage = "could not get permission scope"

var result PolicyRepresentation
resp, err := g.GetRequestWithBearerAuth(ctx, token).
SetResult(&result).
SetBody(result).
Get(g.getAdminRealmURL(realm, "clients", idOfClient, "authz", "resource-server", "permission", "scope", idOfScope))

if err := checkForError(resp, err, errMessage); err != nil {
return nil, err
}

return &result, nil
}

// UpdatePermissionScope updates a permission scope associated with the client
func (g *GoCloak) UpdatePermissionScope(ctx context.Context, token, realm, idOfClient string, idOfScope string, policy PolicyRepresentation) error {
const errMessage = "could not create permission scope"

resp, err := g.GetRequestWithBearerAuth(ctx, token).
SetBody(policy).
Put(g.getAdminRealmURL(realm, "clients", idOfClient, "authz", "resource-server", "permission", "scope", idOfScope))

return checkForError(resp, err, errMessage)
}

// UpdateScope updates a scope associated with the client
func (g *GoCloak) UpdateScope(ctx context.Context, token, realm, idOfClient string, scope ScopeRepresentation) error {
const errMessage = "could not update scope"
Expand Down
133 changes: 133 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,103 @@ func Test_CreateListGetUpdateDeleteGetChildGroup(t *testing.T) {
require.NoError(t, err, "GetGroup failed")
}

func Test_GroupPermissions(t *testing.T) {
cfg := GetConfig(t)
client := NewClientWithDebug(t)
token := GetAdminToken(t, client)

// Create
tearDown, groupID := CreateGroup(t, client)
// Delete
defer tearDown()

groupPermission, err := client.GetGroupManagementPermissions(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
groupID,
)
require.NoError(t, err, "GetGroupManagementPermissions failed")
require.Equal(t, false, *groupPermission.Enabled)

groupPermission.Enabled = gocloak.BoolP(true)
updatedGroupPermission, err := client.UpdateGroupManagementPermissions(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
groupID,
*groupPermission,
)
require.NoError(t, err, "UpdateGroupManagementPermissions failed")
require.Equal(t, true, *updatedGroupPermission.Enabled)

clients, err := client.GetClients(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
gocloak.GetClientsParams{
ClientID: gocloak.StringP("realm-management"),
},
)
require.NoError(t, err, "GetClients failed")
require.Equal(t, 1, len(clients))
realManagementClient := clients[0]

_, policyID := CreatePolicy(t, client, gocloakClientID, gocloak.PolicyRepresentation{
Name: GetRandomNameP("PolicyName"),
Description: gocloak.StringP("Policy Description"),
Type: gocloak.StringP("client"),
Logic: gocloak.POSITIVE,
ClientPolicyRepresentation: gocloak.ClientPolicyRepresentation{
Clients: &[]string{
gocloakClientID,
},
},
})

for _, scopeID := range *updatedGroupPermission.ScopePermissions {
permissionScope, err := client.GetPermissionScope(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
*realManagementClient.ID,
scopeID)
require.NoError(t, err, "GetPermissionScope failed for %s", scopeID)

scopePolicies, err := client.GetAuthorizationPolicyScopes(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
*realManagementClient.ID,
scopeID)
require.NoError(t, err, "GetAuthorizationPolicyScopes failed for %s", scopeID)
require.Equal(t, 1, len(scopePolicies), "GetAuthorizationPolicyScopes found more than 1 policies")
scopePolicy := scopePolicies[0]

policyResources, err := client.GetAuthorizationPolicyResources(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
*realManagementClient.ID,
scopeID)
require.NoError(t, err, "GetAuthorizationPolicyResources failed for %s", scopeID)
require.Equal(t, 1, len(policyResources), "GetAuthorizationPolicyResources found more than 1 policies")
policyResource := policyResources[0]

permissionScope.Policies = &[]string{policyID}
permissionScope.Resources = &[]string{*policyResource.ID}
permissionScope.Scopes = &[]string{*scopePolicy.ID}
err = client.UpdatePermissionScope(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
*realManagementClient.ID,
scopeID,
*permissionScope)
require.NoError(t, err, "UpdatePermissionScope failed for %s", scopeID)
}
}

func CreateClientRole(t *testing.T, client *gocloak.GoCloak) (func(), string) {
cfg := GetConfig(t)
token := GetAdminToken(t, client)
Expand Down Expand Up @@ -1226,6 +1323,42 @@ func CreateClientRole(t *testing.T, client *gocloak.GoCloak) (func(), string) {
return tearDown, roleName
}

func Test_ClientPermissions(t *testing.T) {
cfg := GetConfig(t)
client := NewClientWithDebug(t)
token := GetAdminToken(t, client)

t.Logf("Checking Client Permission")
testClient := gocloak.Client{
ClientID: GetRandomNameP("ClientID"),
BaseURL: gocloak.StringP("https://example.com"),
FullScopeAllowed: gocloak.BoolP(false),
}
// Creating client
tearDownClient, idOfClient := CreateClient(t, client, &testClient)
defer tearDownClient()

clientPermissions, err := client.GetClientManagementPermissions(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
idOfClient,
)
require.NoError(t, err, "GetClientManagementPermissions failed")
require.Equal(t, false, *clientPermissions.Enabled)

clientPermissions.Enabled = gocloak.BoolP(true)
updatedClientPermissions, err := client.UpdateClientManagementPermissions(
context.Background(),
token.AccessToken,
cfg.GoCloak.Realm,
idOfClient,
*clientPermissions,
)
require.NoError(t, err, "UpdateClientManagementPermissions failed")
require.Equal(t, true, *updatedClientPermissions.Enabled)
}

func Test_CreateClientRole(t *testing.T) {
t.Parallel()
client := NewClientWithDebug(t)
Expand Down
8 changes: 8 additions & 0 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,14 @@ type RequiredActionProviderRepresentation struct {
ProviderID *string `json:"providerId,omitempty"`
}

// ManagementPermissionRepresentation is a representation of management permissions
// v18: https://www.keycloak.org/docs-api/18.0/rest-api/#_managementpermissionreference
type ManagementPermissionRepresentation struct {
Enabled *bool `json:"enabled,omitempty"`
Resource *string `json:"resource,omitempty"`
ScopePermissions *map[string]string `json:"scopePermissions,omitempty"`
}

// prettyStringStruct returns struct formatted into pretty string
func prettyStringStruct(t interface{}) string {
json, err := json.MarshalIndent(t, "", "\t")
Expand Down
14 changes: 12 additions & 2 deletions run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
docker-compose down
docker-compose up -d

sleep 10
keycloakServer=http://localhost:8080
url="${keycloakServer}/health"
echo "Checking service availability at $url (CTRL+C to exit)"
while true; do
response=$(curl -s -o /dev/null -w "%{http_code}" $url)
if [ $response -eq 200 ]; then
break
fi
sleep 1
done
echo "Service is now available at ${keycloakServer}"

ARGS=()
if [ $# -gt 0 ]; then
Expand All @@ -13,4 +23,4 @@ fi

go test -failfast -race -cover -coverprofile=coverage.out -covermode=atomic -p 10 -cpu 1,2 -bench . -benchmem ${ARGS[@]}

docker-compose down
docker-compose down

0 comments on commit f08754e

Please sign in to comment.