patSupplier;
+ private TokenCallable patSupplier;
- public static AuthzClient create() {
+ /**
+ * Creates a new instance.
+ *
+ *
This method expects a keycloak.json
in the classpath, otherwise an exception will be thrown.
+ *
+ * @return a new instance
+ * @throws RuntimeException in case there is no keycloak.json
file in the classpath or the file could not be parsed
+ */
+ public static AuthzClient create() throws RuntimeException {
InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("keycloak.json");
if (configStream == null) {
@@ -56,16 +64,158 @@ public class AuthzClient {
}
}
+ /**
+ *
Creates a new instance.
+ *
+ * @param configuration the client configuration
+ * @return a new instance
+ */
public static AuthzClient create(Configuration configuration) {
return new AuthzClient(configuration, configuration.getClientAuthenticator());
}
+ /**
+ *
Creates a new instance.
+ *
+ * @param configuration the client configuration
+ * @param authenticator the client authenticator
+ * @return a new instance
+ */
public static AuthzClient create(Configuration configuration, ClientAuthenticator authenticator) {
return new AuthzClient(configuration, authenticator);
}
private final ServerConfiguration serverConfiguration;
- private final Configuration deployment;
+ private final Configuration configuration;
+
+ /**
+ *
Creates a {@link ProtectionResource} instance which can be used to access the Protection API.
+ *
+ *
When using this method, the PAT (the access token with the uma_protection scope) is obtained for the client
+ * itself, using any of the supported credential types (client/secret, jwt, etc).
+ *
+ * @return a {@link ProtectionResource}
+ */
+ public ProtectionResource protection() {
+ return new ProtectionResource(this.http, this.serverConfiguration, createPatSupplier());
+ }
+
+ /**
+ *
Creates a {@link ProtectionResource} instance which can be used to access the Protection API.
+ *
+ * @param the PAT (the access token with the uma_protection scope)
+ * @return a {@link ProtectionResource}
+ */
+ public ProtectionResource protection(final String accessToken) {
+ return new ProtectionResource(this.http, this.serverConfiguration, new TokenCallable(http, configuration, serverConfiguration) {
+ @Override
+ public String call() {
+ return accessToken;
+ }
+
+ @Override
+ protected boolean isRetry() {
+ return false;
+ }
+ });
+ }
+
+ /**
+ *
Creates a {@link ProtectionResource} instance which can be used to access the Protection API.
+ *
+ *
When using this method, the PAT (the access token with the uma_protection scope) is obtained for a given user.
+ *
+ * @return a {@link ProtectionResource}
+ */
+ public ProtectionResource protection(String userName, String password) {
+ return new ProtectionResource(this.http, this.serverConfiguration, createPatSupplier(userName, password));
+ }
+
+ /**
+ *
Creates a {@link AuthorizationResource} instance which can be used to obtain permissions from the server.
+ *
+ * @return a {@link AuthorizationResource}
+ */
+ public AuthorizationResource authorization() {
+ return new AuthorizationResource(configuration, serverConfiguration, this.http, null);
+ }
+
+ /**
+ *
Creates a {@link AuthorizationResource} instance which can be used to obtain permissions from the server.
+ *
+ * @param accessToken the Access Token that will be used as a bearer to access the token endpoint
+ * @return a {@link AuthorizationResource}
+ */
+ public AuthorizationResource authorization(final String accessToken) {
+ return new AuthorizationResource(configuration, serverConfiguration, this.http, new TokenCallable(http, configuration, serverConfiguration) {
+ @Override
+ public String call() {
+ return accessToken;
+ }
+
+ @Override
+ protected boolean isRetry() {
+ return false;
+ }
+ });
+ }
+
+ /**
+ *
Creates a {@link AuthorizationResource} instance which can be used to obtain permissions from the server.
+ *
+ * @param userName an ID Token or Access Token representing an identity and/or access context
+ * @param password
+ * @return a {@link AuthorizationResource}
+ */
+ public AuthorizationResource authorization(final String userName, final String password) {
+ return new AuthorizationResource(configuration, serverConfiguration, this.http, createRefreshableAccessTokenSupplier(userName, password));
+ }
+
+ /**
+ * Obtains an access token using the client credentials.
+ *
+ * @return an {@link AccessTokenResponse}
+ */
+ public AccessTokenResponse obtainAccessToken() {
+ return this.http.post(this.serverConfiguration.getTokenEndpoint())
+ .authentication()
+ .client()
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ }
+
+ /**
+ * Obtains an access token using the resource owner credentials.
+ *
+ * @return an {@link AccessTokenResponse}
+ */
+ public AccessTokenResponse obtainAccessToken(String userName, String password) {
+ return this.http.post(this.serverConfiguration.getTokenEndpoint())
+ .authentication()
+ .oauth2ResourceOwnerPassword(userName, password)
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ }
+
+ /**
+ * Returns the configuration obtained from the server at the UMA Discovery Endpoint.
+ *
+ * @return the {@link ServerConfiguration}
+ */
+ public ServerConfiguration getServerConfiguration() {
+ return this.serverConfiguration;
+ }
+
+ /**
+ * Obtains the client configuration
+ *
+ * @return the {@link Configuration}
+ */
+ public Configuration getConfiguration() {
+ return this.configuration;
+ }
private AuthzClient(Configuration configuration, ClientAuthenticator authenticator) {
if (configuration == null) {
@@ -78,14 +228,14 @@ public class AuthzClient {
throw new IllegalArgumentException("Configuration URL can not be null.");
}
- configurationUrl += "/realms/" + configuration.getRealm() + "/.well-known/uma-configuration";
+ configurationUrl += "/realms/" + configuration.getRealm() + "/.well-known/uma2-configuration";
- this.deployment = configuration;
+ this.configuration = configuration;
this.http = new Http(configuration, authenticator != null ? authenticator : configuration.getClientAuthenticator());
try {
- this.serverConfiguration = this.http.get(URI.create(configurationUrl))
+ this.serverConfiguration = this.http.get(configurationUrl)
.response().json(ServerConfiguration.class)
.execute();
} catch (Exception e) {
@@ -95,85 +245,18 @@ public class AuthzClient {
this.http.setServerConfiguration(this.serverConfiguration);
}
- private AuthzClient(Configuration configuration) {
- this(configuration, null);
- }
-
- public ProtectionResource protection() {
- return new ProtectionResource(this.http, createPatSupplier());
- }
-
- public AuthorizationResource authorization(String accesstoken) {
- return new AuthorizationResource(this.http, accesstoken);
- }
-
- public AuthorizationResource authorization(String userName, String password) {
- return new AuthorizationResource(this.http, obtainAccessToken(userName, password).getToken());
- }
-
- public EntitlementResource entitlement(String eat) {
- return new EntitlementResource(this.http, eat);
- }
-
- public AccessTokenResponse obtainAccessToken() {
- return this.http.post(this.serverConfiguration.getTokenEndpoint())
- .authentication()
- .client()
- .response()
- .json(AccessTokenResponse.class)
- .execute();
- }
-
- public AccessTokenResponse obtainAccessToken(String userName, String password) {
- return this.http.post(this.serverConfiguration.getTokenEndpoint())
- .authentication()
- .oauth2ResourceOwnerPassword(userName, password)
- .response()
- .json(AccessTokenResponse.class)
- .execute();
- }
-
- public ServerConfiguration getServerConfiguration() {
- return this.serverConfiguration;
- }
-
- public Configuration getConfiguration() {
- return this.deployment;
- }
-
- private Callable createPatSupplier() {
+ private TokenCallable createPatSupplier(String userName, String password) {
if (patSupplier == null) {
- patSupplier = new Callable() {
- AccessTokenResponse clientToken = obtainAccessToken();
-
- @Override
- public String call() {
- String token = clientToken.getToken();
-
- try {
- AccessToken accessToken = JsonSerialization.readValue(new JWSInput(token).getContent(), AccessToken.class);
-
- if (accessToken.isActive()) {
- return token;
- }
-
- clientToken = http.post(serverConfiguration.getTokenEndpoint())
- .authentication().client()
- .form()
- .param("grant_type", "refresh_token")
- .param("refresh_token", clientToken.getRefreshToken())
- .response()
- .json(AccessTokenResponse.class)
- .execute();
- } catch (Exception e) {
- patSupplier = null;
- throw new RuntimeException(e);
- }
-
- return clientToken.getToken();
- }
- };
+ patSupplier = createRefreshableAccessTokenSupplier(userName, password);
}
return patSupplier;
}
-}
+
+ private TokenCallable createPatSupplier() {
+ return createPatSupplier(null, null);
+ }
+
+ private TokenCallable createRefreshableAccessTokenSupplier(final String userName, final String password) {
+ return new TokenCallable(userName, password, http, configuration, serverConfiguration);
+ }
+}
\ No newline at end of file
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
index 076c2dbd7a..d9077e5c92 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
@@ -17,11 +17,12 @@
*/
package org.keycloak.authorization.client;
-import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
* @author Pedro Igor
*/
public interface ClientAuthenticator {
- void configureClientCredentials(HashMap requestParams, HashMap requestHeaders);
+ void configureClientCredentials(Map> requestParams, Map requestHeaders);
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
index 647891ff4a..7e6802e9bc 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
@@ -17,14 +17,14 @@
*/
package org.keycloak.authorization.client;
-import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.BasicAuthHelper;
-import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author Pedro Igor
@@ -34,10 +34,22 @@ public class Configuration extends AdapterConfig {
@JsonIgnore
private HttpClient httpClient;
+ @JsonIgnore
+ private ClientAuthenticator clientAuthenticator = createDefaultClientAuthenticator();
+
public Configuration() {
}
+ /**
+ * Creates a new instance.
+ *
+ * @param authServerUrl the server's URL. E.g.: http://{server}:{port}/auth.(not {@code null})
+ * @param realm the realm name (not {@code null})
+ * @param clientId the client id (not {@code null})
+ * @param clientCredentials a map with the client credentials (not {@code null})
+ * @param httpClient the {@link HttpClient} instance that should be used when sending requests to the server, or {@code null} if a default instance should be created
+ */
public Configuration(String authServerUrl, String realm, String clientId, Map clientCredentials, HttpClient httpClient) {
this.authServerUrl = authServerUrl;
setAuthServerUrl(authServerUrl);
@@ -47,29 +59,34 @@ public class Configuration extends AdapterConfig {
this.httpClient = httpClient;
}
- @JsonIgnore
- private ClientAuthenticator clientAuthenticator = new ClientAuthenticator() {
- @Override
- public void configureClientCredentials(HashMap requestParams, HashMap requestHeaders) {
- String secret = (String) getCredentials().get("secret");
-
- if (secret == null) {
- throw new RuntimeException("Client secret not provided.");
- }
-
- requestHeaders.put("Authorization", BasicAuthHelper.createHeader(getResource(), secret));
- }
- };
-
public HttpClient getHttpClient() {
if (this.httpClient == null) {
this.httpClient = HttpClients.createDefault();
}
-
return httpClient;
}
- public ClientAuthenticator getClientAuthenticator() {
+ ClientAuthenticator getClientAuthenticator() {
return this.clientAuthenticator;
}
+
+ /**
+ * Creates a default client authenticator which uses HTTP BASIC and client id and secret to authenticate the client.
+ *
+ * @return the default client authenticator
+ */
+ private ClientAuthenticator createDefaultClientAuthenticator() {
+ return new ClientAuthenticator() {
+ @Override
+ public void configureClientCredentials(Map> requestParams, Map requestHeaders) {
+ String secret = (String) getCredentials().get("secret");
+
+ if (secret == null) {
+ throw new RuntimeException("Client secret not provided.");
+ }
+
+ requestHeaders.put("Authorization", BasicAuthHelper.createHeader(getResource(), secret));
+ }
+ };
+ }
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java
deleted file mode 100644
index ef535862d2..0000000000
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source
- *
- * Copyright 2015 Red Hat, Inc. and/or its affiliates.
- *
- * 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.client.representation;
-
-/**
- * @author Pedro Igor
- */
-public class AuthorizationRequest {
-
- private String ticket;
- private String rpt;
-
- public AuthorizationRequest(String ticket, String rpt) {
- this.ticket = ticket;
- this.rpt = rpt;
- }
-
- public AuthorizationRequest(String ticket) {
- this(ticket, null);
- }
-
- public AuthorizationRequest() {
- this(null, null);
- }
-
- public String getTicket() {
- return this.ticket;
- }
-
- public String getRpt() {
- return this.rpt;
- }
-}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java
deleted file mode 100644
index 472c89ae50..0000000000
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source
- *
- * Copyright 2015 Red Hat, Inc. and/or its affiliates.
- *
- * 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.client.representation;
-
-/**
- * @author Pedro Igor
- */
-public class AuthorizationResponse {
-
- private String rpt;
-
- public AuthorizationResponse(String rpt) {
- this.rpt = rpt;
- }
-
- public AuthorizationResponse() {
- this(null);
- }
-
- public String getRpt() {
- return this.rpt;
- }
-
- public void setRpt(final String rpt) {
- this.rpt = rpt;
- }
-}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java
deleted file mode 100644
index b3efa85c96..0000000000
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package org.keycloak.authorization.client.representation;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * An {@code {@link EntitlementRequest} represents a request sent to the server containing the permissions being requested.
- *
- *
Along with an entitlement request additional {@link AuthorizationRequestMetadata} information can be passed in order to define what clients expect from
- * the server when evaluating the requested permissions and when returning with a response.
- *
- * @author Pedro Igor
- */
-public class EntitlementRequest {
-
- private String rpt;
- private AuthorizationRequestMetadata metadata;
-
- private List permissions = new ArrayList<>();
-
- /**
- * Returns the permissions being requested.
- *
- * @return the permissions being requested (not {@code null})
- */
- public List getPermissions() {
- return permissions;
- }
-
- /**
- * Set the permissions being requested
- *
- * @param permissions the permissions being requests (not {@code null})
- */
- public void setPermissions(List permissions) {
- this.permissions = permissions;
- }
-
- /**
- * Adds the given {@link PermissionRequest} to the list of requested permissions.
- *
- * @param request the permission to request (not {@code null})
- */
- public void addPermission(PermissionRequest request) {
- getPermissions().add(request);
- }
-
- /**
- * Returns a {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested.
- *
- * @return a previously issued RPT (may be {@code null})
- */
- public String getRpt() {
- return rpt;
- }
-
- /**
- * A {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested.
- *
- * @param rpt a previously issued RPT. If {@code null}, only the requested permissions are evaluated
- */
- public void setRpt(String rpt) {
- this.rpt = rpt;
- }
-
- /**
- * Return the {@link Metadata} associated with this request.
- *
- * @return
- */
- public AuthorizationRequestMetadata getMetadata() {
- return metadata;
- }
-
- /**
- * The {@link Metadata} associated with this request. The metadata defines specific information that should be considered
- * by the server when evaluating and returning permissions.
- *
- * @param metadata the {@link Metadata} associated with this request (may be {@code null})
- */
- public void setMetadata(AuthorizationRequestMetadata metadata) {
- this.metadata = metadata;
- }
-}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java
deleted file mode 100644
index fd3dc632b0..0000000000
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source
- *
- * Copyright 2015 Red Hat, Inc. and/or its affiliates.
- *
- * 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.client.representation;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author Pedro Igor
- */
-public class ErrorResponse {
-
- private String error;
-
- @JsonProperty("error_description")
- private String description;
-
- @JsonProperty("error_uri")
- private String uri;
-
- public ErrorResponse(final String error, final String description, final String uri) {
- this.error = error;
- this.description = description;
- this.uri = uri;
- }
-
- public ErrorResponse(final String error) {
- this(error, null, null);
- }
-
- public ErrorResponse() {
- this(null, null, null);
- }
-
- public String getError() {
- return this.error;
- }
-
- public String getDescription() {
- return this.description;
- }
-
- public String getUri() {
- return this.uri;
- }
-}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java
deleted file mode 100644
index 38d54710db..0000000000
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source
- *
- * Copyright 2015 Red Hat, Inc. and/or its affiliates.
- *
- * 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.client.representation;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.Set;
-
-/**
- * @author Pedro Igor
- */
-public class PermissionRequest {
-
- @JsonProperty("resource_set_id")
- private String resourceSetId;
-
- @JsonProperty("resource_set_name")
- private String resourceSetName;
-
- private Set scopes;
-
- public PermissionRequest() {
-
- }
-
- public PermissionRequest(String resourceSetId, String resourceSetName, Set scopes) {
- this.resourceSetId = resourceSetId;
- this.resourceSetName = resourceSetName;
- this.scopes = scopes;
- }
-
- public PermissionRequest(String resourceSetName) {
- this.resourceSetName = resourceSetName;
- }
-
- public PermissionRequest(String resourceSetName, Set scopes) {
- this.resourceSetName = resourceSetName;
- this.scopes = scopes;
- }
-
- public String getResourceSetId() {
- return this.resourceSetId;
- }
-
- public void setResourceSetId(String resourceSetId) {
- this.resourceSetId = resourceSetId;
- }
-
- public Set getScopes() {
- return this.scopes;
- }
-
- public void setScopes(Set scopes) {
- this.scopes = scopes;
- }
-
- public String getResourceSetName() {
- return this.resourceSetName;
- }
-
- public void setResourceSetName(String resourceSetName) {
- this.resourceSetName = resourceSetName;
- }
-}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java
deleted file mode 100644
index 0f279bc62d..0000000000
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source
- *
- * Copyright 2015 Red Hat, Inc. and/or its affiliates.
- *
- * 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.client.representation;
-
-import com.fasterxml.jackson.annotation.JsonUnwrapped;
-
-/**
- * @author Pedro Igor
- */
-public class RegistrationResponse {
-
- private final ResourceRepresentation resourceDescription;
-
- public RegistrationResponse(ResourceRepresentation resourceDescription) {
- this.resourceDescription = resourceDescription;
- }
-
- public RegistrationResponse() {
- this(null);
- }
-
- @JsonUnwrapped
- public ResourceRepresentation getResourceDescription() {
- return this.resourceDescription;
- }
-
- public String getId() {
- if (this.resourceDescription != null) {
- return this.resourceDescription.getId();
- }
-
- return null;
- }
-}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
index 65ec87c321..c2e8f8a9e1 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
@@ -17,14 +17,14 @@
*/
package org.keycloak.authorization.client.representation;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
/**
* One or more resources that the resource server manages as a set of protected resources.
*
@@ -38,13 +38,17 @@ public class ResourceRepresentation {
private String id;
private String name;
+ private String displayName;
private String uri;
private String type;
+
+ @JsonProperty("resource_scopes")
private Set scopes;
@JsonProperty("icon_uri")
private String iconUri;
private String owner;
+ private Boolean ownerManagedAccess;
/**
* Creates a new instance.
@@ -106,6 +110,10 @@ public class ResourceRepresentation {
return this.name;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
public String getUri() {
return this.uri;
}
@@ -129,6 +137,10 @@ public class ResourceRepresentation {
this.name = name;
}
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
public void setUri(String uri) {
this.uri = uri;
}
@@ -153,6 +165,14 @@ public class ResourceRepresentation {
this.owner = owner;
}
+ public void setOwnerManagedAccess(Boolean ownerManagedAccess) {
+ this.ownerManagedAccess = ownerManagedAccess;
+ }
+
+ public Boolean getOwnerManagedAccess() {
+ return ownerManagedAccess;
+ }
+
public void addScope(ScopeRepresentation scopeRepresentation) {
if (this.scopes == null) {
this.scopes = new HashSet<>();
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java
index 6716165838..f708e52af1 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java
@@ -1,13 +1,12 @@
/*
- * JBoss, Home of Professional Open Source
- *
- * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ * Copyright 2018 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
+ * 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,
@@ -17,219 +16,194 @@
*/
package org.keycloak.authorization.client.representation;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
-import java.net.URI;
-import java.util.Set;
+import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Pedro Igor
*/
public class ServerConfiguration {
- private String version;
- private URI issuer;
-
- @JsonProperty("pat_profiles_supported")
- private Set patProfiles;
-
- @JsonProperty("pat_grant_types_supported")
- private Set patGrantTypes;
-
- @JsonProperty("aat_profiles_supported")
- private Set aatProfiles;
-
- @JsonProperty("aat_grant_types_supported")
- private Set aatGrantTypes;
-
- @JsonProperty("rpt_profiles_supported")
- private Set rptProfiles;
-
- @JsonProperty("claim_token_profiles_supported")
- private Set claimTokenProfiles;
-
- @JsonProperty("dynamic_client_endpoint")
- private URI dynamicClientEndpoint;
-
- @JsonProperty("token_endpoint")
- private URI tokenEndpoint;
+ @JsonProperty("issuer")
+ private String issuer;
@JsonProperty("authorization_endpoint")
- private URI authorizationEndpoint;
+ private String authorizationEndpoint;
- @JsonProperty("requesting_party_claims_endpoint")
- private URI requestingPartyClaimsEndpoint;
+ @JsonProperty("token_endpoint")
+ private String tokenEndpoint;
- @JsonProperty("resource_set_registration_endpoint")
- private URI resourceSetRegistrationEndpoint;
+ @JsonProperty("token_introspection_endpoint")
+ private String tokenIntrospectionEndpoint;
- @JsonProperty("introspection_endpoint")
- private URI introspectionEndpoint;
+ @JsonProperty("userinfo_endpoint")
+ private String userinfoEndpoint;
- @JsonProperty("permission_registration_endpoint")
- private URI permissionRegistrationEndpoint;
+ @JsonProperty("end_session_endpoint")
+ private String logoutEndpoint;
- @JsonProperty("rpt_endpoint")
- private URI rptEndpoint;
+ @JsonProperty("jwks_uri")
+ private String jwksUri;
- /**
- * Non-standard, Keycloak specific configuration options
- */
- private String realm;
+ @JsonProperty("check_session_iframe")
+ private String checkSessionIframe;
- private String realmPublicKey;
+ @JsonProperty("grant_types_supported")
+ private List grantTypesSupported;
- private URI serverUrl;
+ @JsonProperty("response_types_supported")
+ private List responseTypesSupported;
- public String getVersion() {
- return this.version;
+ @JsonProperty("subject_types_supported")
+ private List subjectTypesSupported;
+
+ @JsonProperty("id_token_signing_alg_values_supported")
+ private List idTokenSigningAlgValuesSupported;
+
+ @JsonProperty("userinfo_signing_alg_values_supported")
+ private List userInfoSigningAlgValuesSupported;
+
+ @JsonProperty("request_object_signing_alg_values_supported")
+ private List requestObjectSigningAlgValuesSupported;
+
+ @JsonProperty("response_modes_supported")
+ private List responseModesSupported;
+
+ @JsonProperty("registration_endpoint")
+ private String registrationEndpoint;
+
+ @JsonProperty("token_endpoint_auth_methods_supported")
+ private List tokenEndpointAuthMethodsSupported;
+
+ @JsonProperty("token_endpoint_auth_signing_alg_values_supported")
+ private List tokenEndpointAuthSigningAlgValuesSupported;
+
+ @JsonProperty("claims_supported")
+ private List claimsSupported;
+
+ @JsonProperty("claim_types_supported")
+ private List claimTypesSupported;
+
+ @JsonProperty("claims_parameter_supported")
+ private Boolean claimsParameterSupported;
+
+ @JsonProperty("scopes_supported")
+ private List scopesSupported;
+
+ @JsonProperty("request_parameter_supported")
+ private Boolean requestParameterSupported;
+
+ @JsonProperty("request_uri_parameter_supported")
+ private Boolean requestUriParameterSupported;
+
+ @JsonProperty("resource_registration_endpoint")
+ private String resourceRegistrationEndpoint;
+
+ @JsonProperty("permission_endpoint")
+ private String permissionEndpoint;
+
+ public String getIssuer() {
+ return issuer;
}
- void setVersion(final String version) {
- this.version = version;
+ public String getAuthorizationEndpoint() {
+ return authorizationEndpoint;
}
- public URI getIssuer() {
- return this.issuer;
+ public String getTokenEndpoint() {
+ return tokenEndpoint;
}
- void setIssuer(final URI issuer) {
- this.issuer = issuer;
+ public String getTokenIntrospectionEndpoint() {
+ return tokenIntrospectionEndpoint;
}
- public Set getPatProfiles() {
- return this.patProfiles;
+ public String getUserinfoEndpoint() {
+ return userinfoEndpoint;
}
- void setPatProfiles(final Set patProfiles) {
- this.patProfiles = patProfiles;
+ public String getLogoutEndpoint() {
+ return logoutEndpoint;
}
- public Set getPatGrantTypes() {
- return this.patGrantTypes;
+ public String getJwksUri() {
+ return jwksUri;
}
- void setPatGrantTypes(final Set patGrantTypes) {
- this.patGrantTypes = patGrantTypes;
+ public String getCheckSessionIframe() {
+ return checkSessionIframe;
}
- public Set getAatProfiles() {
- return this.aatProfiles;
+ public List getGrantTypesSupported() {
+ return grantTypesSupported;
}
- void setAatProfiles(final Set aatProfiles) {
- this.aatProfiles = aatProfiles;
+ public List getResponseTypesSupported() {
+ return responseTypesSupported;
}
- public Set getAatGrantTypes() {
- return this.aatGrantTypes;
+ public List getSubjectTypesSupported() {
+ return subjectTypesSupported;
}
- void setAatGrantTypes(final Set aatGrantTypes) {
- this.aatGrantTypes = aatGrantTypes;
+ public List getIdTokenSigningAlgValuesSupported() {
+ return idTokenSigningAlgValuesSupported;
}
- public Set getRptProfiles() {
- return this.rptProfiles;
+ public List getUserInfoSigningAlgValuesSupported() {
+ return userInfoSigningAlgValuesSupported;
}
- void setRptProfiles(final Set rptProfiles) {
- this.rptProfiles = rptProfiles;
+ public List getRequestObjectSigningAlgValuesSupported() {
+ return requestObjectSigningAlgValuesSupported;
}
- public Set getClaimTokenProfiles() {
- return this.claimTokenProfiles;
+ public List getResponseModesSupported() {
+ return responseModesSupported;
}
- void setClaimTokenProfiles(final Set claimTokenProfiles) {
- this.claimTokenProfiles = claimTokenProfiles;
+ public String getRegistrationEndpoint() {
+ return registrationEndpoint;
}
- public URI getDynamicClientEndpoint() {
- return this.dynamicClientEndpoint;
+ public List getTokenEndpointAuthMethodsSupported() {
+ return tokenEndpointAuthMethodsSupported;
}
- void setDynamicClientEndpoint(final URI dynamicClientEndpoint) {
- this.dynamicClientEndpoint = dynamicClientEndpoint;
+ public List getTokenEndpointAuthSigningAlgValuesSupported() {
+ return tokenEndpointAuthSigningAlgValuesSupported;
}
- public URI getTokenEndpoint() {
- return this.tokenEndpoint;
+ public List getClaimsSupported() {
+ return claimsSupported;
}
- void setTokenEndpoint(final URI tokenEndpoint) {
- this.tokenEndpoint = tokenEndpoint;
+ public List getClaimTypesSupported() {
+ return claimTypesSupported;
}
- public URI getAuthorizationEndpoint() {
- return this.authorizationEndpoint;
+ public Boolean getClaimsParameterSupported() {
+ return claimsParameterSupported;
}
- void setAuthorizationEndpoint(final URI authorizationEndpoint) {
- this.authorizationEndpoint = authorizationEndpoint;
+ public List getScopesSupported() {
+ return scopesSupported;
}
- public URI getRequestingPartyClaimsEndpoint() {
- return this.requestingPartyClaimsEndpoint;
+ public Boolean getRequestParameterSupported() {
+ return requestParameterSupported;
}
- void setRequestingPartyClaimsEndpoint(final URI requestingPartyClaimsEndpoint) {
- this.requestingPartyClaimsEndpoint = requestingPartyClaimsEndpoint;
+ public Boolean getRequestUriParameterSupported() {
+ return requestUriParameterSupported;
}
- public URI getResourceSetRegistrationEndpoint() {
- return this.resourceSetRegistrationEndpoint;
+ public String getResourceRegistrationEndpoint() {
+ return resourceRegistrationEndpoint;
}
- void setResourceSetRegistrationEndpoint(final URI resourceSetRegistrationEndpoint) {
- this.resourceSetRegistrationEndpoint = resourceSetRegistrationEndpoint;
- }
-
- public URI getIntrospectionEndpoint() {
- return this.introspectionEndpoint;
- }
-
- void setIntrospectionEndpoint(final URI introspectionEndpoint) {
- this.introspectionEndpoint = introspectionEndpoint;
- }
-
- public URI getPermissionRegistrationEndpoint() {
- return this.permissionRegistrationEndpoint;
- }
-
- void setPermissionRegistrationEndpoint(final URI permissionRegistrationEndpoint) {
- this.permissionRegistrationEndpoint = permissionRegistrationEndpoint;
- }
-
- public URI getRptEndpoint() {
- return this.rptEndpoint;
- }
-
- void setRptEndpoint(final URI rptEndpoint) {
- this.rptEndpoint = rptEndpoint;
- }
-
- public String getRealm() {
- return this.realm;
- }
-
- public void setRealm(final String realm) {
- this.realm = realm;
- }
-
- public String getRealmPublicKey() {
- return this.realmPublicKey;
- }
-
- public void setRealmPublicKey(String realmPublicKey) {
- this.realmPublicKey = realmPublicKey;
- }
-
- public URI getServerUrl() {
- return this.serverUrl;
- }
-
- public void setServerUrl(URI serverUrl) {
- this.serverUrl = serverUrl;
+ public String getPermissionEndpoint() {
+ return permissionEndpoint;
}
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
index 8fcc6f31e0..8bd0424322 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
@@ -17,12 +17,12 @@
*/
package org.keycloak.authorization.client.representation;
+import java.util.List;
+
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.authorization.Permission;
-import java.util.List;
-
/**
* @author Pedro Igor
*/
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java
index 0f22ebe89e..6b30b0d355 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java
@@ -18,34 +18,82 @@
package org.keycloak.authorization.client.resource;
-import static org.keycloak.authorization.client.util.Throwables.handleAndWrapException;
+import java.util.concurrent.Callable;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
-import org.keycloak.util.JsonSerialization;
+import org.keycloak.authorization.client.util.HttpMethod;
+import org.keycloak.authorization.client.util.Throwables;
+import org.keycloak.authorization.client.util.TokenCallable;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
/**
+ * An entry point for obtaining permissions from the server.
+ *
* @author Pedro Igor
*/
public class AuthorizationResource {
- private final Http http;
- private final String accessToken;
+ private Configuration configuration;
+ private ServerConfiguration serverConfiguration;
+ private Http http;
+ private TokenCallable token;
- public AuthorizationResource(Http http, String aat) {
+ public AuthorizationResource(Configuration configuration, ServerConfiguration serverConfiguration, Http http, TokenCallable token) {
+ this.configuration = configuration;
+ this.serverConfiguration = serverConfiguration;
this.http = http;
- this.accessToken = aat;
+ this.token = token;
}
- public AuthorizationResponse authorize(AuthorizationRequest request) {
+ /**
+ * Query the server for all permissions.
+ *
+ * @return an {@link AuthorizationResponse} with a RPT holding all granted permissions
+ * @throws AuthorizationDeniedException in case the request was denied by the server
+ */
+ public AuthorizationResponse authorize() throws AuthorizationDeniedException {
+ return authorize(new AuthorizationRequest());
+ }
+
+ /**
+ * Query the server for permissions given an {@link AuthorizationRequest}.
+ *
+ * @param request an {@link AuthorizationRequest} (not {@code null})
+ * @return an {@link AuthorizationResponse} with a RPT holding all granted permissions
+ * @throws AuthorizationDeniedException in case the request was denied by the server
+ */
+ public AuthorizationResponse authorize(final AuthorizationRequest request) throws AuthorizationDeniedException {
+ if (request == null) {
+ throw new IllegalArgumentException("Authorization request must not be null");
+ }
+
+ Callable callable = new Callable() {
+ @Override
+ public AuthorizationResponse call() throws Exception {
+ request.setAudience(configuration.getResource());
+
+ HttpMethod method = http.post(serverConfiguration.getTokenEndpoint());
+
+ if (token != null) {
+ method = method.authorizationBearer(token.call());
+ }
+
+ return method
+ .authentication()
+ .uma(request)
+ .response()
+ .json(AuthorizationResponse.class)
+ .execute();
+ }
+ };
try {
- return this.http.post("/authz/authorize")
- .authorizationBearer(this.accessToken)
- .json(JsonSerialization.writeValueAsBytes(request))
- .response().json(AuthorizationResponse.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Failed to obtain authorization data", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, token, "Failed to obtain authorization data", cause);
}
}
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java
deleted file mode 100644
index 1103c2d0e8..0000000000
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.keycloak.authorization.client.resource;
-
-import static org.keycloak.authorization.client.util.Throwables.handleAndWrapException;
-
-import org.keycloak.authorization.client.representation.EntitlementRequest;
-import org.keycloak.authorization.client.representation.EntitlementResponse;
-import org.keycloak.authorization.client.util.Http;
-import org.keycloak.util.JsonSerialization;
-
-/**
- * @author Pedro Igor
- */
-public class EntitlementResource {
-
- private final Http http;
- private final String eat;
-
- public EntitlementResource(Http http, String eat) {
- this.http = http;
- this.eat = eat;
- }
-
- public EntitlementResponse getAll(String resourceServerId) {
- try {
- return this.http.get("/authz/entitlement/" + resourceServerId)
- .authorizationBearer(eat)
- .response().json(EntitlementResponse.class).execute();
- } catch (Exception cause) {
- throw handleAndWrapException("Failed to obtain entitlements", cause);
- }
- }
-
- public EntitlementResponse get(String resourceServerId, EntitlementRequest request) {
- try {
- return this.http.post("/authz/entitlement/" + resourceServerId)
- .authorizationBearer(eat)
- .json(JsonSerialization.writeValueAsBytes(request))
- .response().json(EntitlementResponse.class).execute();
- } catch (Exception cause) {
- throw handleAndWrapException("Failed to obtain entitlements", cause);
- }
- }
-}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java
index 785a3a6d4b..b6628a9ac8 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java
@@ -17,36 +17,205 @@
*/
package org.keycloak.authorization.client.resource;
-import static org.keycloak.authorization.client.util.Throwables.handleAndWrapException;
-
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.Callable;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.authorization.client.representation.PermissionResponse;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
+import org.keycloak.authorization.client.util.Throwables;
+import org.keycloak.authorization.client.util.TokenCallable;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
+import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
import org.keycloak.util.JsonSerialization;
/**
+ * An entry point for managing permission tickets using the Protection API.
+ *
* @author Pedro Igor
*/
public class PermissionResource {
private final Http http;
- private final Callable pat;
+ private final ServerConfiguration serverConfiguration;
+ private final TokenCallable pat;
- public PermissionResource(Http http, Callable pat) {
+ public PermissionResource(Http http, ServerConfiguration serverConfiguration, TokenCallable pat) {
this.http = http;
+ this.serverConfiguration = serverConfiguration;
this.pat = pat;
}
+ /**
+ * @deprecated use {@link #create(PermissionRequest)}
+ * @param request
+ * @return
+ */
+ @Deprecated
public PermissionResponse forResource(PermissionRequest request) {
+ return create(request);
+ }
+
+ /**
+ * Creates a new permission ticket for a single resource and scope(s).
+ *
+ * @param request the {@link PermissionRequest} representing the resource and scope(s) (not {@code null})
+ * @return a permission response holding a permission ticket with the requested permissions
+ */
+ public PermissionResponse create(PermissionRequest request) {
+ return create(Arrays.asList(request));
+ }
+
+ /**
+ * Creates a new permission ticket for a set of one or more resource and scope(s).
+ *
+ * @param request the {@link PermissionRequest} representing the resource and scope(s) (not {@code null})
+ * @return a permission response holding a permission ticket with the requested permissions
+ */
+ public PermissionResponse create(final List requests) {
+ if (requests == null || requests.isEmpty()) {
+ throw new IllegalArgumentException("Permission request must not be null or empty");
+ }
+ Callable callable = new Callable() {
+ @Override
+ public PermissionResponse call() throws Exception {
+ return http.post(serverConfiguration.getPermissionEndpoint())
+ .authorizationBearer(pat.call())
+ .json(JsonSerialization.writeValueAsBytes(requests))
+ .response().json(PermissionResponse.class).execute();
+ }
+ };
try {
- return this.http.post("/authz/protection/permission")
- .authorizationBearer(this.pat.call())
- .json(JsonSerialization.writeValueAsBytes(request))
- .response().json(PermissionResponse.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Error obtaining permission ticket", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error creating permission ticket", cause);
+ }
+ }
+
+ /**
+ * Query the server for any permission ticket associated with the given scopeId
.
+ *
+ * @param scopeId the scope id (not {@code null})
+ * @return a list of permission tickets associated with the given scopeId
+ */
+ public List findByScope(final String scopeId) {
+ if (scopeId == null) {
+ throw new IllegalArgumentException("Scope id must not be null");
+ }
+ Callable> callable = new Callable>() {
+ @Override
+ public List call() throws Exception {
+ return http.>get(serverConfiguration.getPermissionEndpoint())
+ .authorizationBearer(pat.call())
+ .param("scopeId", scopeId)
+ .response().json(new TypeReference>(){}).execute();
+ }
+ };
+ try {
+ return callable.call();
+ } catch (Exception cause) {
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error querying permission ticket by scope", cause);
+ }
+ }
+
+ /**
+ * Query the server for any permission ticket associated with the given resourceId
.
+ *
+ * @param resourceId the resource id (not {@code null})
+ * @return a list of permission tickets associated with the given resourceId
+ */
+ public List findByResource(final String resourceId) {
+ if (resourceId == null) {
+ throw new IllegalArgumentException("Resource id must not be null");
+ }
+ Callable> callable = new Callable>() {
+ @Override
+ public List call() throws Exception {
+ return http.>get(serverConfiguration.getPermissionEndpoint())
+ .authorizationBearer(pat.call())
+ .param("resourceId", resourceId)
+ .response().json(new TypeReference>(){}).execute();
+ }
+ };
+ try {
+ return callable.call();
+ } catch (Exception cause) {
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error querying permission ticket by resource", cause);
+ }
+ }
+
+ /**
+ * Query the server for any permission ticket with the matching arguments.
+ *
+ * @param resourceId the resource id or name
+ * @param scopeId the scope id or name
+ * @param owner the owner id or name
+ * @param requester the requester id or name
+ * @param granted if true, only permission tickets marked as granted are returned.
+ * @param returnNames if the response should include names for resource, scope and owner
+ * @param firstResult the position of the first resource to retrieve
+ * @param maxResult the maximum number of resources to retrieve
+ * @return a list of permission tickets with the matching arguments
+ */
+ public List find(final String resourceId,
+ final String scopeId,
+ final String owner,
+ final String requester,
+ final Boolean granted,
+ final Boolean returnNames,
+ final Integer firstResult,
+ final Integer maxResult) {
+ Callable> callable = new Callable>() {
+ @Override
+ public List call() throws Exception {
+ return http.>get(serverConfiguration.getPermissionEndpoint())
+ .authorizationBearer(pat.call())
+ .param("resourceId", resourceId)
+ .param("scopeId", scopeId)
+ .param("owner", owner)
+ .param("requester", requester)
+ .param("granted", granted == null ? null : granted.toString())
+ .param("returnNames", returnNames == null ? null : returnNames.toString())
+ .param("firstResult", firstResult == null ? null : firstResult.toString())
+ .param("maxResult", maxResult == null ? null : maxResult.toString())
+ .response().json(new TypeReference>(){}).execute();
+ }
+ };
+ try {
+ return callable.call();
+ } catch (Exception cause) {
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error querying permission ticket", cause);
+ }
+ }
+
+ /**
+ * Updates a permission ticket.
+ *
+ * @param ticket the permission ticket
+ */
+ public void update(final PermissionTicketRepresentation ticket) {
+ if (ticket == null) {
+ throw new IllegalArgumentException("Permission ticket must not be null or empty");
+ }
+ if (ticket.getId() == null) {
+ throw new IllegalArgumentException("Permission ticket must have an id");
+ }
+ Callable callable = new Callable() {
+ @Override
+ public Object call() throws Exception {
+ http.put(serverConfiguration.getPermissionEndpoint())
+ .json(JsonSerialization.writeValueAsBytes(ticket))
+ .authorizationBearer(pat.call())
+ .response().json(List.class).execute();
+ return null;
+ }
+ };
+ try {
+ callable.call();
+ } catch (Exception cause) {
+ Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error updating permission ticket", cause);
}
}
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
index fcf1e436f8..cc92712726 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
@@ -17,88 +17,214 @@
*/
package org.keycloak.authorization.client.resource;
-import static org.keycloak.authorization.client.util.Throwables.handleAndWrapException;
-
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Callable;
-import org.keycloak.authorization.client.representation.RegistrationResponse;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
+import org.keycloak.authorization.client.util.Throwables;
+import org.keycloak.authorization.client.util.TokenCallable;
import org.keycloak.util.JsonSerialization;
/**
+ * An entry point for managing resources using the Protection API.
+ *
* @author Pedro Igor
*/
public class ProtectedResource {
private final Http http;
- private final Callable pat;
+ private ServerConfiguration serverConfiguration;
+ private final TokenCallable pat;
- public ProtectedResource(Http http, Callable pat) {
+ ProtectedResource(Http http, ServerConfiguration serverConfiguration, TokenCallable pat) {
this.http = http;
+ this.serverConfiguration = serverConfiguration;
this.pat = pat;
}
- public RegistrationResponse create(ResourceRepresentation resource) {
+ /**
+ * Creates a new resource.
+ *
+ * @param resource the resource data
+ * @return a {@link RegistrationResponse}
+ */
+ public ResourceRepresentation create(final ResourceRepresentation resource) {
+ Callable callable = new Callable() {
+ @Override
+ public ResourceRepresentation call() throws Exception {
+ return http.post(serverConfiguration.getResourceRegistrationEndpoint())
+ .authorizationBearer(pat.call())
+ .json(JsonSerialization.writeValueAsBytes(resource))
+ .response().json(ResourceRepresentation.class).execute();
+ }
+ };
try {
- return this.http.post("/authz/protection/resource_set")
- .authorizationBearer(this.pat.call())
- .json(JsonSerialization.writeValueAsBytes(resource))
- .response().json(RegistrationResponse.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not create resource", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not create resource", cause);
}
}
- public void update(ResourceRepresentation resource) {
+ /**
+ * Updates a resource.
+ *
+ * @param resource the resource data
+ * @return a {@link RegistrationResponse}
+ */
+ public void update(final ResourceRepresentation resource) {
+ if (resource.getId() == null) {
+ throw new IllegalArgumentException("You must provide the resource id");
+ }
+
+ Callable callable = new Callable() {
+ @Override
+ public Object call() throws Exception {
+ http.put(serverConfiguration.getResourceRegistrationEndpoint() + "/" + resource.getId())
+ .authorizationBearer(pat.call())
+ .json(JsonSerialization.writeValueAsBytes(resource)).execute();
+ return null;
+ }
+ };
try {
- this.http.put("/authz/protection/resource_set/" + resource.getId())
- .authorizationBearer(this.pat.call())
- .json(JsonSerialization.writeValueAsBytes(resource)).execute();
+ callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not update resource", cause);
+ Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not update resource", cause);
}
}
- public RegistrationResponse findById(String id) {
+ /**
+ * Query the server for a resource given its id
.
+ *
+ * @param id the resource id
+ * @return a {@link ResourceRepresentation}
+ */
+ public ResourceRepresentation findById(final String id) {
+ Callable callable = new Callable() {
+ @Override
+ public ResourceRepresentation call() throws Exception {
+ return http.get(serverConfiguration.getResourceRegistrationEndpoint() + "/" + id)
+ .authorizationBearer(pat.call())
+ .response().json(ResourceRepresentation.class).execute();
+ }
+ };
try {
- return this.http.get("/authz/protection/resource_set/" + id)
- .authorizationBearer(this.pat.call())
- .response().json(RegistrationResponse.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not find resource", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not find resource", cause);
}
}
- public Set findByFilter(String filter) {
+ /**
+ * Query the server for a resource given its name
.
+ *
+ * @param id the resource name
+ * @return a {@link ResourceRepresentation}
+ */
+ public ResourceRepresentation findByName(String name) {
+ String[] representations = find(null, name, null, null, null, null, null, null);
+
+ if (representations.length == 0) {
+ return null;
+ }
+
+ return findById(representations[0]);
+ }
+
+ /**
+ * Query the server for any resource with the matching arguments.
+ *
+ * @param id the resource id
+ * @param name the resource name
+ * @param uri the resource uri
+ * @param owner the resource owner
+ * @param type the resource type
+ * @param scope the resource scope
+ * @param firstResult the position of the first resource to retrieve
+ * @param maxResult the maximum number of resources to retrieve
+ * @return an array of strings with the resource ids
+ */
+ public String[] find(final String id, final String name, final String uri, final String owner, final String type, final String scope, final Integer firstResult, final Integer maxResult) {
+ Callable callable = new Callable() {
+ @Override
+ public String[] call() throws Exception {
+ return http.get(serverConfiguration.getResourceRegistrationEndpoint())
+ .authorizationBearer(pat.call())
+ .param("_id", id)
+ .param("name", name)
+ .param("uri", uri)
+ .param("owner", owner)
+ .param("type", type)
+ .param("scope", scope)
+ .param("deep", Boolean.FALSE.toString())
+ .param("first", firstResult != null ? firstResult.toString() : null)
+ .param("max", maxResult != null ? maxResult.toString() : null)
+ .response().json(String[].class).execute();
+ }
+ };
try {
- return this.http.get("/authz/protection/resource_set")
- .authorizationBearer(this.pat.call())
- .param("filter", filter)
- .response().json(Set.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not find resource", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not find resource", cause);
}
}
- public Set findAll() {
+ /**
+ * Query the server for all resources.
+ *
+ * @return @return an array of strings with the resource ids
+ */
+ public String[] findAll() {
try {
- return this.http.get("/authz/protection/resource_set")
- .authorizationBearer(this.pat.call())
- .response().json(Set.class).execute();
+ return find(null,null , null, null, null, null, null, null);
} catch (Exception cause) {
- throw handleAndWrapException("Could not find resource", cause);
+ throw Throwables.handleWrapException("Could not find resource", cause);
}
}
- public void delete(String id) {
+ /**
+ * Deletes a resource with the given id
.
+ *
+ * @param id the resource id
+ */
+ public void delete(final String id) {
+ Callable callable = new Callable() {
+ @Override
+ public Object call() throws Exception {
+ http.delete(serverConfiguration.getResourceRegistrationEndpoint() + "/" + id)
+ .authorizationBearer(pat.call())
+ .execute();
+ return null;
+ }
+ };
try {
- this.http.delete("/authz/protection/resource_set/" + id)
- .authorizationBearer(this.pat.call())
- .execute();
+ callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not delete resource", cause);
+ Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "", cause);
}
}
+
+ /**
+ * Query the server for all resources with the given uri.
+ *
+ * @param uri the resource uri
+ */
+ public List findByUri(String uri) {
+ String[] ids = find(null, null, uri, null, null, null, null, null);
+
+ if (ids.length == 0) {
+ return Collections.emptyList();
+ }
+
+ List representations = new ArrayList<>();
+
+ for (String id : ids) {
+ representations.add(findById(id));
+ }
+
+ return representations;
+ }
}
\ No newline at end of file
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java
index 3d2eb2cfe7..7268fe954a 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java
@@ -17,38 +17,58 @@
*/
package org.keycloak.authorization.client.resource;
-import java.util.concurrent.Callable;
-
+import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
import org.keycloak.authorization.client.util.Http;
+import org.keycloak.authorization.client.util.TokenCallable;
/**
+ * An entry point to access the Protection API endpoints.
+ *
* @author Pedro Igor
*/
public class ProtectionResource {
- private final Callable pat;
+ private final TokenCallable pat;
private final Http http;
+ private ServerConfiguration serverConfiguration;
- public ProtectionResource(Http http, Callable pat) {
+ public ProtectionResource(Http http, ServerConfiguration serverConfiguration, TokenCallable pat) {
if (pat == null) {
throw new RuntimeException("No access token was provided when creating client for Protection API.");
}
this.http = http;
+ this.serverConfiguration = serverConfiguration;
this.pat = pat;
}
+ /**
+ * Creates a {@link ProtectedResource} which can be used to manage resources.
+ *
+ * @return a {@link ProtectedResource}
+ */
public ProtectedResource resource() {
- return new ProtectedResource(http, pat);
+ return new ProtectedResource(http, serverConfiguration, pat);
}
+ /**
+ * Creates a {@link PermissionResource} which can be used to manage permission tickets.
+ *
+ * @return a {@link PermissionResource}
+ */
public PermissionResource permission() {
- return new PermissionResource(http, pat);
+ return new PermissionResource(http, serverConfiguration, pat);
}
+ /**
+ * Introspects the given rpt
using the token introspection endpoint.
+ *
+ * @param rpt the rpt to introspect
+ * @return the {@link TokenIntrospectionResponse}
+ */
public TokenIntrospectionResponse introspectRequestingPartyToken(String rpt) {
- return this.http.post("/protocol/openid-connect/token/introspect")
+ return this.http.post(serverConfiguration.getTokenIntrospectionEndpoint())
.authentication()
.client()
.form()
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
index f72e6b7669..eecb7e30af 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
@@ -22,8 +22,6 @@ import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.representation.ServerConfiguration;
-import java.net.URI;
-
/**
* @author Pedro Igor
*/
@@ -39,27 +37,19 @@ public class Http {
}
public HttpMethod get(String path) {
- return method(RequestBuilder.get().setUri(this.serverConfiguration.getIssuer() + path));
- }
-
- public HttpMethod get(URI path) {
return method(RequestBuilder.get().setUri(path));
}
- public HttpMethod post(URI path) {
+ public HttpMethod post(String path) {
return method(RequestBuilder.post().setUri(path));
}
- public HttpMethod post(String path) {
- return method(RequestBuilder.post().setUri(this.serverConfiguration.getIssuer() + path));
- }
-
public HttpMethod put(String path) {
- return method(RequestBuilder.put().setUri(this.serverConfiguration.getIssuer() + path));
+ return method(RequestBuilder.put().setUri(path));
}
public HttpMethod delete(String path) {
- return method(RequestBuilder.delete().setUri(this.serverConfiguration.getIssuer() + path));
+ return method(RequestBuilder.delete().setUri(path));
}
private HttpMethod method(RequestBuilder builder) {
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
index 9a7e51a72e..230b7f8c74 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
@@ -43,17 +43,17 @@ public class HttpMethod {
private final HttpClient httpClient;
private final ClientAuthenticator authenticator;
- private final RequestBuilder builder;
+ protected final RequestBuilder builder;
protected final Configuration configuration;
- protected final HashMap headers;
- protected final HashMap params;
+ protected final Map headers;
+ protected final Map> params;
private HttpMethodResponse response;
public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder) {
- this(configuration, authenticator, builder, new HashMap(), new HashMap());
+ this(configuration, authenticator, builder, new HashMap>(), new HashMap());
}
- public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, HashMap params, HashMap headers) {
+ public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, Map> params, Map headers) {
this.configuration = configuration;
this.httpClient = configuration.getHttpClient();
this.authenticator = authenticator;
@@ -108,8 +108,10 @@ public class HttpMethod {
}
protected void preExecute(RequestBuilder builder) {
- for (Map.Entry param : params.entrySet()) {
- builder.addParameter(param.getKey(), param.getValue());
+ for (Map.Entry> param : params.entrySet()) {
+ for (String value : param.getValue()) {
+ builder.addParameter(param.getKey(), value);
+ }
}
}
@@ -128,7 +130,30 @@ public class HttpMethod {
}
public HttpMethod param(String name, String value) {
- this.params.put(name, value);
+ if (value != null) {
+ List values = params.get(name);
+
+ if (values == null || !values.isEmpty()) {
+ values = new ArrayList<>();
+ params.put(name, values);
+ }
+
+ values.add(value);
+ }
+ return this;
+ }
+
+ public HttpMethod params(String name, String value) {
+ if (value != null) {
+ List values = params.get(name);
+
+ if (values == null) {
+ values = new ArrayList<>();
+ params.put(name, values);
+ }
+
+ values.add(value);
+ }
return this;
}
@@ -145,8 +170,10 @@ public class HttpMethod {
if (params != null) {
List formparams = new ArrayList<>();
- for (Map.Entry param : params.entrySet()) {
- formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
+ for (Map.Entry> param : params.entrySet()) {
+ for (String value : param.getValue()) {
+ formparams.add(new BasicNameValuePair(param.getKey(), value));
+ }
}
try {
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
index 8807d393bd..33674fbc0c 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
@@ -17,8 +17,16 @@
*/
package org.keycloak.authorization.client.util;
+import java.util.Arrays;
+import java.util.Set;
+
+import org.apache.http.Header;
import org.keycloak.OAuth2Constants;
import org.keycloak.authorization.client.ClientAuthenticator;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
+import org.keycloak.representations.idm.authorization.PermissionTicketToken;
+import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission;
/**
* @author Pedro Igor
@@ -34,16 +42,84 @@ public class HttpMethodAuthenticator {
}
public HttpMethod client() {
- this.method.params.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS);
+ this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.CLIENT_CREDENTIALS));
authenticator.configureClientCredentials(this.method.params, this.method.headers);
return this.method;
}
public HttpMethod oauth2ResourceOwnerPassword(String userName, String password) {
client();
- this.method.params.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
- this.method.params.put("username", userName);
- this.method.params.put("password", password);
+ this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.PASSWORD));
+ this.method.params.put("username", Arrays.asList(userName));
+ this.method.params.put("password", Arrays.asList(password));
return this.method;
}
+
+ public HttpMethod uma() {
+ // if there is an authorization bearer header authenticate using bearer token
+ Header authorizationHeader = method.builder.getFirstHeader("Authorization");
+
+ if (!(authorizationHeader != null && authorizationHeader.getValue().toLowerCase().startsWith("bearer"))) {
+ client();
+ }
+
+ method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.UMA_GRANT_TYPE));
+ return method;
+ }
+
+ public HttpMethod uma(AuthorizationRequest request) {
+ String ticket = request.getTicket();
+ PermissionTicketToken permissions = request.getPermissions();
+
+ if (ticket == null && permissions == null) {
+ throw new IllegalArgumentException("You must either provide a permission ticket or the permissions you want to request.");
+ }
+
+ uma();
+ method.param("ticket", ticket);
+ method.param("claim_token", request.getClaimToken());
+ method.param("claim_token_format", request.getClaimTokenFormat());
+ method.param("pct", request.getPct());
+ method.param("rpt", request.getRpt());
+ method.param("scope", request.getScope());
+ method.param("audience", request.getAudience());
+
+ if (permissions != null) {
+ for (ResourcePermission permission : permissions.getResources()) {
+ String resourceId = permission.getResourceId();
+ Set scopes = permission.getScopes();
+ StringBuilder value = new StringBuilder();
+
+ if (resourceId != null) {
+ value.append(resourceId);
+ }
+
+ if (scopes != null && !scopes.isEmpty()) {
+ value.append("#");
+ for (String scope : scopes) {
+ if (!value.toString().endsWith("#")) {
+ value.append(",");
+ }
+ value.append(scope);
+ }
+ }
+
+ method.params("permission", value.toString());
+ }
+ }
+
+ Metadata metadata = request.getMetadata();
+
+ if (metadata != null) {
+ if (metadata.getIncludeResourceName() != null) {
+ method.param("response_include_resource_name", metadata.getIncludeResourceName().toString());
+ }
+
+ if (metadata.getLimit() != null) {
+ method.param("response_permissions_limit", metadata.getLimit().toString());
+ }
+ }
+
+ return method;
+ }
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
index fceca19d61..7cfba8ef0b 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
@@ -17,10 +17,12 @@
*/
package org.keycloak.authorization.client.util;
-import org.keycloak.util.JsonSerialization;
-
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.keycloak.util.JsonSerialization;
+
/**
* @author Pedro Igor
*/
@@ -58,4 +60,22 @@ public class HttpMethodResponse {
}
};
}
+
+ public HttpMethodResponse json(final TypeReference responseType) {
+ return new HttpMethodResponse(this.method) {
+ @Override
+ public R execute() {
+ return method.execute(new HttpResponseProcessor() {
+ @Override
+ public R process(byte[] entity) {
+ try {
+ return (R) JsonSerialization.readValue(new ByteArrayInputStream(entity), responseType);
+ } catch (IOException e) {
+ throw new RuntimeException("Error parsing JSON response.", e);
+ }
+ }
+ });
+ }
+ };
+ }
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java
index d51b27ecc7..ae2eaf11a6 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java
@@ -16,7 +16,10 @@
*/
package org.keycloak.authorization.client.util;
+import java.util.concurrent.Callable;
+
import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
/**
* @author Pedro Igor
@@ -24,19 +27,68 @@ import org.keycloak.authorization.client.AuthorizationDeniedException;
public final class Throwables {
/**
- * Handles an {@code exception} and wraps it into a {@link RuntimeException}. The resulting exception contains
- * more details in case the given {@code exception} is of a {@link HttpResponseException}.
+ * Handles an {@code cause} and wraps it into a {@link RuntimeException}. The resulting cause contains
+ * more details in case the given {@code cause} is of a {@link HttpResponseException}.
*
+ *
+ * @param callable
+ * @param pat
* @param message the message
- * @param exception the root exception
- * @return a {@link RuntimeException} wrapping the given {@code exception}
+ * @param cause the root cause
+ * @return a {@link RuntimeException} wrapping the given {@code cause}
*/
- public static RuntimeException handleAndWrapException(String message, Exception exception) {
- if (exception instanceof HttpResponseException) {
- throw handleAndWrapHttpResponseException(message, HttpResponseException.class.cast(exception));
+ public static RuntimeException handleWrapException(String message, Throwable cause) {
+ if (cause instanceof HttpResponseException) {
+ throw handleAndWrapHttpResponseException(message, HttpResponseException.class.cast(cause));
}
- return new RuntimeException(message, exception);
+ return new RuntimeException(message, cause);
+ }
+
+ /**
+ * Retries the given {@code callable} after obtaining a fresh {@code token} from the server. If the attempt to retry fails
+ * the exception is handled as defined by {@link #handleWrapException(String, Throwable)}.
+ *
+ *
A retry is only attempted in case the {@code cause} is a {@link HttpResponseException} with a 403 status code. In some cases the
+ * session associated with the token is no longer valid and a new token must be issues.
+ *
+ * @param callable the callable to retry
+ * @param token the token
+ * @param message the message
+ * @param cause the cause
+ * @param the result of the callable
+ * @return the result of the callable
+ * @throws RuntimeException in case the attempt to retry fails
+ */
+ public static V retryAndWrapExceptionIfNecessary(Callable callable, TokenCallable token, String message, Throwable cause) throws RuntimeException {
+ if (token == null || !token.isRetry()) {
+ throw handleWrapException(message, cause);
+ }
+
+ if (cause instanceof HttpResponseException) {
+ HttpResponseException httpe = HttpResponseException.class.cast(cause);
+
+ if (httpe.getStatusCode() == 403) {
+ TokenIntrospectionResponse response = token.getHttp().post(token.getServerConfiguration().getTokenIntrospectionEndpoint())
+ .authentication()
+ .client()
+ .param("token", token.call())
+ .response().json(TokenIntrospectionResponse.class).execute();
+
+ if (!response.getActive()) {
+ token.clearToken();
+ try {
+ return callable.call();
+ } catch (Exception e) {
+ throw handleWrapException(message, e);
+ }
+ }
+
+ throw handleWrapException(message, cause);
+ }
+ }
+
+ throw new RuntimeException(message, cause);
}
private static RuntimeException handleAndWrapHttpResponseException(String message, HttpResponseException exception) {
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java
new file mode 100644
index 0000000000..b1c328020f
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2018 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.client.util;
+
+import java.util.concurrent.Callable;
+
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.util.JsonSerialization;
+
+public class TokenCallable implements Callable {
+
+ private final String userName;
+ private final String password;
+ private final Http http;
+ private final Configuration configuration;
+ private final ServerConfiguration serverConfiguration;
+ private AccessTokenResponse clientToken;
+
+ public TokenCallable(String userName, String password, Http http, Configuration configuration, ServerConfiguration serverConfiguration) {
+ this.userName = userName;
+ this.password = password;
+ this.http = http;
+ this.configuration = configuration;
+ this.serverConfiguration = serverConfiguration;
+ }
+
+ public TokenCallable(Http http, Configuration configuration, ServerConfiguration serverConfiguration) {
+ this(null, null, http, configuration, serverConfiguration);
+ }
+
+ @Override
+ public String call() {
+ if (clientToken == null) {
+ if (userName == null || password == null) {
+ clientToken = obtainAccessToken();
+ } else {
+ clientToken = obtainAccessToken(userName, password);
+ }
+ }
+
+ String token = clientToken.getToken();
+
+ try {
+ AccessToken accessToken = JsonSerialization.readValue(new JWSInput(token).getContent(), AccessToken.class);
+
+ if (accessToken.isActive()) {
+ return token;
+ }
+
+ clientToken = http.post(serverConfiguration.getTokenEndpoint())
+ .authentication().client()
+ .form()
+ .param("grant_type", "refresh_token")
+ .param("refresh_token", clientToken.getRefreshToken())
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ } catch (Exception e) {
+ clientToken = null;
+ throw new RuntimeException(e);
+ }
+
+ return clientToken.getToken();
+ }
+
+ /**
+ * Obtains an access token using the client credentials.
+ *
+ * @return an {@link AccessTokenResponse}
+ */
+ AccessTokenResponse obtainAccessToken() {
+ return this.http.post(this.serverConfiguration.getTokenEndpoint())
+ .authentication()
+ .client()
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ }
+
+ /**
+ * Obtains an access token using the resource owner credentials.
+ *
+ * @return an {@link AccessTokenResponse}
+ */
+ AccessTokenResponse obtainAccessToken(String userName, String password) {
+ return this.http.post(this.serverConfiguration.getTokenEndpoint())
+ .authentication()
+ .oauth2ResourceOwnerPassword(userName, password)
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ }
+
+ Http getHttp() {
+ return http;
+ }
+
+ protected boolean isRetry() {
+ return true;
+ }
+
+ Configuration getConfiguration() {
+ return configuration;
+ }
+
+ ServerConfiguration getServerConfiguration() {
+ return serverConfiguration;
+ }
+
+ void clearToken() {
+ clientToken = null;
+ }
+}
diff --git a/common/src/main/java/org/keycloak/common/util/Time.java b/common/src/main/java/org/keycloak/common/util/Time.java
index 54809d8dab..e48f217a9c 100644
--- a/common/src/main/java/org/keycloak/common/util/Time.java
+++ b/common/src/main/java/org/keycloak/common/util/Time.java
@@ -51,6 +51,15 @@ public class Time {
return new Date(((long) time ) * 1000);
}
+ /**
+ * Returns {@link Date} object, its value set to time
+ * @param time Time in milliseconds since the epoch
+ * @return see description
+ */
+ public static Date toDate(long time) {
+ return new Date(time);
+ }
+
/**
* Returns time in milliseconds for a time in seconds. No adjustment is made to the parameter.
* @param time Time in seconds since the epoch
diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java
index e096e7e2e0..0a9b33259c 100644
--- a/core/src/main/java/org/keycloak/AuthorizationContext.java
+++ b/core/src/main/java/org/keycloak/AuthorizationContext.java
@@ -68,7 +68,7 @@ public class AuthorizationContext {
if (hasResourcePermission(resourceName)) {
for (Permission permission : authorization.getPermissions()) {
for (PathConfig pathHolder : paths.values()) {
- if (pathHolder.getId().equals(permission.getResourceSetId())) {
+ if (pathHolder.getId().equals(permission.getResourceId())) {
if (permission.getScopes().contains(scopeName)) {
return true;
}
@@ -98,7 +98,7 @@ public class AuthorizationContext {
}
for (Permission permission : authorization.getPermissions()) {
- if (permission.getResourceSetName().equals(resourceName) || permission.getResourceSetId().equals(resourceName)) {
+ if (permission.getResourceName().equals(resourceName) || permission.getResourceId().equals(resourceName)) {
return true;
}
}
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 59e0eeed4e..df5411257d 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -111,6 +111,8 @@ public interface OAuth2Constants {
String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt";
String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token";
+ String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
+
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index 26dc22005b..89dadbfdad 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -17,14 +17,13 @@
*/
package org.keycloak.representations.adapters.config;
+import java.util.ArrayList;
+import java.util.List;
+
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
/**
* @author Pedro Igor
*/
@@ -37,26 +36,18 @@ public class PolicyEnforcerConfig {
@JsonProperty("enforcement-mode")
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
- @JsonProperty("user-managed-access")
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private UmaProtocolConfig userManagedAccess;
-
- @JsonProperty("entitlement")
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private EntitlementProtocolConfig entitlement;
-
@JsonProperty("paths")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List paths = new ArrayList<>();
- @JsonProperty("online-introspection")
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private Boolean onlineIntrospection = Boolean.FALSE;
-
@JsonProperty("on-deny-redirect-to")
@JsonInclude(JsonInclude.Include.NON_NULL)
private String onDenyRedirectTo;
+ @JsonProperty("user-managed-access")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private UserManagedAccessConfig userManagedAccess;
+
public Boolean isCreateResources() {
return this.createResources;
}
@@ -73,26 +64,14 @@ public class PolicyEnforcerConfig {
this.enforcementMode = enforcementMode;
}
- public UmaProtocolConfig getUserManagedAccess() {
+ public UserManagedAccessConfig getUserManagedAccess() {
return this.userManagedAccess;
}
- public EntitlementProtocolConfig getEntitlement() {
- return this.entitlement;
- }
-
- public Boolean isOnlineIntrospection() {
- return onlineIntrospection;
- }
-
public void setCreateResources(Boolean createResources) {
this.createResources = createResources;
}
- public void setOnlineIntrospection(Boolean onlineIntrospection) {
- this.onlineIntrospection = onlineIntrospection;
- }
-
public void setPaths(List paths) {
this.paths = paths;
}
@@ -101,14 +80,10 @@ public class PolicyEnforcerConfig {
return onDenyRedirectTo;
}
- public void setUserManagedAccess(UmaProtocolConfig userManagedAccess) {
+ public void setUserManagedAccess(UserManagedAccessConfig userManagedAccess) {
this.userManagedAccess = userManagedAccess;
}
- public void setEntitlement(EntitlementProtocolConfig entitlement) {
- this.entitlement = entitlement;
- }
-
public void setOnDenyRedirectTo(String onDenyRedirectTo) {
this.onDenyRedirectTo = onDenyRedirectTo;
}
@@ -259,11 +234,7 @@ public class PolicyEnforcerConfig {
ANY
}
- public static class UmaProtocolConfig {
-
- }
-
- public static class EntitlementProtocolConfig {
+ public static class UserManagedAccessConfig {
}
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index cd52d7ce0d..1c039d3033 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -145,6 +145,8 @@ public class RealmRepresentation {
protected String keycloakVersion;
+ protected Boolean userManagedAccessAllowed;
+
@Deprecated
protected Boolean social;
@Deprecated
@@ -964,4 +966,12 @@ public class RealmRepresentation {
public void setFederatedUsers(List federatedUsers) {
this.federatedUsers = federatedUsers;
}
+
+ public void setUserManagedAccessAllowed(Boolean userManagedAccessAllowed) {
+ this.userManagedAccessAllowed = userManagedAccessAllowed;
+ }
+
+ public Boolean isUserManagedAccessAllowed() {
+ return userManagedAccessAllowed;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
new file mode 100644
index 0000000000..764ae02e6b
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2018 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.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission;
+
+/**
+ * @author Pedro Igor
+ */
+public class AuthorizationRequest {
+
+ private String ticket;
+ private String rpt;
+ private String claimToken;
+ private String claimTokenFormat;
+ private String pct;
+ private String scope;
+ private PermissionTicketToken permissions = new PermissionTicketToken();
+ private Metadata metadata;
+ private String audience;
+ private String accessToken;
+ private boolean submitRequest;
+
+ public AuthorizationRequest(String ticket) {
+ this.ticket = ticket;
+ }
+
+ public AuthorizationRequest() {
+ this(null);
+ }
+
+ public String getTicket() {
+ return this.ticket;
+ }
+
+ public void setTicket(String ticket) {
+ this.ticket = ticket;
+ }
+
+ public String getRpt() {
+ return this.rpt;
+ }
+
+ public void setRpt(String rpt) {
+ this.rpt = rpt;
+ }
+
+ public void setClaimToken(String claimToken) {
+ this.claimToken = claimToken;
+ }
+
+ public String getClaimToken() {
+ return claimToken;
+ }
+
+ public void setClaimTokenFormat(String claimTokenFormat) {
+ this.claimTokenFormat = claimTokenFormat;
+ }
+
+ public String getClaimTokenFormat() {
+ return claimTokenFormat;
+ }
+
+ public void setPct(String pct) {
+ this.pct = pct;
+ }
+
+ public String getPct() {
+ return pct;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setPermissions(PermissionTicketToken permissions) {
+ this.permissions = permissions;
+ }
+
+ public PermissionTicketToken getPermissions() {
+ return permissions;
+ }
+
+ public Metadata getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Metadata metadata) {
+ this.metadata = metadata;
+ }
+
+ public void setAudience(String audience) {
+ this.audience = audience;
+ }
+
+ public String getAudience() {
+ return audience;
+ }
+
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public void addPermission(String resourceId, List scopes) {
+ addPermission(resourceId, scopes.toArray(new String[scopes.size()]));
+ }
+
+ public void addPermission(String resourceId, String... scopes) {
+ if (permissions == null) {
+ permissions = new PermissionTicketToken(new ArrayList());
+ }
+
+ ResourcePermission permission = null;
+
+ for (ResourcePermission resourcePermission : permissions.getResources()) {
+ if (resourcePermission.getResourceId().equals(resourceId)) {
+ permission = resourcePermission;
+ break;
+ }
+ }
+
+ if (permission == null) {
+ permission = new ResourcePermission(resourceId, new HashSet());
+ permissions.getResources().add(permission);
+ }
+
+ permission.getScopes().addAll(Arrays.asList(scopes));
+ }
+
+ public void setSubmitRequest(boolean submitRequest) {
+ this.submitRequest = submitRequest;
+ }
+
+ public boolean isSubmitRequest() {
+ return submitRequest && ticket != null;
+ }
+
+ public static class Metadata {
+
+ private Boolean includeResourceName;
+ private Integer limit;
+
+ public Boolean getIncludeResourceName() {
+ if (includeResourceName == null) {
+ includeResourceName = Boolean.TRUE;
+ }
+ return includeResourceName;
+ }
+
+ public void setIncludeResourceName(Boolean includeResourceName) {
+ this.includeResourceName = includeResourceName;
+ }
+
+ public Integer getLimit() {
+ return limit;
+ }
+
+ public void setLimit(Integer limit) {
+ this.limit = limit;
+ }
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationResponse.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationResponse.java
new file mode 100644
index 0000000000..449e1b75c5
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 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 org.keycloak.representations.AccessTokenResponse;
+
+/**
+ * @author Pedro Igor
+ */
+public class AuthorizationResponse extends AccessTokenResponse {
+
+ private boolean upgraded;
+
+ public AuthorizationResponse() {
+ }
+
+ public AuthorizationResponse(AccessTokenResponse response, boolean upgraded) {
+ setToken(response.getToken());
+ setTokenType("Bearer");
+ setRefreshToken(response.getRefreshToken());
+ setRefreshExpiresIn(response.getRefreshExpiresIn());
+ setExpiresIn(response.getExpiresIn());
+ setNotBeforePolicy(response.getNotBeforePolicy());
+ this.upgraded = upgraded;
+ }
+
+ public boolean isUpgraded() {
+ return upgraded;
+ }
+
+ public void setUpgraded(boolean upgraded) {
+ this.upgraded = upgraded;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
index 74df64fde5..ed392f07a7 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
@@ -28,11 +28,11 @@ import java.util.Set;
*/
public class Permission {
- @JsonProperty("resource_set_id")
- private String resourceSetId;
+ @JsonProperty("rsid")
+ private String resourceId;
- @JsonProperty("resource_set_name")
- private final String resourceSetName;
+ @JsonProperty("rsname")
+ private final String resourceName;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Set scopes;
@@ -44,19 +44,19 @@ public class Permission {
this(null, null, null, null);
}
- public Permission(final String resourceSetId, String resourceSetName, final Set scopes, Map> claims) {
- this.resourceSetId = resourceSetId;
- this.resourceSetName = resourceSetName;
+ public Permission(final String resourceId, String resourceName, final Set scopes, Map> claims) {
+ this.resourceId = resourceId;
+ this.resourceName = resourceName;
this.scopes = scopes;
this.claims = claims;
}
- public String getResourceSetId() {
- return this.resourceSetId;
+ public String getResourceId() {
+ return this.resourceId;
}
- public String getResourceSetName() {
- return this.resourceSetName;
+ public String getResourceName() {
+ return this.resourceName;
}
public Set getScopes() {
@@ -75,7 +75,7 @@ public class Permission {
public String toString() {
StringBuilder builder = new StringBuilder();
- builder.append("Permission {").append("id=").append(resourceSetId).append(", name=").append(resourceSetName)
+ builder.append("Permission {").append("id=").append(resourceId).append(", name=").append(resourceName)
.append(", scopes=").append(scopes).append("}");
return builder.toString();
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
new file mode 100644
index 0000000000..5830e160da
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 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.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author Pedro Igor
+ */
+public class PermissionRequest {
+
+ private String resourceId;
+ private Set scopes;
+ private String resourceServerId;
+
+ public PermissionRequest(String resourceId, String... scopes) {
+ this.resourceId = resourceId;
+ if (scopes != null) {
+ this.scopes = new HashSet(Arrays.asList(scopes));
+ }
+ }
+
+ public PermissionRequest() {
+ this(null, null);
+ }
+
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ @JsonProperty("resource_id")
+ public void setResourceId(String resourceSetId) {
+ this.resourceId = resourceSetId;
+ }
+
+ public Set getScopes() {
+ return scopes;
+ }
+
+ @JsonProperty("resource_scopes")
+ public void setScopes(Set scopes) {
+ this.scopes = scopes;
+ }
+
+ @JsonProperty("resource_server_id")
+ public void setResourceServerId(String resourceServerId) {
+ this.resourceServerId = resourceServerId;
+ }
+
+ public String getResourceServerId() {
+ return resourceServerId;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionResponse.java
similarity index 79%
rename from authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java
rename to core/src/main/java/org/keycloak/representations/idm/authorization/PermissionResponse.java
index 1002bb59c3..2d2d7bbeea 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionResponse.java
@@ -1,13 +1,12 @@
/*
- * JBoss, Home of Professional Open Source
- *
- * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ * Copyright 2018 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
+ * 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,
@@ -15,7 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.keycloak.authorization.client.representation;
+
+package org.keycloak.representations.idm.authorization;
/**
* @author Pedro Igor
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketRepresentation.java
new file mode 100644
index 0000000000..2a3e020b2b
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketRepresentation.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 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;
+
+/**
+ * @author Pedro Igor
+ */
+public class PermissionTicketRepresentation {
+
+ private String id;
+ private String owner;
+ private String resource;
+ private String scope;
+ private boolean granted;
+ private String scopeName;
+ private String resourceName;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ public boolean isGranted() {
+ return granted;
+ }
+
+ public void setGranted(boolean granted) {
+ this.granted = granted;
+ }
+
+ public void setScopeName(String scopeName) {
+ this.scopeName = scopeName;
+ }
+
+ public String getScopeName() {
+ return scopeName;
+ }
+
+ public void setResourceName(String resourceName) {
+ this.resourceName = resourceName;
+ }
+
+ public String getResourceName() {
+ return resourceName;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
new file mode 100644
index 0000000000..ff4a927a3a
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 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.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.TokenIdGenerator;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.JsonWebToken;
+
+/**
+ * @author Pedro Igor
+ */
+public class PermissionTicketToken extends JsonWebToken {
+
+ private final List resources;
+
+ public PermissionTicketToken() {
+ this(new ArrayList());
+ }
+
+ public PermissionTicketToken(List resources, String audience, AccessToken accessToken) {
+ if (accessToken != null) {
+ id(TokenIdGenerator.generateId());
+ subject(accessToken.getSubject());
+ expiration(accessToken.getExpiration());
+ notBefore(accessToken.getNotBefore());
+ issuedAt(accessToken.getIssuedAt());
+ issuedFor(accessToken.getIssuedFor());
+ }
+ if (audience != null) {
+ audience(audience);
+ }
+ this.resources = resources;
+ }
+
+ public PermissionTicketToken(List resources) {
+ this(resources, null, null);
+ }
+
+ public List getResources() {
+ return this.resources;
+ }
+
+ public static class ResourcePermission {
+
+ @JsonProperty("id")
+ private String resourceId;
+
+ @JsonProperty("scopes")
+ private Set scopes;
+
+ public ResourcePermission() {
+ }
+
+ public ResourcePermission(String resourceId, Set scopes) {
+ this.resourceId = resourceId;
+ this.scopes = scopes;
+ }
+
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ public Set getScopes() {
+ return scopes;
+ }
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java
index c058b9d112..4188ab3fe5 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java
@@ -24,6 +24,14 @@ public class ResourceOwnerRepresentation {
private String id;
private String name;
+ public ResourceOwnerRepresentation() {
+
+ }
+
+ public ResourceOwnerRepresentation(String id) {
+ this.id = id;
+ }
+
public String getId() {
return this.id;
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
index acbd2f2430..ae876f0347 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
@@ -47,10 +47,12 @@ public class ResourceRepresentation {
@JsonProperty("icon_uri")
private String iconUri;
private ResourceOwnerRepresentation owner;
+ private Boolean ownerManagedAccess;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List policies;
private List typedScopes;
+ private String displayName;
/**
* Creates a new instance.
@@ -121,6 +123,10 @@ public class ResourceRepresentation {
return this.name;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
public String getUri() {
return this.uri;
}
@@ -145,6 +151,10 @@ public class ResourceRepresentation {
this.name = name;
}
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
public void setUri(String uri) {
this.uri = uri;
}
@@ -169,6 +179,14 @@ public class ResourceRepresentation {
this.owner = owner;
}
+ public Boolean getOwnerManagedAccess() {
+ return ownerManagedAccess;
+ }
+
+ public void setOwnerManagedAccess(Boolean ownerManagedAccess) {
+ this.ownerManagedAccess = ownerManagedAccess;
+ }
+
public void setTypedScopes(List typedScopes) {
this.typedScopes = typedScopes;
}
@@ -177,6 +195,15 @@ public class ResourceRepresentation {
return typedScopes;
}
+ public void addScope(String... scopeNames) {
+ if (scopes == null) {
+ scopes = new HashSet<>();
+ }
+ for (String scopeName : scopeNames) {
+ scopes.add(new ScopeRepresentation(scopeName));
+ }
+ }
+
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java
index 3a1f2525f9..e6445e4abc 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java
@@ -35,6 +35,7 @@ public class ScopeRepresentation {
private String iconUri;
private List policies;
private List resources;
+ private String displayName;
/**
* Creates an instance.
@@ -67,6 +68,10 @@ public class ScopeRepresentation {
return this.name;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
public String getIconUri() {
return this.iconUri;
}
@@ -83,6 +88,10 @@ public class ScopeRepresentation {
this.name = name;
}
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
public void setIconUri(String iconUri) {
this.iconUri = iconUri;
}
diff --git a/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
index 0aea6b0452..c511b2da9d 100644
--- a/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
+++ b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
@@ -38,8 +38,8 @@
for (Permission permission : authzContext.getPermissions()) {
%>
- Resource: <%= permission.getResourceSetName() %>
- ID: <%= permission.getResourceSetId() %>
+ Resource: <%= permission.getResourceName() %>
+ ID: <%= permission.getResourceId() %>
<%
}
diff --git a/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
index ea37d60e9a..4c4573b0b1 100644
--- a/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
+++ b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
@@ -18,18 +18,14 @@
package org.keycloak.authz.helloworld;
import org.keycloak.authorization.client.AuthzClient;
-import org.keycloak.authorization.client.representation.EntitlementRequest;
-import org.keycloak.authorization.client.representation.EntitlementResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.authorization.client.representation.RegistrationResponse;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
import org.keycloak.authorization.client.resource.ProtectedResource;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
-import java.util.Set;
-
/**
* @author Pedro Igor
*/
@@ -47,28 +43,10 @@ public class AuthorizationClientExample {
// create a new instance based on the configuration defined in keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();
- // query the server for a resource with a given name
- Set resourceId = authzClient.protection()
- .resource()
- .findByFilter("name=Default Resource");
-
- // obtain an Entitlement API Token in order to get access to the Entitlement API.
- // this token is just an access token issued to a client on behalf of an user
- // with a scope = kc_entitlement
- String eat = getEntitlementAPIToken(authzClient);
-
- // create an entitlement request
- EntitlementRequest request = new EntitlementRequest();
- PermissionRequest permission = new PermissionRequest();
-
- permission.setResourceSetId(resourceId.iterator().next());
-
- request.addPermission(permission);
-
- // send the entitlement request to the server in order to
+ // send the authorization request to the server in order to
// obtain an RPT with all permissions granted to the user
- EntitlementResponse response = authzClient.entitlement(eat).get("hello-world-authz-service", request);
- String rpt = response.getRpt();
+ AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize();
+ String rpt = response.getToken();
TokenIntrospectionResponse requestingPartyToken = authzClient.protection().introspectRequestingPartyToken(rpt);
@@ -78,7 +56,6 @@ public class AuthorizationClientExample {
for (Permission granted : requestingPartyToken.getPermissions()) {
System.out.println(granted);
}
-
}
private static void createResource() {
@@ -94,18 +71,18 @@ public class AuthorizationClientExample {
newResource.addScope(new ScopeRepresentation("urn:hello-world-authz:scopes:view"));
ProtectedResource resourceClient = authzClient.protection().resource();
- Set existingResource = resourceClient.findByFilter("name=" + newResource.getName());
+ ResourceRepresentation existingResource = resourceClient.findByName(newResource.getName());
- if (!existingResource.isEmpty()) {
- resourceClient.delete(existingResource.iterator().next());
+ if (existingResource != null) {
+ resourceClient.delete(existingResource.getId());
}
// create the resource on the server
- RegistrationResponse response = resourceClient.create(newResource);
+ ResourceRepresentation response = resourceClient.create(newResource);
String resourceId = response.getId();
// query the resource using its newly generated id
- ResourceRepresentation resource = resourceClient.findById(resourceId).getResourceDescription();
+ ResourceRepresentation resource = resourceClient.findById(resourceId);
System.out.println(resource);
}
@@ -120,20 +97,20 @@ public class AuthorizationClientExample {
resource.setName("New Resource");
ProtectedResource resourceClient = authzClient.protection().resource();
- Set existingResource = resourceClient.findByFilter("name=" + resource.getName());
+ ResourceRepresentation existingResource = resourceClient.findByName(resource.getName());
- if (existingResource.isEmpty()) {
+ if (existingResource == null) {
createResource();
}
- resource.setId(existingResource.iterator().next());
+ resource.setId(existingResource.getId());
resource.setUri("Changed URI");
// update the resource on the server
resourceClient.update(resource);
// query the resource using its newly generated id
- ResourceRepresentation existing = resourceClient.findById(resource.getId()).getResourceDescription();
+ ResourceRepresentation existing = resourceClient.findById(resource.getId());
System.out.println(existing);
}
@@ -142,23 +119,16 @@ public class AuthorizationClientExample {
// create a new instance based on the configuration define at keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();
- // obtain an Entitlement API Token in order to get access to the Entitlement API.
- // this token is just an access token issued to a client on behalf of an user
- // with a scope = kc_entitlement
- String eat = getEntitlementAPIToken(authzClient);
+ // create an authorization request
+ AuthorizationRequest request = new AuthorizationRequest();
- // create an entitlement request
- EntitlementRequest request = new EntitlementRequest();
- PermissionRequest permission = new PermissionRequest();
+ // add permissions to the request based on the resources and scopes you want to check access
+ request.addPermission("Default Resource");
- permission.setResourceSetName("Default Resource");
-
- request.addPermission(permission);
-
- // send the entitlement request to the server in order to obtain a RPT
- // with all permissions granted to the user
- EntitlementResponse response = authzClient.entitlement(eat).get("hello-world-authz-service", request);
- String rpt = response.getRpt();
+ // send the entitlement request to the server in order to
+ // obtain an RPT with permissions for a single resource
+ AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(request);
+ String rpt = response.getToken();
System.out.println("You got a RPT: " + rpt);
@@ -169,27 +139,16 @@ public class AuthorizationClientExample {
// create a new instance based on the configuration defined in keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();
- // obtian a Entitlement API Token in order to get access to the Entitlement API.
- // this token is just an access token issued to a client on behalf of an user with a scope kc_entitlement
- String eat = getEntitlementAPIToken(authzClient);
+ // create an authorization request
+ AuthorizationRequest request = new AuthorizationRequest();
- // send the entitlement request to the server in order to obtain a RPT with all permissions granted to the user
- EntitlementResponse response = authzClient.entitlement(eat).getAll("hello-world-authz-service");
- String rpt = response.getRpt();
+ // send the entitlement request to the server in order to
+ // obtain an RPT with all permissions granted to the user
+ AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(request);
+ String rpt = response.getToken();
System.out.println("You got a RPT: " + rpt);
// now you can use the RPT to access protected resources on the resource server
}
-
- /**
- * Obtain an Entitlement API Token or EAT from the server. Usually, EATs are going to be obtained by clients using a
- * authorization_code grant type. Here we are using resource owner credentials for demonstration purposes.
- *
- * @param authzClient the authorization client instance
- * @return a string representing a EAT
- */
- private static String getEntitlementAPIToken(AuthzClient authzClient) {
- return authzClient.obtainAccessToken("alice", "alice").getToken();
- }
}
diff --git a/examples/authz/hello-world/src/main/resources/keycloak.json b/examples/authz/hello-world/src/main/resources/keycloak.json
index b337389c89..4f9b0e5904 100644
--- a/examples/authz/hello-world/src/main/resources/keycloak.json
+++ b/examples/authz/hello-world/src/main/resources/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "hello-world-authz",
- "auth-server-url" : "http://localhost:8080/auth",
+ "auth-server-url" : "http://localhost:8180/auth",
"resource" : "hello-world-authz-service",
"credentials": {
"secret": "secret"
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
index 203b6e2095..158d89f426 100755
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
@@ -11,15 +11,15 @@
-
-
+
+
-Show Requesting Party Token | Show Access Token | Request Entitlements | Sign Out
+Show Requesting Party Token | Show Access Token | Request Entitlements | My Account | Sign Out
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js b/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js
index e58c5f55d7..b552391ca2 100755
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js
@@ -42,6 +42,9 @@ module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Albu
Album.query(function (albums) {
$scope.albums = albums;
});
+ Album.shares(function (albums) {
+ $scope.shares = albums;
+ });
$scope.Identity = Identity;
@@ -50,6 +53,23 @@ module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Albu
$route.reload();
});
}
+
+ $scope.requestDeleteAccess = function (album) {
+ new Album(album).$delete({id: album.id}, function () {
+ // no-op
+ }, function () {
+ document.getElementById("output").innerHTML = 'Sent authorization request to resource owner, please, wait for approval.';
+ });
+ }
+
+ $scope.hasAccess = function (share, scope) {
+ for (i = 0; i < share.scopes.length; i++) {
+ if (share.scopes[i] == scope) {
+ return true;
+ }
+ }
+ return false;
+ }
});
module.controller('TokenCtrl', function ($scope, Identity) {
@@ -98,7 +118,9 @@ module.controller('AdminAlbumCtrl', function ($scope, $http, $route, $location,
});
module.factory('Album', ['$resource', function ($resource) {
- return $resource(apiUrl + '/album/:id');
+ return $resource(apiUrl + '/album/:id', {id: '@id'}, {
+ shares: {url: apiUrl + '/album/shares', method: 'GET', isArray: true}
+ });
}]);
module.factory('Profile', ['$resource', function ($resource) {
@@ -133,11 +155,46 @@ module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
}
if (rejection.config.url.indexOf('/authorize') == -1 && retry) {
- var deferred = $q.defer();
-
// here is the authorization logic, which tries to obtain an authorization token from the server in case the resource server
// returns a 403 or 401.
- Identity.authorization.authorize(rejection.headers('WWW-Authenticate')).then(function (rpt) {
+ var wwwAuthenticateHeader = rejection.headers('WWW-Authenticate');
+
+ // when using UMA, a WWW-Authenticate header should be returned by the resource server
+ if (!wwwAuthenticateHeader) {
+ return $q.reject(rejection);
+ }
+
+ // when using UMA, a WWW-Authenticate header should contain UMA data
+ if (wwwAuthenticateHeader.indexOf('UMA') == -1) {
+ return $q.reject(rejection);
+ }
+
+ var deferred = $q.defer();
+
+ var params = wwwAuthenticateHeader.split(',');
+ var ticket;
+
+ // try to extract the permission ticket from the WWW-Authenticate header
+ for (i = 0; i < params.length; i++) {
+ var param = params[i].split('=');
+
+ if (param[0] == 'ticket') {
+ ticket = param[1].substring(1, param[1].length - 1).trim();
+ break;
+ }
+ }
+
+ // a permission ticket must exist in order to send an authorization request
+ if (!ticket) {
+ return $q.reject(rejection);
+ }
+
+ // prepare a authorization request with the permission ticket
+ var authorizationRequest = {};
+ authorizationRequest.ticket = ticket;
+
+ // send the authorization request, if successful retry the request
+ Identity.authorization.authorize(authorizationRequest).then(function (rpt) {
deferred.resolve(rejection);
}, function () {
document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/identity.js b/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/identity.js
index 9a018e4747..4088f8075c 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/identity.js
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/identity.js
@@ -34,6 +34,10 @@
keycloak.logout();
};
+ this.account = function () {
+ keycloak.accountManagement();
+ }
+
this.hasRole = function (name) {
if (keycloak && keycloak.hasRealmRole(name)) {
return true;
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json b/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
index affafdd82c..d9354e380a 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "photoz",
- "auth-server-url" : "http://localhost:8080/auth",
+ "auth-server-url" : "http://localhost:8180/auth",
"ssl-required" : "external",
"resource" : "photoz-html5-client",
"public-client" : true
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
index 78c252a85c..fffcdeab9f 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
@@ -5,18 +5,18 @@
Create Album |
My Profile
-
+
Your Albums
You don't have any albums, yet.
+
Shared With Me
+
You don't have any shares, yet.
+
\ No newline at end of file
diff --git a/examples/authz/photoz/photoz-realm.json b/examples/authz/photoz/photoz-realm.json
index 118b9828f1..4a15c389a6 100644
--- a/examples/authz/photoz/photoz-realm.json
+++ b/examples/authz/photoz/photoz-realm.json
@@ -1,6 +1,7 @@
{
"realm": "photoz",
"enabled": true,
+ "userManagedAccessAllowed": "true",
"sslRequired": "external",
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
@@ -26,6 +27,9 @@
"clientRoles": {
"photoz-restful-api": [
"manage-albums"
+ ],
+ "account": [
+ "manage-account"
]
}
},
@@ -47,6 +51,9 @@
"clientRoles": {
"photoz-restful-api": [
"manage-albums"
+ ],
+ "account": [
+ "manage-account"
]
}
},
@@ -100,13 +107,13 @@
{
"clientId": "photoz-html5-client",
"enabled": true,
- "adminUrl": "/photoz-html5-client",
- "baseUrl": "/photoz-html5-client",
+ "adminUrl": "http://localhost:8080/photoz-html5-client",
+ "baseUrl": "http://localhost:8080/photoz-html5-client",
"publicClient": true,
"consentRequired" : true,
"fullScopeAllowed" : true,
"redirectUris": [
- "/photoz-html5-client/*"
+ "http://localhost:8080/photoz-html5-client/*"
],
"webOrigins": ["http://localhost:8080"]
},
@@ -114,10 +121,10 @@
"clientId": "photoz-restful-api",
"secret": "secret",
"enabled": true,
- "baseUrl": "/photoz-restful-api",
+ "baseUrl": "http://localhost:8080/photoz-restful-api",
"authorizationServicesEnabled" : true,
"redirectUris": [
- "/photoz-restful-api/*"
+ "http://localhost:8080/photoz-html5-client"
],
"webOrigins" : ["http://localhost:8080"]
}
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index 056ff05f79..b49ba90123 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -1,14 +1,11 @@
package org.keycloak.example.photoz.album;
-import org.keycloak.KeycloakSecurityContext;
-import org.keycloak.authorization.client.AuthzClient;
-import org.keycloak.authorization.client.ClientAuthorizationContext;
-import org.keycloak.authorization.client.representation.ResourceRepresentation;
-import org.keycloak.authorization.client.representation.ScopeRepresentation;
-import org.keycloak.authorization.client.resource.ProtectionResource;
-import org.keycloak.example.photoz.ErrorResponse;
-import org.keycloak.example.photoz.entity.Album;
-import org.keycloak.example.photoz.util.Transaction;
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
import javax.inject.Inject;
import javax.persistence.EntityManager;
@@ -24,18 +21,24 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
-import java.security.Principal;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.ClientAuthorizationContext;
+import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.representation.ScopeRepresentation;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.example.photoz.ErrorResponse;
+import org.keycloak.example.photoz.entity.Album;
+import org.keycloak.example.photoz.util.Transaction;
+import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
@Path("/album")
@Transaction
public class AlbumService {
- public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view";
- public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete";
+ public static final String SCOPE_ALBUM_VIEW = "album:view";
+ public static final String SCOPE_ALBUM_DELETE = "album:delete";
@Inject
private EntityManager entityManager;
@@ -60,9 +63,12 @@ public class AlbumService {
throw new ErrorResponse("Name [" + newAlbum.getName() + "] already taken. Choose another one.", Status.CONFLICT);
}
- this.entityManager.persist(newAlbum);
-
- createProtectedResource(newAlbum);
+ try {
+ this.entityManager.persist(newAlbum);
+ createProtectedResource(newAlbum);
+ } catch (Exception e) {
+ getAuthzClient().protection().resource().delete(newAlbum.getExternalId());
+ }
return Response.ok(newAlbum).build();
}
@@ -88,6 +94,29 @@ public class AlbumService {
return Response.ok(this.entityManager.createQuery("from Album where userId = :id").setParameter("id", request.getUserPrincipal().getName()).getResultList()).build();
}
+ @GET
+ @Path("/shares")
+ @Produces("application/json")
+ public Response findShares() {
+ List
permissions = getAuthzClient().protection().permission().find(null, null, null, getKeycloakSecurityContext().getToken().getSubject(), true, true, null, null);
+ Map shares = new HashMap<>();
+
+ for (PermissionTicketRepresentation permission : permissions) {
+ SharedAlbum share = shares.get(permission.getResource());
+
+ if (share == null) {
+ share = new SharedAlbum(Album.class.cast(entityManager.createQuery("from Album where externalId = :externalId").setParameter("externalId", permission.getResource()).getSingleResult()));
+ shares.put(permission.getResource(), share);
+ }
+
+ if (permission.getScope() != null) {
+ share.addScope(permission.getScopeName());
+ }
+ }
+
+ return Response.ok(shares.values()).build();
+ }
+
@GET
@Path("{id}")
@Produces("application/json")
@@ -111,8 +140,11 @@ public class AlbumService {
ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album");
albumResource.setOwner(album.getUserId());
+ albumResource.setOwnerManagedAccess(true);
- getAuthzClient().protection().resource().create(albumResource);
+ ResourceRepresentation response = getAuthzClient().protection().resource().create(albumResource);
+
+ album.setExternalId(response.getId());
} catch (Exception e) {
throw new RuntimeException("Could not register protected resource.", e);
}
@@ -123,13 +155,13 @@ public class AlbumService {
try {
ProtectionResource protection = getAuthzClient().protection();
- Set search = protection.resource().findByFilter("uri=" + uri);
+ List search = protection.resource().findByUri(uri);
if (search.isEmpty()) {
throw new RuntimeException("Could not find protected resource with URI [" + uri + "]");
}
- protection.resource().delete(search.iterator().next());
+ protection.resource().delete(search.get(0).getId());
} catch (Exception e) {
throw new RuntimeException("Could not search protected resource.", e);
}
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/SharedAlbum.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/SharedAlbum.java
new file mode 100644
index 0000000000..dfc5fb1281
--- /dev/null
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/SharedAlbum.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 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.example.photoz.album;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.keycloak.example.photoz.entity.Album;
+
+public class SharedAlbum {
+
+ private Album album;
+ private List scopes;
+
+ public SharedAlbum(Album album) {
+ this.album = album;
+ }
+
+ public Album getAlbum() {
+ return album;
+ }
+
+ public List getScopes() {
+ return scopes;
+ }
+
+ public void addScope(String scope) {
+ if (scopes == null) {
+ scopes = new ArrayList<>();
+ }
+ scopes.add(scope);
+ }
+}
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
index 990595e58a..d8dda5fe21 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
@@ -17,6 +17,9 @@
*/
package org.keycloak.example.photoz.entity;
+import java.util.ArrayList;
+import java.util.List;
+
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -43,6 +46,9 @@ public class Album {
@Column(nullable = false)
private String userId;
+ @Column
+ private String externalId;
+
public String getId() {
return this.id;
}
@@ -74,4 +80,12 @@ public class Album {
public String getUserId() {
return this.userId;
}
+
+ public void setExternalId(String externalId) {
+ this.externalId = externalId;
+ }
+
+ public String getExternalId() {
+ return externalId;
+ }
}
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json b/examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json
index 28b87bc579..d94ce40f59 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json
+++ b/examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json
@@ -2,13 +2,23 @@
"allowRemoteResourceManagement": true,
"policyEnforcementMode": "ENFORCING",
"resources": [
+ {
+ "name": "Admin Resources",
+ "uri": "/admin/*",
+ "type": "http://photoz.com/admin",
+ "scopes": [
+ {
+ "name": "admin:manage"
+ }
+ ]
+ },
{
"name": "User Profile Resource",
"uri": "/profile",
"type": "http://photoz.com/profile",
"scopes": [
{
- "name": "urn:photoz.com:scopes:profile:view"
+ "name": "profile:view"
}
]
},
@@ -18,28 +28,45 @@
"type": "http://photoz.com/album",
"scopes": [
{
- "name": "urn:photoz.com:scopes:album:view"
+ "name": "album:delete"
},
{
- "name": "urn:photoz.com:scopes:album:delete"
- },
- {
- "name": "urn:photoz.com:scopes:album:create"
- }
- ]
- },
- {
- "name": "Admin Resources",
- "uri": "/admin/*",
- "type": "http://photoz.com/admin",
- "scopes": [
- {
- "name": "urn:photoz.com:scopes:album:admin:manage"
+ "name": "album:view"
}
]
}
],
"policies": [
+ {
+ "name": "Only Owner and Administrators Policy",
+ "description": "Defines that only the resource owner and administrators can do something",
+ "type": "aggregate",
+ "logic": "POSITIVE",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]"
+ }
+ },
+ {
+ "name": "Administration Policy",
+ "description": "Defines that only administrators from a specific network address can do something.",
+ "type": "aggregate",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "applyPolicies": "[\"Any Admin Policy\",\"Only From a Specific Client Address\"]"
+ }
+ },
+ {
+ "name": "Only From @keycloak.org or Admin",
+ "description": "Defines that only users from @keycloak.org",
+ "type": "js",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRealmRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}"
+ }
+ },
{
"name": "Only Owner Policy",
"description": "Defines that only the resource owner is allowed to do something",
@@ -66,16 +93,6 @@
"roles": "[{\"id\":\"admin\",\"required\":true}]"
}
},
- {
- "name": "Any User Policy",
- "description": "Defines that only users from well known clients are allowed to access",
- "type": "role",
- "logic": "POSITIVE",
- "decisionStrategy": "UNANIMOUS",
- "config": {
- "roles": "[{\"id\":\"user\"},{\"id\":\"manage-albums\",\"required\":true}]"
- }
- },
{
"name": "Only From a Specific Client Address",
"description": "Defines that only clients from a specific address can do something",
@@ -87,45 +104,13 @@
}
},
{
- "name": "Administration Policy",
- "description": "Defines that only administrators from a specific network address can do something.",
- "type": "aggregate",
+ "name": "Any User Policy",
+ "description": "Defines that only users from well known clients are allowed to access",
+ "type": "role",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "applyPolicies": "[\"Only From a Specific Client Address\",\"Any Admin Policy\"]"
- }
- },
- {
- "name": "Only Owner and Administrators Policy",
- "description": "Defines that only the resource owner and administrators can do something",
- "type": "aggregate",
- "logic": "POSITIVE",
- "decisionStrategy": "AFFIRMATIVE",
- "config": {
- "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]"
- }
- },
- {
- "name": "Only From @keycloak.org or Admin",
- "description": "Defines that only users from @keycloak.org",
- "type": "js",
- "logic": "POSITIVE",
- "decisionStrategy": "UNANIMOUS",
- "config": {
- "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRealmRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}"
- }
- },
- {
- "name": "Album Resource Permission",
- "description": "General policies that apply to all album resources.",
- "type": "resource",
- "logic": "POSITIVE",
- "decisionStrategy": "AFFIRMATIVE",
- "config": {
- "defaultResourceType": "http://photoz.com/album",
- "default": "true",
- "applyPolicies": "[\"Any User Policy\",\"Administration Policy\"]"
+ "roles": "[{\"id\":\"user\",\"required\":false},{\"id\":\"photoz-restful-api/manage-albums\",\"required\":true}]"
}
},
{
@@ -136,8 +121,20 @@
"decisionStrategy": "UNANIMOUS",
"config": {
"defaultResourceType": "http://photoz.com/admin",
- "default": "true",
- "applyPolicies": "[\"Administration Policy\"]"
+ "applyPolicies": "[\"Administration Policy\"]",
+ "default": "true"
+ }
+ },
+ {
+ "name": "Album Resource Permission",
+ "description": "A default permission that defines access for any album resource",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Album Resource\"]",
+ "scopes": "[\"album:view\",\"album:delete\"]",
+ "applyPolicies": "[\"Only Owner and Administrators Policy\"]"
}
},
{
@@ -147,37 +144,9 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "applyPolicies": "[\"Only From @keycloak.org or Admin\"]",
- "scopes": "[\"urn:photoz.com:scopes:profile:view\"]"
+ "scopes": "[\"profile:view\"]",
+ "applyPolicies": "[\"Only From @keycloak.org or Admin\"]"
}
- },
- {
- "name": "Delete Album Permission",
- "description": "A policy that only allows the owner to delete his albums.",
- "type": "scope",
- "logic": "POSITIVE",
- "decisionStrategy": "UNANIMOUS",
- "config": {
- "applyPolicies": "[\"Only Owner and Administrators Policy\"]",
- "scopes": "[\"urn:photoz.com:scopes:album:delete\"]"
- }
- }
- ],
- "scopes": [
- {
- "name": "urn:photoz.com:scopes:profile:view"
- },
- {
- "name": "urn:photoz.com:scopes:album:view"
- },
- {
- "name": "urn:photoz.com:scopes:album:create"
- },
- {
- "name": "urn:photoz.com:scopes:album:delete"
- },
- {
- "name": "urn:photoz.com:scopes:album:admin:manage"
}
]
}
\ No newline at end of file
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
index 9e06730fe3..774845052b 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "photoz",
- "auth-server-url": "http://localhost:8080/auth",
+ "auth-server-url": "http://localhost:8180/auth",
"ssl-required": "external",
"resource": "photoz-restful-api",
"bearer-only" : true,
@@ -8,35 +8,28 @@
"secret": "secret"
},
"policy-enforcer": {
- "user-managed-access" : {},
+ "enforcement-mode": "PERMISSIVE",
+ "user-managed-access": {},
"paths": [
- {
- "path" : "/album/*",
- "methods" : [
- {
- "method": "POST",
- "scopes" : ["urn:photoz.com:scopes:album:create"]
- },
- {
- "method": "GET",
- "scopes" : ["urn:photoz.com:scopes:album:view"]
- }
- ]
- },
{
"name" : "Album Resource",
"path" : "/album/{id}",
"methods" : [
{
"method": "DELETE",
- "scopes" : ["urn:photoz.com:scopes:album:delete"]
+ "scopes" : ["album:delete"]
},
{
"method": "GET",
- "scopes" : ["urn:photoz.com:scopes:album:view"]
+ "scopes" : ["album:view"]
}
]
},
+ {
+ "name" : "Album Resource",
+ "path" : "/album/shares",
+ "enforcement-mode": "DISABLED"
+ },
{
"path" : "/profile"
},
diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json
index 7983fa39f1..d2834c3ce8 100644
--- a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "servlet-authz",
- "auth-server-url": "http://localhost:8080/auth",
+ "auth-server-url": "http://localhost:8180/auth",
"ssl-required": "external",
"resource": "servlet-authz-app",
"credentials": {
diff --git a/examples/authz/servlet-authz/src/main/webapp/index.jsp b/examples/authz/servlet-authz/src/main/webapp/index.jsp
index 3fbfca269c..345a69dffc 100755
--- a/examples/authz/servlet-authz/src/main/webapp/index.jsp
+++ b/examples/authz/servlet-authz/src/main/webapp/index.jsp
@@ -23,8 +23,8 @@
for (Permission permission : authzContext.getPermissions()) {
%>
- Resource: <%= permission.getResourceSetName() %>
- ID: <%= permission.getResourceSetId() %>
+ Resource: <%= permission.getResourceName() %>
+ ID: <%= permission.getResourceId() %>
Scopes: <%= permission.getScopes() %>
<%
diff --git a/examples/authz/servlet-authz/src/main/webapp/logout-include.jsp b/examples/authz/servlet-authz/src/main/webapp/logout-include.jsp
index 364d8877ab..21ef2edebf 100644
--- a/examples/authz/servlet-authz/src/main/webapp/logout-include.jsp
+++ b/examples/authz/servlet-authz/src/main/webapp/logout-include.jsp
@@ -7,5 +7,5 @@
String contextPath = request.getContextPath();
String redirectUri = scheme + "://" + host + ":" + port + contextPath;
%>
-
\ No newline at end of file
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index dd623775f7..7e6c3c7342 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -141,6 +141,18 @@ public class RealmAdapter implements CachedRealmModel {
updated.setEnabled(enabled);
}
+ @Override
+ public boolean isUserManagedAccessAllowed() {
+ if (isUpdated()) return updated.isEnabled();
+ return cached.isAllowUserManagedAccess();
+ }
+
+ @Override
+ public void setUserManagedAccessAllowed(boolean userManagedAccessAllowed) {
+ getDelegateForUpdate();
+ updated.setUserManagedAccessAllowed(userManagedAccessAllowed);
+ }
+
@Override
public SslRequired getSslRequired() {
if (isUpdated()) return updated.getSslRequired();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java
new file mode 100644
index 0000000000..d6a7e0758e
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 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.models.cache.infinispan.authorization;
+
+import org.keycloak.authorization.model.CachedModel;
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.models.cache.infinispan.authorization.entities.CachedPermissionTicket;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class PermissionTicketAdapter implements PermissionTicket, CachedModel {
+
+ protected CachedPermissionTicket cached;
+ protected StoreFactoryCacheSession cacheSession;
+ protected PermissionTicket updated;
+
+ public PermissionTicketAdapter(CachedPermissionTicket cached, StoreFactoryCacheSession cacheSession) {
+ this.cached = cached;
+ this.cacheSession = cacheSession;
+ }
+
+ @Override
+ public PermissionTicket getDelegateForUpdate() {
+ if (updated == null) {
+ cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getResourceId(), cached.getScopeId(), cached.getResourceServerId());
+ updated = cacheSession.getPermissionTicketStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
+ if (updated == null) throw new IllegalStateException("Not found in database");
+ }
+ return updated;
+ }
+
+ protected boolean invalidated;
+
+ protected void invalidateFlag() {
+ invalidated = true;
+ }
+
+ @Override
+ public void invalidate() {
+ invalidated = true;
+ getDelegateForUpdate();
+ }
+
+ @Override
+ public long getCacheTimestamp() {
+ return cached.getCacheTimestamp();
+ }
+
+ protected boolean isUpdated() {
+ if (updated != null) return true;
+ if (!invalidated) return false;
+ updated = cacheSession.getPermissionTicketStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
+ if (updated == null) throw new IllegalStateException("Not found in database");
+ return true;
+ }
+
+
+ @Override
+ public String getId() {
+ if (isUpdated()) return updated.getId();
+ return cached.getId();
+ }
+
+ @Override
+ public String getOwner() {
+ if (isUpdated()) return updated.getOwner();
+ return cached.getOwner();
+ }
+
+ @Override
+ public String getRequester() {
+ if (isUpdated()) return updated.getRequester();
+ return cached.getRequester();
+ }
+
+ @Override
+ public boolean isGranted() {
+ if (isUpdated()) return updated.isGranted();
+ return cached.isGranted();
+ }
+
+ @Override
+ public Long getCreatedTimestamp() {
+ if (isUpdated()) return updated.getCreatedTimestamp();
+ return cached.getCreatedTimestamp();
+ }
+
+ @Override
+ public Long getGrantedTimestamp() {
+ if (isUpdated()) return updated.getGrantedTimestamp();
+ return cached.getGrantedTimestamp();
+ }
+
+ @Override
+ public void setGrantedTimestamp(Long millis) {
+ getDelegateForUpdate();
+ cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getResourceId(), cached.getScopeId(), cached.getResourceServerId());
+ updated.setGrantedTimestamp(millis);
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return cacheSession.getResourceServerStore().findById(cached.getResourceServerId());
+ }
+
+ @Override
+ public Resource getResource() {
+ return cacheSession.getResourceStore().findById(cached.getResourceId(), getResourceServer().getId());
+ }
+
+ @Override
+ public Scope getScope() {
+ return cacheSession.getScopeStore().findById(cached.getScopeId(), getResourceServer().getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return getId().hashCode();
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
index 7660c96d1e..ae96113e94 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
@@ -25,6 +25,7 @@ import org.keycloak.models.cache.infinispan.authorization.entities.CachedPolicy;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.Logic;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
@@ -206,14 +207,15 @@ public class PolicyAdapter implements Policy, CachedModel {
@Override
public void addScope(Scope scope) {
getDelegateForUpdate();
+ cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), new HashSet<>(Arrays.asList(scope.getId())), cached.getResourceServerId());
updated.addScope(scope);
}
@Override
public void removeScope(Scope scope) {
getDelegateForUpdate();
+ cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), new HashSet<>(Arrays.asList(scope.getId())), cached.getResourceServerId());
updated.removeScope(scope);
-
}
@Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
index 38b68601aa..d310fcaeda 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
@@ -17,9 +17,11 @@
package org.keycloak.models.cache.infinispan.authorization;
import org.keycloak.authorization.model.CachedModel;
+import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PermissionTicketStore;
import org.keycloak.models.cache.infinispan.authorization.entities.CachedResource;
import java.util.Collections;
@@ -96,7 +98,19 @@ public class ResourceAdapter implements Resource, CachedModel {
getDelegateForUpdate();
cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
updated.setName(name);
+ }
+ @Override
+ public String getDisplayName() {
+ if (isUpdated()) return updated.getDisplayName();
+ return cached.getDisplayName();
+ }
+
+ @Override
+ public void setDisplayName(String name) {
+ getDelegateForUpdate();
+ cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+ updated.setDisplayName(name);
}
@Override
@@ -165,8 +179,33 @@ public class ResourceAdapter implements Resource, CachedModel {
}
@Override
- public void updateScopes(Set scopes) {
+ public boolean isOwnerManagedAccess() {
+ if (isUpdated()) return updated.isOwnerManagedAccess();
+ return cached.isOwnerManagedAccess();
+ }
+
+ @Override
+ public void setOwnerManagedAccess(boolean ownerManagedAccess) {
getDelegateForUpdate();
+ cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+ updated.setOwnerManagedAccess(ownerManagedAccess);
+ }
+
+ @Override
+ public void updateScopes(Set scopes) {
+ Resource updated = getDelegateForUpdate();
+
+ for (Scope scope : updated.getScopes()) {
+ if (!scopes.contains(scope)) {
+ PermissionTicketStore permissionStore = cacheSession.getPermissionTicketStoreDelegate();
+ List permissions = permissionStore.findByScope(scope.getId(), getResourceServer().getId());
+
+ for (PermissionTicket permission : permissions) {
+ permissionStore.delete(permission.getId());
+ }
+ }
+ }
+
cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), scopes.stream().map(scope1 -> scope1.getId()).collect(Collectors.toSet()), cached.getResourceServerId(), cached.getOwner());
updated.updateScopes(scopes);
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java
index d90b27a19f..a4492a7b71 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java
@@ -91,6 +91,18 @@ public class ScopeAdapter implements Scope, CachedModel {
}
+ @Override
+ public String getDisplayName() {
+ if (isUpdated()) return updated.getDisplayName();
+ return cached.getDisplayName();
+ }
+
+ @Override
+ public void setDisplayName(String name) {
+ getDelegateForUpdate();
+ updated.setDisplayName(name);
+ }
+
@Override
public String getIconUri() {
if (isUpdated()) return updated.getIconUri();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
index 3f189a5a40..7b463bec3d 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
@@ -68,6 +68,7 @@ public class StoreFactoryCacheManager extends CacheManager {
invalidations.add(id);
invalidations.add(StoreFactoryCacheSession.getScopeByNameCacheKey(name, serverId));
invalidations.add(StoreFactoryCacheSession.getResourceByScopeCacheKey(id, serverId));
+ invalidations.add(StoreFactoryCacheSession.getPermissionTicketByScope(id, serverId));
}
public void scopeRemoval(String id, String name, String serverId, Set