KEYCLOAK-17637 Client Scope Policy for authorization service
This commit is contained in:
parent
6d17117f42
commit
45202bd49a
20 changed files with 1074 additions and 10 deletions
|
@ -188,7 +188,12 @@ public class AuthzClient {
|
||||||
* @return a {@link AuthorizationResource}
|
* @return a {@link AuthorizationResource}
|
||||||
*/
|
*/
|
||||||
public AuthorizationResource authorization(final String userName, final String password) {
|
public AuthorizationResource authorization(final String userName, final String password) {
|
||||||
return new AuthorizationResource(configuration, serverConfiguration, this.http, createRefreshableAccessTokenSupplier(userName, password));
|
return authorization(userName, password, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationResource authorization(final String userName, final String password, final String scope) {
|
||||||
|
return new AuthorizationResource(configuration, serverConfiguration, this.http,
|
||||||
|
createRefreshableAccessTokenSupplier(userName, password, scope));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -276,6 +281,11 @@ public class AuthzClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private TokenCallable createRefreshableAccessTokenSupplier(final String userName, final String password) {
|
private TokenCallable createRefreshableAccessTokenSupplier(final String userName, final String password) {
|
||||||
return new TokenCallable(userName, password, http, configuration, serverConfiguration);
|
return createRefreshableAccessTokenSupplier(userName, password, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenCallable createRefreshableAccessTokenSupplier(final String userName, final String password,
|
||||||
|
final String scope) {
|
||||||
|
return new TokenCallable(userName, password, scope, http, configuration, serverConfiguration);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -48,10 +48,16 @@ public class HttpMethodAuthenticator<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpMethod<R> oauth2ResourceOwnerPassword(String userName, String password) {
|
public HttpMethod<R> oauth2ResourceOwnerPassword(String userName, String password) {
|
||||||
|
return oauth2ResourceOwnerPassword(userName, password, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpMethod<R> oauth2ResourceOwnerPassword(String userName, String password, String scope) {
|
||||||
client();
|
client();
|
||||||
this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.PASSWORD));
|
this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.PASSWORD));
|
||||||
this.method.params.put("username", Arrays.asList(userName));
|
this.method.params.put("username", Arrays.asList(userName));
|
||||||
this.method.params.put("password", Arrays.asList(password));
|
this.method.params.put("password", Arrays.asList(password));
|
||||||
|
if (scope != null)
|
||||||
|
this.method.params.put("scope", Arrays.asList(scope));
|
||||||
return this.method;
|
return this.method;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,19 +33,27 @@ public class TokenCallable implements Callable<String> {
|
||||||
private static Logger log = Logger.getLogger(TokenCallable.class);
|
private static Logger log = Logger.getLogger(TokenCallable.class);
|
||||||
private final String userName;
|
private final String userName;
|
||||||
private final String password;
|
private final String password;
|
||||||
|
private final String scope;
|
||||||
private final Http http;
|
private final Http http;
|
||||||
private final Configuration configuration;
|
private final Configuration configuration;
|
||||||
private final ServerConfiguration serverConfiguration;
|
private final ServerConfiguration serverConfiguration;
|
||||||
private AccessTokenResponse tokenResponse;
|
private AccessTokenResponse tokenResponse;
|
||||||
|
|
||||||
public TokenCallable(String userName, String password, Http http, Configuration configuration, ServerConfiguration serverConfiguration) {
|
public TokenCallable(String userName, String password, String scope, Http http, Configuration configuration,
|
||||||
|
ServerConfiguration serverConfiguration) {
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
|
this.scope = scope;
|
||||||
this.http = http;
|
this.http = http;
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
this.serverConfiguration = serverConfiguration;
|
this.serverConfiguration = serverConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TokenCallable(String userName, String password, Http http, Configuration configuration,
|
||||||
|
ServerConfiguration serverConfiguration) {
|
||||||
|
this(userName, password, null, http, configuration, serverConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
public TokenCallable(Http http, Configuration configuration, ServerConfiguration serverConfiguration) {
|
public TokenCallable(Http http, Configuration configuration, ServerConfiguration serverConfiguration) {
|
||||||
this(null, null, http, configuration, serverConfiguration);
|
this(null, null, http, configuration, serverConfiguration);
|
||||||
}
|
}
|
||||||
|
@ -121,12 +129,12 @@ public class TokenCallable implements Callable<String> {
|
||||||
* @return an {@link AccessTokenResponse}
|
* @return an {@link AccessTokenResponse}
|
||||||
*/
|
*/
|
||||||
AccessTokenResponse resourceOwnerPasswordGrant(String userName, String password) {
|
AccessTokenResponse resourceOwnerPasswordGrant(String userName, String password) {
|
||||||
return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
|
return resourceOwnerPasswordGrant(userName, password, null);
|
||||||
.authentication()
|
}
|
||||||
.oauth2ResourceOwnerPassword(userName, password)
|
|
||||||
.response()
|
AccessTokenResponse resourceOwnerPasswordGrant(String userName, String password, String scope) {
|
||||||
.json(AccessTokenResponse.class)
|
return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint()).authentication()
|
||||||
.execute();
|
.oauth2ResourceOwnerPassword(userName, password, scope).response().json(AccessTokenResponse.class).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccessTokenResponse refreshToken(String rawRefreshToken) {
|
private AccessTokenResponse refreshToken(String rawRefreshToken) {
|
||||||
|
@ -144,6 +152,8 @@ public class TokenCallable implements Callable<String> {
|
||||||
private AccessTokenResponse obtainTokens() {
|
private AccessTokenResponse obtainTokens() {
|
||||||
if (userName == null || password == null) {
|
if (userName == null || password == null) {
|
||||||
return clientCredentialsGrant();
|
return clientCredentialsGrant();
|
||||||
|
} else if (scope != null) {
|
||||||
|
return resourceOwnerPasswordGrant(userName, password, scope);
|
||||||
} else {
|
} else {
|
||||||
return resourceOwnerPasswordGrant(userName, password);
|
return resourceOwnerPasswordGrant(userName, password);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package org.keycloak.authorization.policy.provider.clientscope;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
|
import org.keycloak.authorization.identity.Identity;
|
||||||
|
import org.keycloak.authorization.model.Policy;
|
||||||
|
import org.keycloak.authorization.policy.evaluation.Evaluation;
|
||||||
|
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||||
|
import org.keycloak.models.ClientScopeModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.representations.idm.authorization.ClientScopePolicyRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
|
||||||
|
*/
|
||||||
|
public class ClientScopePolicyProvider implements PolicyProvider {
|
||||||
|
|
||||||
|
private final BiFunction<Policy, AuthorizationProvider, ClientScopePolicyRepresentation> representationFunction;
|
||||||
|
|
||||||
|
public ClientScopePolicyProvider(
|
||||||
|
BiFunction<Policy, AuthorizationProvider, ClientScopePolicyRepresentation> representationFunction) {
|
||||||
|
this.representationFunction = representationFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void evaluate(Evaluation evaluation) {
|
||||||
|
Policy policy = evaluation.getPolicy();
|
||||||
|
Set<ClientScopePolicyRepresentation.ClientScopeDefinition> clientScopeIds = representationFunction
|
||||||
|
.apply(policy, evaluation.getAuthorizationProvider()).getClientScopes();
|
||||||
|
AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider();
|
||||||
|
RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm();
|
||||||
|
Identity identity = evaluation.getContext().getIdentity();
|
||||||
|
|
||||||
|
for (ClientScopePolicyRepresentation.ClientScopeDefinition clientScopeDefinition : clientScopeIds) {
|
||||||
|
ClientScopeModel clientScope = realm.getClientScopeById(clientScopeDefinition.getId());
|
||||||
|
|
||||||
|
if (clientScope != null) {
|
||||||
|
boolean hasClientScope = hasClientScope(identity, clientScope);
|
||||||
|
|
||||||
|
if (!hasClientScope && clientScopeDefinition.isRequired()) {
|
||||||
|
evaluation.deny();
|
||||||
|
return;
|
||||||
|
} else if (hasClientScope) {
|
||||||
|
evaluation.grant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasClientScope(Identity identity, ClientScopeModel clientScope) {
|
||||||
|
String clientScopeName = clientScope.getName();
|
||||||
|
String[] clientScopes = identity.getAttributes().getValue("scope").asString(0).split(" ");
|
||||||
|
for (String scope : clientScopes) {
|
||||||
|
if (clientScopeName.equals(scope))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package org.keycloak.authorization.policy.provider.clientscope;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
|
import org.keycloak.authorization.model.Policy;
|
||||||
|
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||||
|
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
||||||
|
import org.keycloak.authorization.store.PolicyStore;
|
||||||
|
import org.keycloak.authorization.store.StoreFactory;
|
||||||
|
import org.keycloak.models.ClientScopeModel;
|
||||||
|
import org.keycloak.models.ClientScopeModel.ClientScopeRemovedEvent;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.representations.idm.authorization.ClientScopePolicyRepresentation;
|
||||||
|
import org.keycloak.representations.idm.authorization.ClientScopePolicyRepresentation.ClientScopeDefinition;
|
||||||
|
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
|
||||||
|
*/
|
||||||
|
public class ClientScopePolicyProviderFactory implements PolicyProviderFactory<ClientScopePolicyRepresentation> {
|
||||||
|
|
||||||
|
private ClientScopePolicyProvider provider = new ClientScopePolicyProvider(this::toRepresentation);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PolicyProvider create(KeycloakSession session) {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Scope config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
factory.register(event -> {
|
||||||
|
if (event instanceof ClientScopeRemovedEvent) {
|
||||||
|
KeycloakSession keycloakSession = ((ClientScopeRemovedEvent) event).getKeycloakSession();
|
||||||
|
AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
|
||||||
|
StoreFactory storeFactory = provider.getStoreFactory();
|
||||||
|
PolicyStore policyStore = storeFactory.getPolicyStore();
|
||||||
|
ClientScopeModel removedClientScope = ((ClientScopeRemovedEvent) event).getClientScope();
|
||||||
|
|
||||||
|
Map<Policy.FilterOption, String[]> filters = new HashMap<>();
|
||||||
|
|
||||||
|
filters.put(Policy.FilterOption.TYPE, new String[] { getId() });
|
||||||
|
|
||||||
|
policyStore.findByResourceServer(filters, null, -1, -1).forEach(new Consumer<Policy>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(Policy policy) {
|
||||||
|
List<Map<String, Object>> clientScopes = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Map<String, Object> clientScope : getClientScopes(policy)) {
|
||||||
|
if (!clientScope.get("id").equals(removedClientScope.getId())) {
|
||||||
|
Map<String, Object> updated = new HashMap<>();
|
||||||
|
updated.put("id", clientScope.get("id"));
|
||||||
|
Object required = clientScope.get("required");
|
||||||
|
if (required != null) {
|
||||||
|
updated.put("required", required);
|
||||||
|
}
|
||||||
|
clientScopes.add(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientScopes.isEmpty()) {
|
||||||
|
policyStore.delete(policy.getId());
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
policy.putConfig("clientScopes", JsonSerialization.writeValueAsString(clientScopes));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Error while synchronizing client scopes with policy [" + policy.getName() + "].", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object>[] getClientScopes(Policy policy) {
|
||||||
|
String clientScopes = policy.getConfig().get("clientScopes");
|
||||||
|
|
||||||
|
if (clientScopes != null) {
|
||||||
|
try {
|
||||||
|
return JsonSerialization.readValue(clientScopes.getBytes(), Map[].class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Could not parse client scopes [" + clientScopes + "] from policy config [" + policy.getName() + "].", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Map[] {};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "client-scope";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Client Scope";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGroup() {
|
||||||
|
return "Identity Based";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PolicyProvider create(AuthorizationProvider authorization) {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientScopePolicyRepresentation toRepresentation(Policy policy, AuthorizationProvider authorization) {
|
||||||
|
ClientScopePolicyRepresentation representation = new ClientScopePolicyRepresentation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
representation
|
||||||
|
.setClientScopes(new HashSet<>(Arrays.asList(JsonSerialization.readValue(policy.getConfig().get("clientScopes"),
|
||||||
|
ClientScopePolicyRepresentation.ClientScopeDefinition[].class))));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to deserialize client scopes", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return representation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<ClientScopePolicyRepresentation> getRepresentationType() {
|
||||||
|
return ClientScopePolicyRepresentation.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Policy policy, ClientScopePolicyRepresentation representation, AuthorizationProvider authorization) {
|
||||||
|
updateClientScopes(policy, representation, authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpdate(Policy policy, ClientScopePolicyRepresentation representation, AuthorizationProvider authorization) {
|
||||||
|
updateClientScopes(policy, representation, authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) {
|
||||||
|
try {
|
||||||
|
updateClientScopes(policy, authorization,
|
||||||
|
new HashSet<>(Arrays.asList(JsonSerialization.readValue(representation.getConfig().get("clientScopes"),
|
||||||
|
ClientScopePolicyRepresentation.ClientScopeDefinition[].class))));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to deserialize client scopes during import", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
|
||||||
|
Map<String, String> config = new HashMap<>();
|
||||||
|
Set<ClientScopePolicyRepresentation.ClientScopeDefinition> clientScopes = toRepresentation(policy,
|
||||||
|
authorizationProvider).getClientScopes();
|
||||||
|
|
||||||
|
for (ClientScopePolicyRepresentation.ClientScopeDefinition clientScopeDefinition : clientScopes) {
|
||||||
|
ClientScopeModel clientScope = authorizationProvider.getRealm().getClientScopeById(clientScopeDefinition.getId());
|
||||||
|
|
||||||
|
clientScopeDefinition.setId(clientScope.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config.put("clientScopes", JsonSerialization.writeValueAsString(clientScopes));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to export client scope policy [" + policy.getName() + "]", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
representation.setConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateClientScopes(Policy policy, ClientScopePolicyRepresentation representation,
|
||||||
|
AuthorizationProvider authorization) {
|
||||||
|
updateClientScopes(policy, authorization, representation.getClientScopes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateClientScopes(Policy policy, AuthorizationProvider authorization,
|
||||||
|
Set<ClientScopeDefinition> clientScopes) {
|
||||||
|
RealmModel realm = authorization.getRealm();
|
||||||
|
Set<ClientScopePolicyRepresentation.ClientScopeDefinition> updatedClientScopes = new HashSet<>();
|
||||||
|
|
||||||
|
if (clientScopes != null) {
|
||||||
|
for (ClientScopePolicyRepresentation.ClientScopeDefinition definition : clientScopes) {
|
||||||
|
String clientScopeName = definition.getId();
|
||||||
|
ClientScopeModel clientScope = realm.getClientScopesStream()
|
||||||
|
.filter(scope -> scope.getName().equals(clientScopeName)).findAny().orElse(null);
|
||||||
|
|
||||||
|
if (clientScope == null) {
|
||||||
|
clientScope = realm.getClientScopeById(clientScopeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientScope == null) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Error while updating policy [" + policy.getName() + "]. Client Scope [" + "] could not be found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
definition.setId(clientScope.getId());
|
||||||
|
updatedClientScopes.add(definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
policy.putConfig("clientScopes", JsonSerialization.writeValueAsString(updatedClientScopes));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to serialize client scopes", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,4 +43,5 @@ org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
|
||||||
org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
|
org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
|
||||||
org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
|
org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
|
||||||
org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory
|
org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory
|
||||||
org.keycloak.authorization.policy.provider.permission.UMAPolicyProviderFactory
|
org.keycloak.authorization.policy.provider.permission.UMAPolicyProviderFactory
|
||||||
|
org.keycloak.authorization.policy.provider.clientscope.ClientScopePolicyProviderFactory
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.representations.idm.authorization;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
|
||||||
|
*/
|
||||||
|
public class ClientScopePolicyRepresentation extends AbstractPolicyRepresentation {
|
||||||
|
|
||||||
|
private Set<ClientScopeDefinition> clientScopes;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType() {
|
||||||
|
return "client-scope";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<ClientScopeDefinition> getClientScopes() {
|
||||||
|
return clientScopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientScopes(Set<ClientScopeDefinition> clientScopes) {
|
||||||
|
this.clientScopes = clientScopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addClientScope(String name, boolean required) {
|
||||||
|
if (clientScopes == null) {
|
||||||
|
clientScopes = new HashSet<>();
|
||||||
|
}
|
||||||
|
clientScopes.add(new ClientScopeDefinition(name, required));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addClientScope(String name) {
|
||||||
|
addClientScope(name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ClientScopeDefinition {
|
||||||
|
private String id;
|
||||||
|
private boolean required;
|
||||||
|
|
||||||
|
public ClientScopeDefinition() {
|
||||||
|
this(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientScopeDefinition(String id, boolean required) {
|
||||||
|
this.id = id;
|
||||||
|
this.required = required;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRequired() {
|
||||||
|
return required;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequired(boolean required) {
|
||||||
|
this.required = required;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.admin.client.resource;
|
||||||
|
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
|
import org.keycloak.representations.idm.authorization.ClientScopePolicyRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
|
||||||
|
*/
|
||||||
|
public interface ClientScopePoliciesResource {
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
Response create(ClientScopePolicyRepresentation representation);
|
||||||
|
|
||||||
|
@Path("/search")
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@NoCache
|
||||||
|
ClientScopePolicyRepresentation findByName(@QueryParam("name") String name);
|
||||||
|
}
|
|
@ -103,4 +103,7 @@ public interface PoliciesResource {
|
||||||
|
|
||||||
@Path("group")
|
@Path("group")
|
||||||
GroupPoliciesResource group();
|
GroupPoliciesResource group();
|
||||||
|
|
||||||
|
@Path("client-scope")
|
||||||
|
ClientScopePoliciesResource clientScope();
|
||||||
}
|
}
|
||||||
|
|
|
@ -786,6 +786,20 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
||||||
|
|
||||||
em.createNamedQuery("deleteClientScopeRoleMappingByClientScope").setParameter("clientScope", clientScopeEntity).executeUpdate();
|
em.createNamedQuery("deleteClientScopeRoleMappingByClientScope").setParameter("clientScope", clientScopeEntity).executeUpdate();
|
||||||
em.remove(clientScopeEntity);
|
em.remove(clientScopeEntity);
|
||||||
|
|
||||||
|
session.getKeycloakSessionFactory().publish(new ClientScopeModel.ClientScopeRemovedEvent() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakSession getKeycloakSession() {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientScopeModel getClientScope() {
|
||||||
|
return clientScope;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
em.flush();
|
em.flush();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,19 @@ public class MapClientScopeProvider implements ClientScopeProvider {
|
||||||
session.users().preRemove(clientScope);
|
session.users().preRemove(clientScope);
|
||||||
realm.removeDefaultClientScope(clientScope);
|
realm.removeDefaultClientScope(clientScope);
|
||||||
|
|
||||||
|
session.getKeycloakSessionFactory().publish(new ClientScopeModel.ClientScopeRemovedEvent() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakSession getKeycloakSession() {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientScopeModel getClientScope() {
|
||||||
|
return clientScope;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tx.delete(UUID.fromString(id));
|
tx.delete(UUID.fromString(id));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.models;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
|
import org.keycloak.provider.ProviderEvent;
|
||||||
import org.keycloak.storage.SearchableModelField;
|
import org.keycloak.storage.SearchableModelField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +35,12 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon
|
||||||
public static final SearchableModelField<ClientScopeModel> NAME = new SearchableModelField<>("name", String.class);
|
public static final SearchableModelField<ClientScopeModel> NAME = new SearchableModelField<>("name", String.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClientScopeRemovedEvent extends ProviderEvent {
|
||||||
|
ClientScopeModel getClientScope();
|
||||||
|
|
||||||
|
KeycloakSession getKeycloakSession();
|
||||||
|
}
|
||||||
|
|
||||||
String getId();
|
String getId();
|
||||||
|
|
||||||
String getName();
|
String getName();
|
||||||
|
|
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.authz;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.AuthorizationResource;
|
||||||
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
|
import org.keycloak.admin.client.resource.ClientScopesResource;
|
||||||
|
import org.keycloak.admin.client.resource.ClientsResource;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||||
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
|
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
|
||||||
|
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
|
||||||
|
import org.keycloak.representations.idm.authorization.ClientScopePolicyRepresentation;
|
||||||
|
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
||||||
|
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
|
||||||
|
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||||
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
|
import org.keycloak.testsuite.util.ClientScopeBuilder;
|
||||||
|
import org.keycloak.testsuite.util.RealmBuilder;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
|
||||||
|
*/
|
||||||
|
@AuthServerContainerExclude(AuthServer.REMOTE)
|
||||||
|
public class ClientScopePolicyTest extends AbstractAuthzTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
testRealms
|
||||||
|
.add(RealmBuilder.create().name("authz-test").user(UserBuilder.create().username("marta").password("password"))
|
||||||
|
.clientScope(ClientScopeBuilder.create().name("foo").protocol("openid-connect"))
|
||||||
|
.clientScope(ClientScopeBuilder.create().name("bar").protocol("openid-connect"))
|
||||||
|
.clientScope(ClientScopeBuilder.create().name("baz").protocol("openid-connect"))
|
||||||
|
.clientScope(ClientScopeBuilder.create().name("to-remove-a").protocol("openid-connect"))
|
||||||
|
.clientScope(ClientScopeBuilder.create().name("to-remove-b").protocol("openid-connect"))
|
||||||
|
.client(ClientBuilder.create().clientId("resource-server-test").secret("secret")
|
||||||
|
.authorizationServicesEnabled(true).redirectUris("http://localhost/resource-server-test")
|
||||||
|
.addOptionalClientScopes("foo", "bar", "baz").directAccessGrants())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void configureAuthorization() throws Exception {
|
||||||
|
createResource("Resource A");
|
||||||
|
createResource("Resource B");
|
||||||
|
|
||||||
|
createClientScopePolicy("Client Scope foo Policy", "foo", "bar");
|
||||||
|
createClientScopePolicyAndLastOneRequired("Client Scope bar Policy", "foo", "bar");
|
||||||
|
|
||||||
|
createResourcePermission("Resource A Permission", "Resource A", "Client Scope foo Policy");
|
||||||
|
createResourcePermission("Resource B Permission", "Resource B", "Client Scope bar Policy");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createResource(String name) {
|
||||||
|
AuthorizationResource authorization = getClient().authorization();
|
||||||
|
ResourceRepresentation resource = new ResourceRepresentation(name);
|
||||||
|
|
||||||
|
authorization.resources().create(resource).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createClientScopePolicy(String name, String... clientScopes) {
|
||||||
|
ClientScopePolicyRepresentation policy = new ClientScopePolicyRepresentation();
|
||||||
|
|
||||||
|
policy.setName(name);
|
||||||
|
|
||||||
|
for (String clientScope : clientScopes) {
|
||||||
|
policy.addClientScope(clientScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient().authorization().policies().clientScope().create(policy).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createClientScopePolicyAndLastOneRequired(String name, String... clientScopes) {
|
||||||
|
ClientScopePolicyRepresentation policy = new ClientScopePolicyRepresentation();
|
||||||
|
|
||||||
|
policy.setName(name);
|
||||||
|
|
||||||
|
for (int i = 0; i < clientScopes.length - 1; i++) {
|
||||||
|
policy.addClientScope(clientScopes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
policy.addClientScope(clientScopes[clientScopes.length - 1], true);
|
||||||
|
|
||||||
|
getClient().authorization().policies().clientScope().create(policy).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createResourcePermission(String name, String resource, String... policies) {
|
||||||
|
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
|
||||||
|
|
||||||
|
permission.setName(name);
|
||||||
|
permission.addResource(resource);
|
||||||
|
permission.addPolicy(policies);
|
||||||
|
|
||||||
|
getClient().authorization().permissions().resource().create(permission).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientResource getClient() {
|
||||||
|
return getClient(getRealm());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientResource getClient(RealmResource realm) {
|
||||||
|
ClientsResource clients = realm.clients();
|
||||||
|
return clients.findByClientId("resource-server-test").stream()
|
||||||
|
.map(representation -> clients.get(representation.getId())).findFirst()
|
||||||
|
.orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RealmResource getRealm() {
|
||||||
|
try {
|
||||||
|
return getAdminClient().realm("authz-test");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to create admin client");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWithExpectedClientScope() {
|
||||||
|
// Access Resource A with client scope foo.
|
||||||
|
AuthzClient authzClient = getAuthzClient();
|
||||||
|
PermissionRequest request = new PermissionRequest("Resource A");
|
||||||
|
String ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
AuthorizationResponse response = authzClient.authorization("marta", "password", "foo")
|
||||||
|
.authorize(new AuthorizationRequest(ticket));
|
||||||
|
assertNotNull(response.getToken());
|
||||||
|
|
||||||
|
// Access Resource A with client scope bar.
|
||||||
|
request = new PermissionRequest("Resource A");
|
||||||
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
response = authzClient.authorization("marta", "password", "bar").authorize(new AuthorizationRequest(ticket));
|
||||||
|
assertNotNull(response.getToken());
|
||||||
|
|
||||||
|
// Access Resource B with client scope bar.
|
||||||
|
request = new PermissionRequest("Resource B");
|
||||||
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
response = authzClient.authorization("marta", "password", "bar").authorize(new AuthorizationRequest(ticket));
|
||||||
|
assertNotNull(response.getToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWithoutExpectedClientScope() {
|
||||||
|
// Access Resource A with client scope baz.
|
||||||
|
AuthzClient authzClient = getAuthzClient();
|
||||||
|
PermissionRequest request = new PermissionRequest("Resource A");
|
||||||
|
String ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
try {
|
||||||
|
authzClient.authorization("marta", "password", "baz").authorize(new AuthorizationRequest(ticket));
|
||||||
|
fail("Should fail.");
|
||||||
|
} catch (AuthorizationDeniedException ignore) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access Resource B with client scope foo.
|
||||||
|
request = new PermissionRequest("Resource B");
|
||||||
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
try {
|
||||||
|
authzClient.authorization("marta", "password", "foo").authorize(new AuthorizationRequest(ticket));
|
||||||
|
fail("Should fail.");
|
||||||
|
} catch (AuthorizationDeniedException ignore) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemovePolicyWhenRemovingScope() {
|
||||||
|
createClientScopePolicy("Client Scope To Remove Policy", "to-remove-a", "to-remove-b");
|
||||||
|
ClientScopesResource clientScopes = getRealm().clientScopes();
|
||||||
|
ClientScopeRepresentation scopeRep = clientScopes.findAll().stream().filter(r -> r.getName().equals("to-remove-a"))
|
||||||
|
.findAny().get();
|
||||||
|
|
||||||
|
getClient().removeDefaultClientScope(scopeRep.getId());
|
||||||
|
getRealm().clientScopes().get(scopeRep.getId()).remove();
|
||||||
|
|
||||||
|
ClientScopePolicyRepresentation policyRep = getClient().authorization().policies().clientScope()
|
||||||
|
.findByName("Client Scope To Remove Policy");
|
||||||
|
final String id = scopeRep.getId();
|
||||||
|
|
||||||
|
assertFalse(policyRep.getClientScopes().stream().anyMatch(def -> def.getId().equals(id)));
|
||||||
|
|
||||||
|
scopeRep = clientScopes.findAll().stream().filter(r -> r.getName().equals("to-remove-b")).findAny().get();
|
||||||
|
getClient().removeDefaultClientScope(scopeRep.getId());
|
||||||
|
getRealm().clientScopes().get(scopeRep.getId()).remove();
|
||||||
|
|
||||||
|
assertNull(getClient().authorization().policies().clientScope().findByName("Client Scope To Remove Policy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthzClient getAuthzClient() {
|
||||||
|
return AuthzClient.create(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,6 +79,14 @@ public class ClientBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClientBuilder addOptionalClientScopes(String... optionalClientScopes) {
|
||||||
|
if (rep.getOptionalClientScopes() == null) {
|
||||||
|
rep.setOptionalClientScopes(new ArrayList<>());
|
||||||
|
}
|
||||||
|
rep.getOptionalClientScopes().addAll(Arrays.asList(optionalClientScopes));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ClientBuilder serviceAccount() {
|
public ClientBuilder serviceAccount() {
|
||||||
rep.setServiceAccountsEnabled(true);
|
rep.setServiceAccountsEnabled(true);
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
|
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
|
||||||
|
*/
|
||||||
|
public class ClientScopeBuilder {
|
||||||
|
|
||||||
|
private ClientScopeRepresentation rep;
|
||||||
|
|
||||||
|
public static ClientScopeBuilder create() {
|
||||||
|
ClientScopeRepresentation rep = new ClientScopeRepresentation();
|
||||||
|
return new ClientScopeBuilder(rep);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientScopeBuilder(ClientScopeRepresentation rep) {
|
||||||
|
this.rep = rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientScopeRepresentation build() {
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientScopeBuilder name(String name) {
|
||||||
|
rep.setName(name);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientScopeBuilder protocol(String protocol) {
|
||||||
|
rep.setProtocol(protocol);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.testsuite.util;
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -132,6 +133,18 @@ public class RealmBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RealmBuilder clientScope(ClientScopeBuilder clientScope) {
|
||||||
|
return clientScope(clientScope.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmBuilder clientScope(ClientScopeRepresentation clientScope) {
|
||||||
|
if (rep.getClientScopes() == null) {
|
||||||
|
rep.setClientScopes(new LinkedList<>());
|
||||||
|
}
|
||||||
|
rep.getClientScopes().add(clientScope);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public RealmBuilder identityProvider(IdentityProviderBuilder identityProvider) {
|
public RealmBuilder identityProvider(IdentityProviderBuilder identityProvider) {
|
||||||
return identityProvider(identityProvider.build());
|
return identityProvider(identityProvider.build());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1628,6 +1628,11 @@ authz-no-groups-assigned=No groups assigned.
|
||||||
authz-policy-group-claim=Groups Claim
|
authz-policy-group-claim=Groups Claim
|
||||||
authz-policy-group-claim.tooltip=If defined, the policy will fetch user's groups from the given claim within an access token or ID token representing the identity asking permissions. If not defined, user's groups are obtained from your realm configuration.
|
authz-policy-group-claim.tooltip=If defined, the policy will fetch user's groups from the given claim within an access token or ID token representing the identity asking permissions. If not defined, user's groups are obtained from your realm configuration.
|
||||||
authz-policy-group-groups.tooltip=Specifies the groups allowed by this policy.
|
authz-policy-group-groups.tooltip=Specifies the groups allowed by this policy.
|
||||||
|
# Authz Client Scope Policy Detail
|
||||||
|
authz-add-client-scope-policy=Add Client Scope Policy
|
||||||
|
authz-no-client-scopes-assigned=No client scopes assigned.
|
||||||
|
authz-policy-client-scope-client-scopes.tooltip=Specifies which client scope(s) are allowed by this policy.
|
||||||
|
select-a-client-scope=Select a client scope
|
||||||
|
|
||||||
# Authz Permission List
|
# Authz Permission List
|
||||||
authz-no-permissions-available=No permissions available.
|
authz-no-permissions-available=No permissions available.
|
||||||
|
|
|
@ -396,6 +396,28 @@ module.config(['$routeProvider', function ($routeProvider) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller: 'ResourceServerPolicyAggregateDetailCtrl'
|
controller: 'ResourceServerPolicyAggregateDetailCtrl'
|
||||||
|
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/client-scope/create', {
|
||||||
|
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-client-scope-detail.html',
|
||||||
|
resolve: {
|
||||||
|
realm: function (RealmLoader) {
|
||||||
|
return RealmLoader();
|
||||||
|
},
|
||||||
|
client : function(ClientLoader) {
|
||||||
|
return ClientLoader();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: 'ResourceServerPolicyClientScopeDetailCtrl'
|
||||||
|
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/client-scope/:id', {
|
||||||
|
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-client-scope-detail.html',
|
||||||
|
resolve: {
|
||||||
|
realm: function (RealmLoader) {
|
||||||
|
return RealmLoader();
|
||||||
|
},
|
||||||
|
client : function(ClientLoader) {
|
||||||
|
return ClientLoader();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: 'ResourceServerPolicyClientScopeDetailCtrl'
|
||||||
}).when('/realms/:realm/roles/:role/permissions', {
|
}).when('/realms/:realm/roles/:role/permissions', {
|
||||||
templateUrl : resourceUrl + '/partials/authz/mgmt/realm-role-permissions.html',
|
templateUrl : resourceUrl + '/partials/authz/mgmt/realm-role-permissions.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
|
|
|
@ -2093,6 +2093,105 @@ module.controller('ResourceServerPolicyAggregateDetailCtrl', function($scope, $r
|
||||||
}, realm, client, $scope);
|
}, realm, client, $scope);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.controller('ResourceServerPolicyClientScopeDetailCtrl', function($scope, $route, realm, client, ClientScope, PolicyController) {
|
||||||
|
PolicyController.onInit({
|
||||||
|
getPolicyType : function() {
|
||||||
|
return "client-scope";
|
||||||
|
},
|
||||||
|
|
||||||
|
onInit : function() {
|
||||||
|
ClientScope.query({realm: $route.current.params.realm}, function(data) {
|
||||||
|
$scope.clientScopes = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.selectedClientScopes = [];
|
||||||
|
|
||||||
|
$scope.selectClientScope = function(clientScope) {
|
||||||
|
if (!clientScope || !clientScope.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.selectedClientScope = null;
|
||||||
|
|
||||||
|
for (i = 0; i < $scope.selectedClientScopes.length; i++) {
|
||||||
|
if ($scope.selectedClientScopes[i].id == clientScope.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.selectedClientScopes.push(clientScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.removeFromList = function(clientScope) {
|
||||||
|
var index = $scope.selectedClientScopes.indexOf(clientScope);
|
||||||
|
if (index != -1) {
|
||||||
|
$scope.selectedClientScopes.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInitUpdate : function(policy) {
|
||||||
|
var selectedClientScopes = [];
|
||||||
|
|
||||||
|
if (policy.clientScopes) {
|
||||||
|
var clientScopes = policy.clientScopes;
|
||||||
|
|
||||||
|
for (i = 0; i < clientScopes.length; i++) {
|
||||||
|
ClientScope.get({realm: $route.current.params.realm, clientScope: clientScopes[i].id}, function(data) {
|
||||||
|
for (i = 0; i < clientScopes.length; i++) {
|
||||||
|
if (clientScopes[i].id == data.id) {
|
||||||
|
data.required = clientScopes[i].required ? true : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedClientScopes.push(data);
|
||||||
|
$scope.selectedClientScopes = angular.copy(selectedClientScopes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$watch('selectedClientScopes', function() {
|
||||||
|
if (!angular.equals($scope.selectedClientScopes, selectedClientScopes)) {
|
||||||
|
$scope.changed = true;
|
||||||
|
} else {
|
||||||
|
$scope.changed = false;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdate : function() {
|
||||||
|
var clientScopes = [];
|
||||||
|
|
||||||
|
for (i = 0; i < $scope.selectedClientScopes.length; i++) {
|
||||||
|
var clientScope = {};
|
||||||
|
clientScope.id = $scope.selectedClientScopes[i].id;
|
||||||
|
if ($scope.selectedClientScopes[i].required) {
|
||||||
|
clientScope.required = $scope.selectedClientScopes[i].required;
|
||||||
|
}
|
||||||
|
clientScopes.push(clientScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.policy.clientScopes = clientScopes;
|
||||||
|
delete $scope.policy.config;
|
||||||
|
},
|
||||||
|
|
||||||
|
onCreate : function() {
|
||||||
|
var clientScopes = [];
|
||||||
|
|
||||||
|
for (i = 0; i < $scope.selectedClientScopes.length; i++) {
|
||||||
|
var clientScope = {};
|
||||||
|
clientScope.id = $scope.selectedClientScopes[i].id;
|
||||||
|
if ($scope.selectedClientScopes[i].required) {
|
||||||
|
clientScope.required = $scope.selectedClientScopes[i].required;
|
||||||
|
}
|
||||||
|
clientScopes.push(clientScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.policy.clientScopes = clientScopes;
|
||||||
|
delete $scope.policy.config;
|
||||||
|
}
|
||||||
|
}, realm, client, $scope);
|
||||||
|
});
|
||||||
|
|
||||||
module.service("PolicyController", function($http, $route, $location, ResourceServer, ResourceServerPolicy, ResourceServerPermission, AuthzDialog, Notifications, policyViewState, PolicyProvider, viewState) {
|
module.service("PolicyController", function($http, $route, $location, ResourceServer, ResourceServerPolicy, ResourceServerPermission, AuthzDialog, Notifications, policyViewState, PolicyProvider, viewState) {
|
||||||
|
|
||||||
var PolicyController = {};
|
var PolicyController = {};
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
<!--
|
||||||
|
~ JBoss, Home of Professional Open Source.
|
||||||
|
~ Copyright 2021 Red Hat, Inc., and individual contributors
|
||||||
|
~ as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||||
|
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
|
||||||
|
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
|
||||||
|
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' |
|
||||||
|
translate}}</a></li>
|
||||||
|
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">{{:: 'authz-policies'
|
||||||
|
| translate}}</a></li>
|
||||||
|
<li data-ng-show="policyState.state.policy.name != null && historyBackOnSaveOrCancel">{{policyState.state.policy.name}}</li>
|
||||||
|
<li data-ng-show="policyState.state.policy.name == null && historyBackOnSaveOrCancel">{{::
|
||||||
|
policyState.state.previousPage.name | translate}}</li>
|
||||||
|
<li data-ng-show="create">{{:: 'authz-add-client-scope-policy' | translate}}</li>
|
||||||
|
<li data-ng-hide="create">{{:: 'client-scopes' | translate}}</li>
|
||||||
|
<li data-ng-hide="create">{{originalPolicy.name}}</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h1 data-ng-show="create">{{:: 'authz-add-client-scope-policy' | translate}}</h1>
|
||||||
|
<h1 data-ng-hide="create">
|
||||||
|
{{originalPolicy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
|
||||||
|
data-ng-click="remove()"></i>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form class="form-horizontal" name="clientForm" novalidate>
|
||||||
|
<fieldset class="border-top">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="name">{{:: 'name' | translate}} <span class="required">*</span></label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required
|
||||||
|
data-ng-blur="checkNewNameAvailability()">
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'authz-policy-name.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input class="form-control" type="text" id="description" name="description"
|
||||||
|
data-ng-model="policy.description">
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'authz-policy-description.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group clearfix">
|
||||||
|
<label class="col-md-2 control-label" for="clientScopes">{{:: 'client-scopes' | translate}} <span
|
||||||
|
class="required">*</span></label>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select ui-select2="{ minimumInputLength: 1}" id="clientScopes" data-ng-model="selectedClientScope"
|
||||||
|
data-ng-change="selectClientScope(selectedClientScope);"
|
||||||
|
data-placeholder="{{:: 'select-a-client-scope' | translate}}..."
|
||||||
|
ng-options="clientScope as clientScope.name for clientScope in clientScopes"
|
||||||
|
data-ng-required="selectedClientScopes.length == 0">
|
||||||
|
<option></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<kc-tooltip>{{:: 'authz-policy-client-scope-client-scopes.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group clearfix" style="margin-top: -15px;">
|
||||||
|
<label class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<table class="table table-striped table-bordered" id="selected-client-scopes">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-sm-5">{{:: 'name' | translate}}</th>
|
||||||
|
<th>{{:: 'authz-required' | translate}}</th>
|
||||||
|
<th>{{:: 'actions' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="clientScope in selectedClientScopes | orderBy:'name'">
|
||||||
|
<td>{{clientScope.name}}</td>
|
||||||
|
<td><input type="checkbox" ng-model="clientScope.required" id="{{clientScope.id}}"></td>
|
||||||
|
<td class="kc-action-cell">
|
||||||
|
<button class="btn btn-default btn-block btn-sm" ng-click="removeFromList(clientScope);">{{::
|
||||||
|
'remove' | translate}}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-ng-show="!selectedClientScopes.length">
|
||||||
|
<td class="text-muted" colspan="3">{{:: 'authz-no-client-scopes-assigned' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group clearfix">
|
||||||
|
<label class="col-md-2 control-label" for="logic">{{:: 'authz-policy-logic' | translate}}</label>
|
||||||
|
|
||||||
|
<div class="col-sm-1">
|
||||||
|
<select class="form-control" id="logic" data-ng-model="policy.logic">
|
||||||
|
<option value="POSITIVE">{{:: 'authz-policy-logic-positive' | translate}}</option>
|
||||||
|
<option value="NEGATIVE">{{:: 'authz-policy-logic-negative' | translate}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<kc-tooltip>{{:: 'authz-policy-logic.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" data-ng-model="policy.type" />
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-group" data-ng-show="access.manageAuthorization">
|
||||||
|
<div class="col-md-10 col-md-offset-2">
|
||||||
|
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||||
|
<button kc-reset data-ng-disabled="!changed && !historyBackOnSaveOrCancel">{{:: 'cancel' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<kc-menu></kc-menu>
|
Loading…
Reference in a new issue