removal
This commit is contained in:
commit
52e40922bc
214 changed files with 5310 additions and 1970 deletions
|
@ -78,13 +78,13 @@ public abstract class AbstractPolicyEnforcer {
|
||||||
|
|
||||||
if (pathConfig == null) {
|
if (pathConfig == null) {
|
||||||
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
||||||
return createAuthorizationContext(accessToken);
|
return createAuthorizationContext(accessToken, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.debugf("Could not find a configuration for path [%s]", path);
|
LOGGER.debugf("Could not find a configuration for path [%s]", path);
|
||||||
|
|
||||||
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
||||||
return createAuthorizationContext(accessToken);
|
return createAuthorizationContext(accessToken, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAccessDenied(httpFacade);
|
handleAccessDenied(httpFacade);
|
||||||
|
@ -100,7 +100,7 @@ public abstract class AbstractPolicyEnforcer {
|
||||||
|
|
||||||
if (isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
|
if (isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
|
||||||
try {
|
try {
|
||||||
return createAuthorizationContext(accessToken);
|
return createAuthorizationContext(accessToken, pathConfig);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
|
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
|
||||||
}
|
}
|
||||||
|
@ -252,8 +252,8 @@ public abstract class AbstractPolicyEnforcer {
|
||||||
return requiredScopes;
|
return requiredScopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthorizationContext createAuthorizationContext(AccessToken accessToken) {
|
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
|
||||||
return new ClientAuthorizationContext(accessToken, this.paths, authzClient);
|
return new ClientAuthorizationContext(accessToken, pathConfig, this.paths, authzClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||||
|
|
|
@ -294,6 +294,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
private String logoutPage;
|
private String logoutPage;
|
||||||
private SignatureAlgorithm signatureAlgorithm;
|
private SignatureAlgorithm signatureAlgorithm;
|
||||||
private String signatureCanonicalizationMethod;
|
private String signatureCanonicalizationMethod;
|
||||||
|
private boolean autodetectBearerOnly;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean turnOffChangeSessionIdOnLogin() {
|
public boolean turnOffChangeSessionIdOnLogin() {
|
||||||
|
@ -439,4 +440,13 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||||
public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
|
public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
|
||||||
this.signatureAlgorithm = signatureAlgorithm;
|
this.signatureAlgorithm = signatureAlgorithm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAutodetectBearerOnly() {
|
||||||
|
return autodetectBearerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
|
||||||
|
this.autodetectBearerOnly = autodetectBearerOnly;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,6 +168,6 @@ public interface SamlDeployment {
|
||||||
}
|
}
|
||||||
PrincipalNamePolicy getPrincipalNamePolicy();
|
PrincipalNamePolicy getPrincipalNamePolicy();
|
||||||
String getPrincipalAttributeName();
|
String getPrincipalAttributeName();
|
||||||
|
boolean isAutodetectBearerOnly();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ public class SP implements Serializable {
|
||||||
private PrincipalNameMapping principalNameMapping;
|
private PrincipalNameMapping principalNameMapping;
|
||||||
private Set<String> roleAttributes;
|
private Set<String> roleAttributes;
|
||||||
private IDP idp;
|
private IDP idp;
|
||||||
|
private boolean autodetectBearerOnly;
|
||||||
|
|
||||||
public String getEntityID() {
|
public String getEntityID() {
|
||||||
return entityID;
|
return entityID;
|
||||||
|
@ -147,4 +148,11 @@ public class SP implements Serializable {
|
||||||
this.logoutPage = logoutPage;
|
this.logoutPage = logoutPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAutodetectBearerOnly() {
|
||||||
|
return autodetectBearerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
|
||||||
|
this.autodetectBearerOnly = autodetectBearerOnly;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ public class ConfigXmlConstants {
|
||||||
public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
|
public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
|
||||||
public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
|
public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
|
||||||
public static final String LOGOUT_PAGE_ATTR = "logoutPage";
|
public static final String LOGOUT_PAGE_ATTR = "logoutPage";
|
||||||
|
public static final String AUTODETECT_BEARER_ONLY_ATTR = "autodetectBearerOnly";
|
||||||
|
|
||||||
public static final String KEYS_ELEMENT = "Keys";
|
public static final String KEYS_ELEMENT = "Keys";
|
||||||
public static final String KEY_ELEMENT = "Key";
|
public static final String KEY_ELEMENT = "Key";
|
||||||
|
|
|
@ -68,6 +68,7 @@ public class DeploymentBuilder {
|
||||||
deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
|
deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
|
||||||
deployment.setLogoutPage(sp.getLogoutPage());
|
deployment.setLogoutPage(sp.getLogoutPage());
|
||||||
deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());
|
deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());
|
||||||
|
deployment.setAutodetectBearerOnly(sp.isAutodetectBearerOnly());
|
||||||
deployment.setSignatureAlgorithm(SignatureAlgorithm.RSA_SHA256);
|
deployment.setSignatureAlgorithm(SignatureAlgorithm.RSA_SHA256);
|
||||||
if (sp.getIdp().getSignatureAlgorithm() != null) {
|
if (sp.getIdp().getSignatureAlgorithm() != null) {
|
||||||
deployment.setSignatureAlgorithm(SignatureAlgorithm.valueOf(sp.getIdp().getSignatureAlgorithm()));
|
deployment.setSignatureAlgorithm(SignatureAlgorithm.valueOf(sp.getIdp().getSignatureAlgorithm()));
|
||||||
|
|
|
@ -89,6 +89,7 @@ public class SPXmlParser extends AbstractParser {
|
||||||
sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
|
sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
|
||||||
sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
|
sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
|
||||||
sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR));
|
sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR));
|
||||||
|
sp.setAutodetectBearerOnly(getBooleanAttributeValue(startElement, ConfigXmlConstants.AUTODETECT_BEARER_ONLY_ATTR));
|
||||||
sp.setTurnOffChangeSessionIdOnLogin(getBooleanAttributeValue(startElement, ConfigXmlConstants.TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN_ATTR));
|
sp.setTurnOffChangeSessionIdOnLogin(getBooleanAttributeValue(startElement, ConfigXmlConstants.TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN_ATTR));
|
||||||
while (xmlEventReader.hasNext()) {
|
while (xmlEventReader.hasNext()) {
|
||||||
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
||||||
|
|
|
@ -568,9 +568,15 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||||
return new AbstractInitiateLogin(deployment, sessionStore) {
|
return new AbstractInitiateLogin(deployment, sessionStore) {
|
||||||
@Override
|
@Override
|
||||||
protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException {
|
protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException {
|
||||||
Document document = authnRequestBuilder.toDocument();
|
if (isAutodetectedBearerOnly(httpFacade.getRequest())) {
|
||||||
SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
|
httpFacade.getResponse().setStatus(401);
|
||||||
SamlUtil.sendSaml(true, httpFacade, deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(), binding, document, samlBinding);
|
httpFacade.getResponse().end();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Document document = authnRequestBuilder.toDocument();
|
||||||
|
SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
|
||||||
|
SamlUtil.sendSaml(true, httpFacade, deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(), binding, document, samlBinding);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -693,4 +699,34 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||||
|
|
||||||
return signature.verify(decodedSignature);
|
return signature.verify(decodedSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isAutodetectedBearerOnly(HttpFacade.Request request) {
|
||||||
|
if (!deployment.isAutodetectBearerOnly()) return false;
|
||||||
|
|
||||||
|
String headerValue = facade.getRequest().getHeader(GeneralConstants.HTTP_HEADER_X_REQUESTED_WITH);
|
||||||
|
if (headerValue != null && headerValue.equalsIgnoreCase("XMLHttpRequest")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerValue = facade.getRequest().getHeader("Faces-Request");
|
||||||
|
if (headerValue != null && headerValue.startsWith("partial/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerValue = facade.getRequest().getHeader("SOAPAction");
|
||||||
|
if (headerValue != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> accepts = facade.getRequest().getHeaders("Accept");
|
||||||
|
if (accepts == null) accepts = Collections.emptyList();
|
||||||
|
|
||||||
|
for (String accept : accepts) {
|
||||||
|
if (accept.contains("text/html") || accept.contains("text/*") || accept.contains("*/*")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,461 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<xs:schema version="1.0"
|
||||||
|
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||||
|
xmlns="urn:keycloak:saml:adapter"
|
||||||
|
targetNamespace="urn:keycloak:saml:adapter"
|
||||||
|
elementFormDefault="qualified"
|
||||||
|
attributeFormDefault="unqualified">
|
||||||
|
|
||||||
|
<xs:element name="keycloak-saml-adapter" type="adapter-type"/>
|
||||||
|
<xs:complexType name="adapter-type">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Keycloak SAML Adapter configuration file.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="SP" maxOccurs="1" minOccurs="0" type="sp-type">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Describes SAML service provider configuration.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="sp-type">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
List of service provider encryption and validation keys.
|
||||||
|
|
||||||
|
If the IDP requires that the client application (SP) sign all of its requests and/or if the IDP will encrypt assertions, you must define the keys used to do this. For client signed documents you must define both the private and public key or certificate that will be used to sign documents. For encryption, you only have to define the private key that will be used to decrypt.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="PrincipalNameMapping" type="principal-name-mapping-type" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>When creating a Java Principal object that you obtain from methods like HttpServletRequest.getUserPrincipal(), you can define what name that is returned by the Principal.getName() method.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="RoleIdentifiers" type="role-identifiers-type" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Defines what SAML attributes within the assertion received from the user should be used as role identifiers within the Java EE Security Context for the user.
|
||||||
|
By default Role attribute values are converted to Java EE roles. Some IDPs send roles via a member or memberOf attribute assertion. You can define one or more Attribute elements to specify which SAML attributes must be converted into roles.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="IDP" type="idp-type" minOccurs="1" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Describes configuration of SAML identity provider for this service provider.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="entityID" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This is the identifier for this client. The IDP needs this value to determine who the client is that is communicating with it.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="sslPolicy" type="ssl-policy-type" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>SSL policy the adapter will enforce.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>SAML clients can request a specific NameID Subject format. Fill in this value if you want a specific format. It must be a standard SAML format identifier, i.e. urn:oasis:names:tc:SAML:2.0:nameid-format:transient. By default, no special format is requested.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="logoutPage" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>URL of the logout page.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>SAML clients can request that a user is re-authenticated even if they are already logged in at the IDP. Default value is false.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="isPassive" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>SAML clients can request that a user is never asked to authenticate even if they are not logged in at the IDP. Set this to true if you want this. Do not use together with forceAuthentication as they are opposite. Default value is false.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The session id is changed by default on a successful login on some platforms to plug a security attack vector. Change this to true to disable this. It is recommended you do not turn it off. Default value is false.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="autodetectBearerOnly" type="xs:boolean" use="optional" default="false">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This should be set to true if your application serves both a web application and web services (e.g. SOAP or REST). It allows you to redirect unauthenticated users of the web application to the Keycloak login page, but send an HTTP 401 status code to unauthenticated SOAP or REST clients instead as they would not understand a redirect to the login page. Keycloak auto-detects SOAP or REST clients based on typical headers like X-Requested-With, SOAPAction or Accept. The default value is false.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="keys-type">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="Key" type="key-type" minOccurs="1" maxOccurs="unbounded">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Describes a single key used for signing or encryption.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="key-type">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="KeyStore" maxOccurs="1" minOccurs="0" type="key-store-type">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Java keystore to load keys and certificates from.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="PrivateKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Private key (PEM format)</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="PublicKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Public key (PEM format)</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="CertificatePem" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Certificate key (PEM format)</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="signing" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Flag defining whether the key should be used for signing.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="encryption" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Flag defining whether the key should be used for encryption</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="key-store-type">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="PrivateKey" maxOccurs="1" minOccurs="0" type="private-key-type">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Private key declaration</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="Certificate" type="certificate-type" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Certificate declaration</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="file" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>File path to the key store.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="resource" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>WAR resource path to the key store. This is a path used in method call to ServletContext.getResourceAsStream().</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="password" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The password of the key store.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="private-key-type">
|
||||||
|
<xs:attribute name="alias" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="password" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Keystores require an additional password to access private keys. In the PrivateKey element you must define this password within a password attribute.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="certificate-type">
|
||||||
|
<xs:attribute name="alias" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="principal-name-mapping-type">
|
||||||
|
<xs:attribute name="policy" type="principal-name-mapping-policy-type" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Policy used to populate value of Java Principal object obtained from methods like HttpServletRequest.getUserPrincipal().</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="attribute" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Name of the SAML assertion attribute to use within.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:simpleType name="principal-name-mapping-policy-type">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="FROM_NAME_ID">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This policy just uses whatever the SAML subject value is. This is the default setting</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:enumeration>
|
||||||
|
<xs:enumeration value="FROM_ATTRIBUTE">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This will pull the value from one of the attributes declared in the SAML assertion received from the server. You'll need to specify the name of the SAML assertion attribute to use within the attribute XML attribute.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:enumeration>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="ssl-policy-type">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="ALL">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>All requests must come in via HTTPS.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:enumeration>
|
||||||
|
<xs:enumeration value="EXTERNAL">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Only non-private IP addresses must come over the wire via HTTPS.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:enumeration>
|
||||||
|
<xs:enumeration value="NONE">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>no requests are required to come over via HTTPS.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:enumeration>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="signature-algorithm-type">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="RSA_SHA1"/>
|
||||||
|
<xs:enumeration value="RSA_SHA256"/>
|
||||||
|
<xs:enumeration value="RSA_SHA512"/>
|
||||||
|
<xs:enumeration value="DSA_SHA1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="binding-type">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="POST"/>
|
||||||
|
<xs:enumeration value="REDIRECT"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:complexType name="role-identifiers-type">
|
||||||
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element name="Attribute" maxOccurs="unbounded" minOccurs="0" type="attribute-type">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Specifies SAML attribute to be converted into roles.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="attribute-type">
|
||||||
|
<xs:attribute name="name" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Specifies name of the SAML attribute to be converted into roles.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="idp-type">
|
||||||
|
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element name="SingleSignOnService" maxOccurs="1" minOccurs="1" type="sign-on-type">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Configuration of the login SAML endpoint of the IDP.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="SingleLogoutService" type="logout-type" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Configuration of the logout SAML endpoint of the IDP</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The Keys sub element of IDP is only used to define the certificate or public key to use to verify documents signed by the IDP.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="HttpClient" type="http-client-type" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Configuration of HTTP client used for automatic obtaining of certificates containing public keys for IDP signature verification via SAML descriptor of the IDP.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="entityID" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>issuer ID of the IDP.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="signaturesRequired" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>If set to true, the client adapter will sign every document it sends to the IDP. Also, the client will expect that the IDP will be signing any documents sent to it. This switch sets the default for all request and response types.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="signatureAlgorithm" type="signature-algorithm-type" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Signature algorithm that the IDP expects signed documents to use. Defaults to RSA_SHA256</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This is the signature canonicalization method that the IDP expects signed documents to use. The default value is https://www.w3.org/2001/10/xml-exc-c14n# and should be good for most IDPs.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="encryption" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation></xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="sign-on-type">
|
||||||
|
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Should the client expect the IDP to sign the assertion response document sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Should the client expect the IDP to sign the individual assertions sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="requestBinding" type="binding-type" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>SAML binding type used for communicating with the IDP. The default value is POST, but you can set it to REDIRECT as well.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="responseBinding" type="binding-type" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>SAML allows the client to request what binding type it wants authn responses to use. This value maps to ProtocolBinding attribute in SAML AuthnRequest. The default is that the client will not request a specific binding type for responses.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="bindingUrl" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This is the URL for the IDP login service that the client will send requests to.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="assertionConsumerServiceUrl" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>URL of the assertion consumer service (ACS) where the IDP login service should send responses to. By default it is unset, relying on the IdP settings. When set, it must end in "/saml". This property is typically accompanied by the responseBinding attribute.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="logout-type">
|
||||||
|
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="signResponse" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Should the client sign logout responses it sends to the IDP requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Should the client expect signed logout request documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Should the client expect signed logout response documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="requestBinding" type="binding-type" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This is the SAML binding type used for communicating SAML requests to the IDP. The default value is POST.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="responseBinding" type="binding-type" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This is the SAML binding type used for communicating SAML responses to the IDP. The default value is POST.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="postBindingUrl" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This is the URL for the IDP's logout service when using the POST binding. This setting is REQUIRED if using the POST binding.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This is the URL for the IDP's logout service when using the REDIRECT binding. This setting is REQUIRED if using the REDIRECT binding.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="http-client-type">
|
||||||
|
<xs:attribute name="allowAnyHostname" type="xs:boolean" use="optional" default="false">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>If the the IDP server requires HTTPS and this config option is set to true the IDP's certificate
|
||||||
|
is validated via the truststore, but host name validation is not done. This setting should only be used during
|
||||||
|
development and never in production as it will partly disable verification of SSL certificates.
|
||||||
|
This seting may be useful in test environments. The default value is false.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="clientKeystore" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This is the file path to a keystore file. This keystore contains client certificate
|
||||||
|
for two-way SSL when the adapter makes HTTPS requests to the IDP server.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="clientKeystorePassword" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Password for the client keystore and for the client's key.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="connectionPoolSize" type="xs:int" use="optional" default="10">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Defines number of pooled connections.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="disableTrustManager" type="xs:boolean" use="optional" default="false">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>If the the IDP server requires HTTPS and this config option is set to true you do not have to specify a truststore.
|
||||||
|
This setting should only be used during development and never in production as it will disable verification of SSL certificates.
|
||||||
|
The default value is false.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="proxyUrl" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>URL to HTTP proxy to use for HTTP connections.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="truststore" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The value is the file path to a keystore file. If you prefix the path with classpath:,
|
||||||
|
then the truststore will be obtained from the deployment's classpath instead. Used for outgoing
|
||||||
|
HTTPS communications to the IDP server. Client making HTTPS requests need
|
||||||
|
a way to verify the host of the server they are talking to. This is what the trustore does.
|
||||||
|
The keystore contains one or more trusted host certificates or certificate authorities.
|
||||||
|
You can create this truststore by extracting the public certificate of the IDP's SSL keystore.
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="truststorePassword" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Password for the truststore keystore.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
</xs:schema>
|
|
@ -37,7 +37,7 @@ import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
*/
|
*/
|
||||||
public class KeycloakSamlAdapterXMLParserTest {
|
public class KeycloakSamlAdapterXMLParserTest {
|
||||||
|
|
||||||
private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_7.xsd";
|
private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_9.xsd";
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public ExpectedException expectedException = ExpectedException.none();
|
public ExpectedException expectedException = ExpectedException.none();
|
||||||
|
@ -91,6 +91,7 @@ public class KeycloakSamlAdapterXMLParserTest {
|
||||||
assertEquals("format", sp.getNameIDPolicyFormat());
|
assertEquals("format", sp.getNameIDPolicyFormat());
|
||||||
assertTrue(sp.isForceAuthentication());
|
assertTrue(sp.isForceAuthentication());
|
||||||
assertTrue(sp.isIsPassive());
|
assertTrue(sp.isIsPassive());
|
||||||
|
assertFalse(sp.isAutodetectBearerOnly());
|
||||||
assertEquals(2, sp.getKeys().size());
|
assertEquals(2, sp.getKeys().size());
|
||||||
Key signing = sp.getKeys().get(0);
|
Key signing = sp.getKeys().get(0);
|
||||||
assertTrue(signing.isSigning());
|
assertTrue(signing.isSigning());
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
|
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_7.xsd">
|
xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_9.xsd">
|
||||||
<SP entityID="sp"
|
<SP entityID="sp"
|
||||||
sslPolicy="EXTERNAL"
|
sslPolicy="EXTERNAL"
|
||||||
nameIDPolicyFormat="format"
|
nameIDPolicyFormat="format"
|
||||||
|
@ -73,4 +73,4 @@
|
||||||
</Keys>
|
</Keys>
|
||||||
</IDP>
|
</IDP>
|
||||||
</SP>
|
</SP>
|
||||||
</keycloak-saml-adapter>
|
</keycloak-saml-adapter>
|
||||||
|
|
|
@ -30,8 +30,8 @@ public class ClientAuthorizationContext extends AuthorizationContext {
|
||||||
|
|
||||||
private final AuthzClient client;
|
private final AuthzClient client;
|
||||||
|
|
||||||
public ClientAuthorizationContext(AccessToken authzToken, Map<String, PolicyEnforcerConfig.PathConfig> paths, AuthzClient client) {
|
public ClientAuthorizationContext(AccessToken authzToken, PolicyEnforcerConfig.PathConfig current, Map<String, PolicyEnforcerConfig.PathConfig> paths, AuthzClient client) {
|
||||||
super(authzToken, paths);
|
super(authzToken, current, paths);
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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.authorization.client.representation;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class AuthorizationRequestMetadata {
|
||||||
|
|
||||||
|
public static final String INCLUDE_RESOURCE_NAME = "include_resource_name";
|
||||||
|
|
||||||
|
@JsonProperty(INCLUDE_RESOURCE_NAME)
|
||||||
|
private boolean includeResourceName = true;
|
||||||
|
|
||||||
|
private int limit;
|
||||||
|
|
||||||
|
public boolean isIncludeResourceName() {
|
||||||
|
return includeResourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIncludeResourceName(boolean includeResourceName) {
|
||||||
|
this.includeResourceName = includeResourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLimit(int limit) {
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLimit() {
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,31 +4,81 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <p>An {@code {@link EntitlementRequest} represents a request sent to the server containing the permissions being requested.
|
||||||
|
*
|
||||||
|
* <p>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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
public class EntitlementRequest {
|
public class EntitlementRequest {
|
||||||
|
|
||||||
private String rpt;
|
private String rpt;
|
||||||
|
private AuthorizationRequestMetadata metadata;
|
||||||
|
|
||||||
private List<PermissionRequest> permissions = new ArrayList<>();
|
private List<PermissionRequest> permissions = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the permissions being requested.
|
||||||
|
*
|
||||||
|
* @return the permissions being requested (not {@code null})
|
||||||
|
*/
|
||||||
public List<PermissionRequest> getPermissions() {
|
public List<PermissionRequest> getPermissions() {
|
||||||
return permissions;
|
return permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRpt() {
|
/**
|
||||||
return rpt;
|
* Set the permissions being requested
|
||||||
}
|
*
|
||||||
|
* @param permissions the permissions being requests (not {@code null})
|
||||||
public void setRpt(String rpt) {
|
*/
|
||||||
this.rpt = rpt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPermissions(List<PermissionRequest> permissions) {
|
public void setPermissions(List<PermissionRequest> permissions) {
|
||||||
this.permissions = 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) {
|
public void addPermission(PermissionRequest request) {
|
||||||
getPermissions().add(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,25 @@ public class PermissionRequest {
|
||||||
|
|
||||||
private Set<String> scopes;
|
private Set<String> scopes;
|
||||||
|
|
||||||
|
public PermissionRequest() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionRequest(String resourceSetId, String resourceSetName, Set<String> scopes) {
|
||||||
|
this.resourceSetId = resourceSetId;
|
||||||
|
this.resourceSetName = resourceSetName;
|
||||||
|
this.scopes = scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionRequest(String resourceSetName) {
|
||||||
|
this.resourceSetName = resourceSetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionRequest(String resourceSetName, Set<String> scopes) {
|
||||||
|
this.resourceSetName = resourceSetName;
|
||||||
|
this.scopes = scopes;
|
||||||
|
}
|
||||||
|
|
||||||
public String getResourceSetId() {
|
public String getResourceSetId() {
|
||||||
return this.resourceSetId;
|
return this.resourceSetId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package org.keycloak.authorization.client.resource;
|
package org.keycloak.authorization.client.resource;
|
||||||
|
|
||||||
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||||
|
import org.keycloak.authorization.client.representation.AuthorizationRequestMetadata;
|
||||||
import org.keycloak.authorization.client.representation.EntitlementRequest;
|
import org.keycloak.authorization.client.representation.EntitlementRequest;
|
||||||
import org.keycloak.authorization.client.representation.EntitlementResponse;
|
import org.keycloak.authorization.client.representation.EntitlementResponse;
|
||||||
import org.keycloak.authorization.client.util.Http;
|
import org.keycloak.authorization.client.util.Http;
|
||||||
|
import org.keycloak.authorization.client.util.HttpMethod;
|
||||||
import org.keycloak.authorization.client.util.HttpResponseException;
|
import org.keycloak.authorization.client.util.HttpResponseException;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
@ -23,9 +25,8 @@ public class EntitlementResource {
|
||||||
public EntitlementResponse getAll(String resourceServerId) {
|
public EntitlementResponse getAll(String resourceServerId) {
|
||||||
try {
|
try {
|
||||||
return this.http.<EntitlementResponse>get("/authz/entitlement/" + resourceServerId)
|
return this.http.<EntitlementResponse>get("/authz/entitlement/" + resourceServerId)
|
||||||
.authorizationBearer(this.eat)
|
.authorizationBearer(eat)
|
||||||
.response()
|
.response().json(EntitlementResponse.class).execute();
|
||||||
.json(EntitlementResponse.class).execute();
|
|
||||||
} catch (HttpResponseException e) {
|
} catch (HttpResponseException e) {
|
||||||
if (403 == e.getStatusCode()) {
|
if (403 == e.getStatusCode()) {
|
||||||
throw new AuthorizationDeniedException(e);
|
throw new AuthorizationDeniedException(e);
|
||||||
|
@ -39,7 +40,7 @@ public class EntitlementResource {
|
||||||
public EntitlementResponse get(String resourceServerId, EntitlementRequest request) {
|
public EntitlementResponse get(String resourceServerId, EntitlementRequest request) {
|
||||||
try {
|
try {
|
||||||
return this.http.<EntitlementResponse>post("/authz/entitlement/" + resourceServerId)
|
return this.http.<EntitlementResponse>post("/authz/entitlement/" + resourceServerId)
|
||||||
.authorizationBearer(this.eat)
|
.authorizationBearer(eat)
|
||||||
.json(JsonSerialization.writeValueAsBytes(request))
|
.json(JsonSerialization.writeValueAsBytes(request))
|
||||||
.response().json(EntitlementResponse.class).execute();
|
.response().json(EntitlementResponse.class).execute();
|
||||||
} catch (HttpResponseException e) {
|
} catch (HttpResponseException e) {
|
||||||
|
|
|
@ -3,10 +3,12 @@ package org.keycloak.authorization.policy.provider.client;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authorization.AuthorizationProvider;
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
|
@ -71,6 +73,21 @@ public class ClientPolicyProviderFactory implements PolicyProviderFactory<Client
|
||||||
updateClients(policy, new HashSet<>(Arrays.asList(getClients(policy))), authorization);
|
updateClients(policy, new HashSet<>(Arrays.asList(getClients(policy))), authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
|
||||||
|
ClientPolicyRepresentation userRep = toRepresentation(policy, new ClientPolicyRepresentation());
|
||||||
|
Map<String, String> config = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
RealmModel realm = authorizationProvider.getRealm();
|
||||||
|
config.put("clients", JsonSerialization.writeValueAsString(userRep.getClients().stream().map(id -> realm.getClientById(id).getClientId()).collect(Collectors.toList())));
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to export user policy [" + policy.getName() + "]", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
representation.setConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PolicyProvider create(KeycloakSession session) {
|
public PolicyProvider create(KeycloakSession session) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -129,7 +146,7 @@ public class ClientPolicyProviderFactory implements PolicyProviderFactory<Client
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateClients(Policy policy, Set<String> clients, AuthorizationProvider authorization) {
|
private void updateClients(Policy policy, Set<String> clients, AuthorizationProvider authorization) {
|
||||||
RealmModel realm = authorization.getKeycloakSession().getContext().getRealm();
|
RealmModel realm = authorization.getRealm();
|
||||||
|
|
||||||
if (clients == null || clients.isEmpty()) {
|
if (clients == null || clients.isEmpty()) {
|
||||||
throw new RuntimeException("No client provided.");
|
throw new RuntimeException("No client provided.");
|
||||||
|
|
|
@ -108,6 +108,30 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory<RolePoli
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
|
||||||
|
Map<String, String> config = new HashMap<>();
|
||||||
|
Set<RolePolicyRepresentation.RoleDefinition> roles = toRepresentation(policy, new RolePolicyRepresentation()).getRoles();
|
||||||
|
|
||||||
|
for (RolePolicyRepresentation.RoleDefinition roleDefinition : roles) {
|
||||||
|
RoleModel role = authorizationProvider.getRealm().getRoleById(roleDefinition.getId());
|
||||||
|
|
||||||
|
if (role.isClientRole()) {
|
||||||
|
roleDefinition.setId(ClientModel.class.cast(role.getContainer()).getClientId() + "/" + role.getName());
|
||||||
|
} else {
|
||||||
|
roleDefinition.setId(role.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config.put("roles", JsonSerialization.writeValueAsString(roles));
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to export role policy [" + policy.getName() + "]", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
representation.setConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateRoles(Policy policy, RolePolicyRepresentation representation, AuthorizationProvider authorization) {
|
private void updateRoles(Policy policy, RolePolicyRepresentation representation, AuthorizationProvider authorization) {
|
||||||
updateRoles(policy, authorization, representation.getRoles());
|
updateRoles(policy, authorization, representation.getRoles());
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,13 @@ package org.keycloak.authorization.policy.provider.user;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authorization.AuthorizationProvider;
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
|
@ -106,6 +108,23 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory<UserPoli
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
|
||||||
|
UserPolicyRepresentation userRep = toRepresentation(policy, new UserPolicyRepresentation());
|
||||||
|
Map<String, String> config = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
UserProvider userProvider = authorizationProvider.getKeycloakSession().users();
|
||||||
|
RealmModel realm = authorizationProvider.getRealm();
|
||||||
|
|
||||||
|
config.put("users", JsonSerialization.writeValueAsString(userRep.getUsers().stream().map(id -> userProvider.getUserById(id, realm).getUsername()).collect(Collectors.toList())));
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to export user policy [" + policy.getName() + "]", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
representation.setConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateUsers(Policy policy, UserPolicyRepresentation representation, AuthorizationProvider authorization) {
|
private void updateUsers(Policy policy, UserPolicyRepresentation representation, AuthorizationProvider authorization) {
|
||||||
updateUsers(policy, authorization, representation.getUsers());
|
updateUsers(policy, authorization, representation.getUsers());
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.common.util;
|
package org.keycloak.common.util;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,9 +26,21 @@ import java.util.Properties;
|
||||||
*/
|
*/
|
||||||
public class SystemEnvProperties extends Properties {
|
public class SystemEnvProperties extends Properties {
|
||||||
|
|
||||||
|
private final Map<String, String> overrides;
|
||||||
|
|
||||||
|
public SystemEnvProperties(Map<String, String> overrides) {
|
||||||
|
this.overrides = overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SystemEnvProperties() {
|
||||||
|
this.overrides = Collections.EMPTY_MAP;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProperty(String key) {
|
public String getProperty(String key) {
|
||||||
if (key.startsWith("env.")) {
|
if (overrides.containsKey(key)) {
|
||||||
|
return overrides.get(key);
|
||||||
|
} else if (key.startsWith("env.")) {
|
||||||
return System.getenv().get(key.substring(4));
|
return System.getenv().get(key.substring(4));
|
||||||
} else {
|
} else {
|
||||||
return System.getProperty(key);
|
return System.getProperty(key);
|
||||||
|
|
|
@ -32,17 +32,19 @@ import java.util.Map;
|
||||||
public class AuthorizationContext {
|
public class AuthorizationContext {
|
||||||
|
|
||||||
private final AccessToken authzToken;
|
private final AccessToken authzToken;
|
||||||
|
private final PathConfig current;
|
||||||
private final Map<String, PathConfig> paths;
|
private final Map<String, PathConfig> paths;
|
||||||
private boolean granted;
|
private boolean granted;
|
||||||
|
|
||||||
public AuthorizationContext(AccessToken authzToken, Map<String, PathConfig> paths) {
|
public AuthorizationContext(AccessToken authzToken, PathConfig current, Map<String, PathConfig> paths) {
|
||||||
this.authzToken = authzToken;
|
this.authzToken = authzToken;
|
||||||
|
this.current = current;
|
||||||
this.paths = paths;
|
this.paths = paths;
|
||||||
this.granted = true;
|
this.granted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorizationContext() {
|
public AuthorizationContext() {
|
||||||
this(null, null);
|
this(null, null, null);
|
||||||
this.granted = false;
|
this.granted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,9 +59,15 @@ public class AuthorizationContext {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Permission permission : authorization.getPermissions()) {
|
if (current != null) {
|
||||||
for (PathConfig pathHolder : this.paths.values()) {
|
if (current.getName().equals(resourceName)) {
|
||||||
if (pathHolder.getName().equals(resourceName)) {
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasResourcePermission(resourceName)) {
|
||||||
|
for (Permission permission : authorization.getPermissions()) {
|
||||||
|
for (PathConfig pathHolder : paths.values()) {
|
||||||
if (pathHolder.getId().equals(permission.getResourceSetId())) {
|
if (pathHolder.getId().equals(permission.getResourceSetId())) {
|
||||||
if (permission.getScopes().contains(scopeName)) {
|
if (permission.getScopes().contains(scopeName)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -83,13 +91,15 @@ public class AuthorizationContext {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current != null) {
|
||||||
|
if (current.getName().equals(resourceName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (Permission permission : authorization.getPermissions()) {
|
for (Permission permission : authorization.getPermissions()) {
|
||||||
for (PathConfig pathHolder : this.paths.values()) {
|
if (permission.getResourceSetName().equals(resourceName) || permission.getResourceSetId().equals(resourceName)) {
|
||||||
if (pathHolder.getName().equals(resourceName)) {
|
return true;
|
||||||
if (pathHolder.getId().equals(permission.getResourceSetId())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ public class PolicyEnforcerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PathConfig> getPaths() {
|
public List<PathConfig> getPaths() {
|
||||||
return Collections.unmodifiableList(this.paths);
|
return this.paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EnforcementMode getEnforcementMode() {
|
public EnforcementMode getEnforcementMode() {
|
||||||
|
|
|
@ -202,28 +202,7 @@ An Angular JS example using Keycloak to secure it.
|
||||||
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
|
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
|
||||||
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
|
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
|
||||||
|
|
||||||
Step 10: Angular2 JS Example
|
Step 10: Pure HTML5/Javascript Example
|
||||||
----------------------------------
|
|
||||||
An Angular2 JS example using Keycloak to secure it. Angular2 is in beta version yet.
|
|
||||||
|
|
||||||
To install angular2
|
|
||||||
```
|
|
||||||
$ cd keycloak/examples/demo-template/angular2-product-app/src/main/webapp/
|
|
||||||
$ npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Transpile TypeScript to JavaScript before running the application.
|
|
||||||
```
|
|
||||||
$ npm run tsc
|
|
||||||
```
|
|
||||||
|
|
||||||
[http://localhost:8080/angular2-product](http://localhost:8080/angular2-product)
|
|
||||||
|
|
||||||
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
|
|
||||||
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
|
|
||||||
|
|
||||||
|
|
||||||
Step 11: Pure HTML5/Javascript Example
|
|
||||||
----------------------------------
|
----------------------------------
|
||||||
An pure HTML5/Javascript example using Keycloak to secure it.
|
An pure HTML5/Javascript example using Keycloak to secure it.
|
||||||
|
|
||||||
|
@ -232,7 +211,7 @@ An pure HTML5/Javascript example using Keycloak to secure it.
|
||||||
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
|
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
|
||||||
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
|
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
|
||||||
|
|
||||||
Step 12: Service Account Example
|
Step 11: Service Account Example
|
||||||
================================
|
================================
|
||||||
An example for retrieve service account dedicated to the Client Application itself (not to any user).
|
An example for retrieve service account dedicated to the Client Application itself (not to any user).
|
||||||
|
|
||||||
|
@ -240,7 +219,7 @@ An example for retrieve service account dedicated to the Client Application itse
|
||||||
|
|
||||||
Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) .
|
Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) .
|
||||||
|
|
||||||
Step 13: Offline Access Example
|
Step 12: Offline Access Example
|
||||||
===============================
|
===============================
|
||||||
An example for retrieve offline token, which is then saved to the database and can be used by application anytime later. Offline token
|
An example for retrieve offline token, which is then saved to the database and can be used by application anytime later. Offline token
|
||||||
is valid even if user is already logged out from SSO. Server restart also won't invalidate offline token. Offline token can be revoked by the user in
|
is valid even if user is already logged out from SSO. Server restart also won't invalidate offline token. Offline token can be revoked by the user in
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!--
|
|
||||||
~ Copyright 2016 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
|
||||||
<parent>
|
|
||||||
<artifactId>keycloak-examples-demo-parent</artifactId>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<version>3.2.0.CR1-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<groupId>org.keycloak.example.demo</groupId>
|
|
||||||
<artifactId>angular2-product-example</artifactId>
|
|
||||||
<packaging>war</packaging>
|
|
||||||
<name>Angular2 Product Portal JS</name>
|
|
||||||
<description/>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<finalName>angular2-product</finalName>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.jboss.as.plugins</groupId>
|
|
||||||
<artifactId>jboss-as-maven-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<skip>false</skip>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.wildfly.plugins</groupId>
|
|
||||||
<artifactId>wildfly-maven-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<skip>false</skip>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-war-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<webResources>
|
|
||||||
<resource>
|
|
||||||
<!-- this is relative to the pom.xml directory -->
|
|
||||||
<directory>src/main/frontend/dist</directory>
|
|
||||||
</resource>
|
|
||||||
</webResources>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>com.github.eirslett</groupId>
|
|
||||||
<artifactId>frontend-maven-plugin</artifactId>
|
|
||||||
<version>1.3</version>
|
|
||||||
<configuration>
|
|
||||||
<nodeVersion>v6.10.0</nodeVersion>
|
|
||||||
<workingDirectory>src/main/frontend</workingDirectory>
|
|
||||||
<installDirectory>target</installDirectory>
|
|
||||||
</configuration>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>install node and npm</id>
|
|
||||||
<goals>
|
|
||||||
<goal>install-node-and-npm</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
<execution>
|
|
||||||
<id>npm install</id>
|
|
||||||
<goals>
|
|
||||||
<goal>npm</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<arguments>install</arguments>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
<execution>
|
|
||||||
<id>npm run build</id>
|
|
||||||
<goals>
|
|
||||||
<goal>npm</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<arguments>run ng -- build --base-href /angular2-product/ --env=war</arguments>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
|
@ -1,57 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
||||||
"project": {
|
|
||||||
"version": "1.0.0-beta.32.3",
|
|
||||||
"name": "angular2-product-app"
|
|
||||||
},
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"root": "src",
|
|
||||||
"outDir": "dist",
|
|
||||||
"assets": [
|
|
||||||
"assets",
|
|
||||||
"favicon.ico"
|
|
||||||
],
|
|
||||||
"index": "index.html",
|
|
||||||
"main": "main.ts",
|
|
||||||
"polyfills": "polyfills.ts",
|
|
||||||
"test": "test.ts",
|
|
||||||
"tsconfig": "tsconfig.json",
|
|
||||||
"prefix": "app",
|
|
||||||
"styles": [
|
|
||||||
"styles.css"
|
|
||||||
],
|
|
||||||
"scripts": [],
|
|
||||||
"environmentSource": "environments/environment.ts",
|
|
||||||
"environments": {
|
|
||||||
"dev": "environments/environment.ts",
|
|
||||||
"war": "environments/environment.war.ts",
|
|
||||||
"prod": "environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"e2e": {
|
|
||||||
"protractor": {
|
|
||||||
"config": "./protractor.conf.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": [
|
|
||||||
{
|
|
||||||
"files": "src/**/*.ts",
|
|
||||||
"project": "src/tsconfig.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": "e2e/**/*.ts",
|
|
||||||
"project": "e2e/tsconfig.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"test": {
|
|
||||||
"karma": {
|
|
||||||
"config": "./karma.conf.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaults": {
|
|
||||||
"styleExt": "css",
|
|
||||||
"component": {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Editor configuration, see http://editorconfig.org
|
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
max_line_length = off
|
|
||||||
trim_trailing_whitespace = false
|
|
|
@ -1,41 +0,0 @@
|
||||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# compiled output
|
|
||||||
/dist
|
|
||||||
/tmp
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
/.idea
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# IDE - VSCode
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
# misc
|
|
||||||
/.sass-cache
|
|
||||||
/connect.lock
|
|
||||||
/coverage/*
|
|
||||||
/libpeerconnection.log
|
|
||||||
npm-debug.log
|
|
||||||
testem.log
|
|
||||||
/typings
|
|
||||||
|
|
||||||
# e2e
|
|
||||||
/e2e/*.js
|
|
||||||
/e2e/*.map
|
|
||||||
|
|
||||||
#System Files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
|
@ -1,36 +0,0 @@
|
||||||
# Angular2ProductApp
|
|
||||||
|
|
||||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.0.0-beta.32.3. Keycloak integration is based on Angular
|
|
||||||
|
|
||||||
## Development server
|
|
||||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
|
||||||
|
|
||||||
This application depends on `database-service` application API deployed at `http://localhost:8080/database-service` so you will have to make sure it allows cross-origin requests. It can be enabled for `database-service` in it's `keycloak.json`:
|
|
||||||
|
|
||||||
{
|
|
||||||
"realm" : "demo",
|
|
||||||
"resource" : "database-service",
|
|
||||||
...
|
|
||||||
"enable-cors": true
|
|
||||||
}
|
|
||||||
|
|
||||||
## Code scaffolding
|
|
||||||
|
|
||||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
|
|
||||||
|
|
||||||
## Running unit tests
|
|
||||||
|
|
||||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
|
||||||
|
|
||||||
## Running end-to-end tests
|
|
||||||
|
|
||||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
|
||||||
Before running the tests make sure you are serving the app via `ng serve` and Keycloak and `database-service` is up and running at `http://localhost:8080`.
|
|
||||||
|
|
||||||
## Further help
|
|
||||||
|
|
||||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { Angular2ProductAppPage } from './app.po';
|
|
||||||
|
|
||||||
describe('angular2-product-app App', () => {
|
|
||||||
let page: Angular2ProductAppPage;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
page = new Angular2ProductAppPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display message saying Angular2 Product', () => {
|
|
||||||
page.navigateTo();
|
|
||||||
expect(page.getParagraphText()).toEqual('Angular2 Product');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load Products', () => {
|
|
||||||
page.navigateTo();
|
|
||||||
const products = page.loadProducts();
|
|
||||||
['iphone', 'ipad', 'ipod'].forEach(e => expect(products).toContain(e));
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { browser, element, by } from 'protractor';
|
|
||||||
|
|
||||||
export class Angular2ProductAppPage {
|
|
||||||
navigateTo() {
|
|
||||||
browser.ignoreSynchronization = true;
|
|
||||||
browser.get('/');
|
|
||||||
browser.getCurrentUrl().then(url => {
|
|
||||||
if (url.includes('/auth/realms/demo')) {
|
|
||||||
element(by.id('username')).sendKeys('bburke@redhat.com');
|
|
||||||
element(by.id('password')).sendKeys('password');
|
|
||||||
element(by.id('kc-login')).click();
|
|
||||||
}
|
|
||||||
browser.ignoreSynchronization = false;
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getParagraphText() {
|
|
||||||
return element(by.css('app-root h1')).getText();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadProducts() {
|
|
||||||
const click = element(by.id('reload-data')).click();
|
|
||||||
browser.wait(click, 2000, 'Products should load within 2 seconds');
|
|
||||||
return element.all(by.css('table.table td')).getText();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"compileOnSave": false,
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": false,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"lib": [
|
|
||||||
"es2016"
|
|
||||||
],
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"outDir": "../dist/out-tsc-e2e",
|
|
||||||
"sourceMap": true,
|
|
||||||
"target": "es6",
|
|
||||||
"typeRoots": [
|
|
||||||
"../node_modules/@types"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/0.13/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '',
|
|
||||||
frameworks: ['jasmine', '@angular/cli'],
|
|
||||||
plugins: [
|
|
||||||
require('karma-jasmine'),
|
|
||||||
require('karma-chrome-launcher'),
|
|
||||||
require('karma-jasmine-html-reporter'),
|
|
||||||
require('karma-coverage-istanbul-reporter'),
|
|
||||||
require('@angular/cli/plugins/karma')
|
|
||||||
],
|
|
||||||
client:{
|
|
||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|
||||||
},
|
|
||||||
files: [
|
|
||||||
{ pattern: './src/test.ts', watched: false }
|
|
||||||
],
|
|
||||||
preprocessors: {
|
|
||||||
'./src/test.ts': ['@angular/cli']
|
|
||||||
},
|
|
||||||
mime: {
|
|
||||||
'text/x-typescript': ['ts','tsx']
|
|
||||||
},
|
|
||||||
coverageIstanbulReporter: {
|
|
||||||
reports: [ 'html', 'lcovonly' ],
|
|
||||||
fixWebpackSourcePaths: true
|
|
||||||
},
|
|
||||||
angularCli: {
|
|
||||||
config: './.angular-cli.json',
|
|
||||||
environment: 'dev'
|
|
||||||
},
|
|
||||||
reporters: config.angularCli && config.angularCli.codeCoverage
|
|
||||||
? ['progress', 'coverage-istanbul']
|
|
||||||
: ['progress', 'kjhtml'],
|
|
||||||
port: 9876,
|
|
||||||
colors: true,
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
autoWatch: true,
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
singleRun: false
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,46 +0,0 @@
|
||||||
{
|
|
||||||
"name": "angular2-product-app",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"angular-cli": {},
|
|
||||||
"scripts": {
|
|
||||||
"ng": "ng",
|
|
||||||
"start": "ng serve",
|
|
||||||
"test": "ng test",
|
|
||||||
"lint": "ng lint",
|
|
||||||
"e2e": "ng e2e"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@angular/common": "^2.4.0",
|
|
||||||
"@angular/compiler": "^2.4.0",
|
|
||||||
"@angular/core": "^2.4.0",
|
|
||||||
"@angular/forms": "^2.4.0",
|
|
||||||
"@angular/http": "^2.4.0",
|
|
||||||
"@angular/platform-browser": "^2.4.0",
|
|
||||||
"@angular/platform-browser-dynamic": "^2.4.0",
|
|
||||||
"@angular/router": "^3.4.0",
|
|
||||||
"core-js": "^2.4.1",
|
|
||||||
"rxjs": "^5.1.0",
|
|
||||||
"zone.js": "^0.7.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@angular/cli": "1.0.0-beta.32.3",
|
|
||||||
"@angular/compiler-cli": "^2.4.0",
|
|
||||||
"@types/jasmine": "2.5.38",
|
|
||||||
"@types/node": "~6.0.60",
|
|
||||||
"codelyzer": "~2.0.0-beta.4",
|
|
||||||
"jasmine-core": "~2.5.2",
|
|
||||||
"jasmine-spec-reporter": "~3.2.0",
|
|
||||||
"karma": "~1.4.1",
|
|
||||||
"karma-chrome-launcher": "~2.0.0",
|
|
||||||
"karma-cli": "~1.0.1",
|
|
||||||
"karma-jasmine": "~1.1.0",
|
|
||||||
"karma-jasmine-html-reporter": "^0.2.2",
|
|
||||||
"karma-coverage-istanbul-reporter": "^0.2.0",
|
|
||||||
"protractor": "~5.1.0",
|
|
||||||
"ts-node": "~2.0.0",
|
|
||||||
"tslint": "~4.4.2",
|
|
||||||
"typescript": "~2.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
// Protractor configuration file, see link for more information
|
|
||||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
|
||||||
|
|
||||||
/*global jasmine */
|
|
||||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
allScriptsTimeout: 11000,
|
|
||||||
specs: [
|
|
||||||
'./e2e/**/*.e2e-spec.ts'
|
|
||||||
],
|
|
||||||
capabilities: {
|
|
||||||
'browserName': 'chrome'
|
|
||||||
},
|
|
||||||
directConnect: true,
|
|
||||||
baseUrl: 'http://localhost:4200/',
|
|
||||||
framework: 'jasmine',
|
|
||||||
jasmineNodeOpts: {
|
|
||||||
showColors: true,
|
|
||||||
defaultTimeoutInterval: 30000,
|
|
||||||
print: function() {}
|
|
||||||
},
|
|
||||||
beforeLaunch: function() {
|
|
||||||
require('ts-node').register({
|
|
||||||
project: 'e2e'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPrepare() {
|
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
<div id="content-area" class="col-md-9" role="main">
|
|
||||||
<div id="content">
|
|
||||||
<h1>{{title}}</h1>
|
|
||||||
<h2><span>Products</span></h2>
|
|
||||||
<button type="button" (click)="logout()">Sign Out</button>
|
|
||||||
<button type="button" id="reload-data" (click)="reloadData()">Reload</button>
|
|
||||||
<table class="table" [hidden]="!products.length">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Product Listing</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let p of products">
|
|
||||||
<td>{{p}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { TestBed, async } from '@angular/core/testing';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
import { KeycloakService } from './keycloak/keycloak.service';
|
|
||||||
import {
|
|
||||||
HttpModule,
|
|
||||||
XHRBackend,
|
|
||||||
ResponseOptions,
|
|
||||||
Response,
|
|
||||||
RequestMethod
|
|
||||||
} from '@angular/http';
|
|
||||||
import {
|
|
||||||
MockBackend,
|
|
||||||
MockConnection
|
|
||||||
} from '@angular/http/testing/mock_backend';
|
|
||||||
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [HttpModule],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: XHRBackend,
|
|
||||||
useClass: MockBackend
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: KeycloakService
|
|
||||||
}
|
|
||||||
],
|
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
});
|
|
||||||
TestBed.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the app', async(() => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.debugElement.componentInstance;
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it(`should have as title 'Angular2 Product'`, async(() => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.debugElement.componentInstance;
|
|
||||||
expect(app.title).toEqual('Angular2 Product');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should render title in a h1 tag', async(() => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
fixture.detectChanges();
|
|
||||||
const compiled = fixture.debugElement.nativeElement;
|
|
||||||
expect(compiled.querySelector('h1').textContent).toContain('Angular2 Product');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should render product list', async(() => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
fixture.componentInstance.products = ['iphone', 'ipad', 'ipod'];
|
|
||||||
fixture.detectChanges();
|
|
||||||
const compiled = fixture.debugElement.nativeElement;
|
|
||||||
expect(compiled.querySelector('table thead tr th').textContent).toContain('Product Listing');
|
|
||||||
expect(compiled.querySelectorAll('table tbody tr td')[0].textContent).toContain('iphone');
|
|
||||||
expect(compiled.querySelectorAll('table tbody tr td')[1].textContent).toContain('ipad');
|
|
||||||
expect(compiled.querySelectorAll('table tbody tr td')[2].textContent).toContain('ipod');
|
|
||||||
}));
|
|
||||||
});
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import {Http, Headers, RequestOptions, Response} from '@angular/http';
|
|
||||||
import {Observable} from 'rxjs/Observable';
|
|
||||||
import 'rxjs/add/operator/map';
|
|
||||||
import {KeycloakService} from './keycloak/keycloak.service';
|
|
||||||
|
|
||||||
import { environment } from '../environments/environment';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
templateUrl: './app.component.html',
|
|
||||||
styleUrls: ['./app.component.css']
|
|
||||||
})
|
|
||||||
export class AppComponent {
|
|
||||||
title = 'Angular2 Product';
|
|
||||||
|
|
||||||
products: string[] = [];
|
|
||||||
|
|
||||||
constructor(private http: Http, private kc: KeycloakService) {}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.kc.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadData() {
|
|
||||||
this.http.get(environment.serviceBaseUrl + '/products')
|
|
||||||
.map(res => res.json())
|
|
||||||
.subscribe(prods => this.products = prods,
|
|
||||||
error => console.log(error));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { HttpModule } from '@angular/http';
|
|
||||||
import { KeycloakService } from './keycloak/keycloak.service';
|
|
||||||
import { KeycloakHttp, KEYCLOAK_HTTP_PROVIDER } from './keycloak/keycloak.http';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
FormsModule,
|
|
||||||
HttpModule
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
KeycloakService,
|
|
||||||
KEYCLOAK_HTTP_PROVIDER
|
|
||||||
],
|
|
||||||
bootstrap: [AppComponent]
|
|
||||||
})
|
|
||||||
export class AppModule { }
|
|
|
@ -1,47 +0,0 @@
|
||||||
import {Injectable, ReflectiveInjector} from '@angular/core';
|
|
||||||
import {async, fakeAsync, tick} from '@angular/core/testing';
|
|
||||||
import {BaseRequestOptions, ConnectionBackend, Http, RequestOptions} from '@angular/http';
|
|
||||||
import {Response, ResponseOptions} from '@angular/http';
|
|
||||||
import {MockBackend, MockConnection} from '@angular/http/testing';
|
|
||||||
|
|
||||||
import { KeycloakHttp, KEYCLOAK_HTTP_PROVIDER, keycloakHttpFactory } from './keycloak.http';
|
|
||||||
import { KeycloakService } from './keycloak.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
class MockKeycloakService extends KeycloakService {
|
|
||||||
getToken(): Promise<string> {
|
|
||||||
return Promise.resolve('hello');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('KeycloakHttp', () => {
|
|
||||||
|
|
||||||
let injector: ReflectiveInjector;
|
|
||||||
let backend: MockBackend;
|
|
||||||
let lastConnection: MockConnection;
|
|
||||||
let http: Http;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
injector = ReflectiveInjector.resolveAndCreate([
|
|
||||||
{provide: ConnectionBackend, useClass: MockBackend},
|
|
||||||
{provide: RequestOptions, useClass: BaseRequestOptions},
|
|
||||||
{provide: KeycloakService, useClass: MockKeycloakService},
|
|
||||||
{
|
|
||||||
provide: Http,
|
|
||||||
useFactory: keycloakHttpFactory,
|
|
||||||
deps: [ConnectionBackend, RequestOptions, KeycloakService]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
http = injector.get(Http);
|
|
||||||
backend = injector.get(ConnectionBackend) as MockBackend;
|
|
||||||
backend.connections.subscribe((c: MockConnection) => lastConnection = c);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set Authorization header', fakeAsync(() => {
|
|
||||||
http.get('foo').subscribe(r => console.log(r));
|
|
||||||
tick();
|
|
||||||
expect(lastConnection).toBeDefined('no http service connection at all?');
|
|
||||||
expect(lastConnection.request.headers.get('Authorization')).toBe('Bearer hello');
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
import {Injectable} from '@angular/core';
|
|
||||||
import {Http, Request, XHRBackend, ConnectionBackend, RequestOptions, RequestOptionsArgs, Response, Headers} from '@angular/http';
|
|
||||||
|
|
||||||
import {KeycloakService} from './keycloak.service';
|
|
||||||
import {Observable} from 'rxjs/Rx';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This provides a wrapper over the ng2 Http class that insures tokens are refreshed on each request.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class KeycloakHttp extends Http {
|
|
||||||
constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions, private _keycloakService: KeycloakService) {
|
|
||||||
super(_backend, _defaultOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
|
|
||||||
const tokenPromise: Promise<string> = this._keycloakService.getToken();
|
|
||||||
const tokenObservable: Observable<string> = Observable.fromPromise(tokenPromise);
|
|
||||||
|
|
||||||
if (typeof url === 'string') {
|
|
||||||
return tokenObservable.map(token => {
|
|
||||||
const authOptions = new RequestOptions({headers: new Headers({'Authorization': 'Bearer ' + token})});
|
|
||||||
return new RequestOptions().merge(options).merge(authOptions);
|
|
||||||
}).concatMap(opts => super.request(url, opts));
|
|
||||||
} else if (url instanceof Request) {
|
|
||||||
return tokenObservable.map(token => {
|
|
||||||
url.headers.set('Authorization', 'Bearer ' + token);
|
|
||||||
return url;
|
|
||||||
}).concatMap(request => super.request(request));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function keycloakHttpFactory(backend: XHRBackend, defaultOptions: RequestOptions, keycloakService: KeycloakService) {
|
|
||||||
return new KeycloakHttp(backend, defaultOptions, keycloakService);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const KEYCLOAK_HTTP_PROVIDER = {
|
|
||||||
provide: Http,
|
|
||||||
useFactory: keycloakHttpFactory,
|
|
||||||
deps: [XHRBackend, RequestOptions, KeycloakService]
|
|
||||||
};
|
|
|
@ -1,60 +0,0 @@
|
||||||
import {Injectable} from '@angular/core';
|
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
|
|
||||||
declare var Keycloak: any;
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class KeycloakService {
|
|
||||||
static auth: any = {};
|
|
||||||
|
|
||||||
static init(): Promise<any> {
|
|
||||||
const keycloakAuth: any = Keycloak({
|
|
||||||
url: environment.keykloakBaseUrl,
|
|
||||||
realm: 'demo',
|
|
||||||
clientId: 'angular2-product',
|
|
||||||
});
|
|
||||||
|
|
||||||
KeycloakService.auth.loggedIn = false;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
keycloakAuth.init({ onLoad: 'login-required' })
|
|
||||||
.success(() => {
|
|
||||||
KeycloakService.auth.loggedIn = true;
|
|
||||||
KeycloakService.auth.authz = keycloakAuth;
|
|
||||||
KeycloakService.auth.logoutUrl = keycloakAuth.authServerUrl
|
|
||||||
+ '/realms/demo/protocol/openid-connect/logout?redirect_uri='
|
|
||||||
+ document.baseURI;
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.error(() => {
|
|
||||||
reject();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
console.log('*** LOGOUT');
|
|
||||||
KeycloakService.auth.loggedIn = false;
|
|
||||||
KeycloakService.auth.authz = null;
|
|
||||||
|
|
||||||
window.location.href = KeycloakService.auth.logoutUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
getToken(): Promise<string> {
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
if (KeycloakService.auth.authz.token) {
|
|
||||||
KeycloakService.auth.authz
|
|
||||||
.updateToken(5)
|
|
||||||
.success(() => {
|
|
||||||
resolve(<string>KeycloakService.auth.authz.token);
|
|
||||||
})
|
|
||||||
.error(() => {
|
|
||||||
reject('Failed to refresh token');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject('Not loggen in');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export const environment = {
|
|
||||||
production: true,
|
|
||||||
keykloakBaseUrl: 'http://localhost:8080/auth',
|
|
||||||
serviceBaseUrl: 'http://localhost:8080/database'
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
// The file contents for the current environment will overwrite these during build.
|
|
||||||
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
|
|
||||||
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
|
|
||||||
// The list of which env maps to which file can be found in `.angular-cli.json`.
|
|
||||||
|
|
||||||
export const environment = {
|
|
||||||
production: false,
|
|
||||||
keykloakBaseUrl: 'http://localhost:8080/auth',
|
|
||||||
serviceBaseUrl: 'http://localhost:8080/database'
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
export const environment = {
|
|
||||||
production: false,
|
|
||||||
keykloakBaseUrl: '/auth',
|
|
||||||
serviceBaseUrl: '/database'
|
|
||||||
};
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.3 KiB |
|
@ -1,15 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Angular2ProductApp</title>
|
|
||||||
<base href="/">
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
<script src="http://localhost:8080/auth/js/keycloak.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<app-root>Loading...</app-root>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { enableProdMode } from '@angular/core';
|
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
|
||||||
|
|
||||||
import { AppModule } from './app/app.module';
|
|
||||||
import { environment } from './environments/environment';
|
|
||||||
import { KeycloakService } from './app/keycloak/keycloak.service';
|
|
||||||
|
|
||||||
if (environment.production) {
|
|
||||||
enableProdMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
KeycloakService.init()
|
|
||||||
.then(() => platformBrowserDynamic().bootstrapModule(AppModule))
|
|
||||||
.catch(e => window.location.reload());
|
|
|
@ -1,68 +0,0 @@
|
||||||
/**
|
|
||||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
|
||||||
* You can add your own extra polyfills to this file.
|
|
||||||
*
|
|
||||||
* This file is divided into 2 sections:
|
|
||||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
|
||||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
|
||||||
* file.
|
|
||||||
*
|
|
||||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
|
||||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
|
||||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
|
||||||
*
|
|
||||||
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* BROWSER POLYFILLS
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
|
||||||
// import 'core-js/es6/symbol';
|
|
||||||
// import 'core-js/es6/object';
|
|
||||||
// import 'core-js/es6/function';
|
|
||||||
// import 'core-js/es6/parse-int';
|
|
||||||
// import 'core-js/es6/parse-float';
|
|
||||||
// import 'core-js/es6/number';
|
|
||||||
// import 'core-js/es6/math';
|
|
||||||
// import 'core-js/es6/string';
|
|
||||||
// import 'core-js/es6/date';
|
|
||||||
// import 'core-js/es6/array';
|
|
||||||
// import 'core-js/es6/regexp';
|
|
||||||
// import 'core-js/es6/map';
|
|
||||||
// import 'core-js/es6/set';
|
|
||||||
|
|
||||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
|
||||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
|
||||||
|
|
||||||
/** IE10 and IE11 requires the following to support `@angular/animation`. */
|
|
||||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
|
||||||
|
|
||||||
|
|
||||||
/** Evergreen browsers require these. **/
|
|
||||||
import 'core-js/es6/reflect';
|
|
||||||
import 'core-js/es7/reflect';
|
|
||||||
|
|
||||||
|
|
||||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
|
||||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* Zone JS is required by Angular itself.
|
|
||||||
*/
|
|
||||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* APPLICATION IMPORTS
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Date, currency, decimal and percent pipes.
|
|
||||||
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
|
|
||||||
*/
|
|
||||||
// import 'intl'; // Run `npm install --save intl`.
|
|
|
@ -1 +0,0 @@
|
||||||
/* You can add global styles to this file, and also import other style files */
|
|
|
@ -1,32 +0,0 @@
|
||||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
|
||||||
|
|
||||||
import 'zone.js/dist/long-stack-trace-zone';
|
|
||||||
import 'zone.js/dist/proxy.js';
|
|
||||||
import 'zone.js/dist/sync-test';
|
|
||||||
import 'zone.js/dist/jasmine-patch';
|
|
||||||
import 'zone.js/dist/async-test';
|
|
||||||
import 'zone.js/dist/fake-async-test';
|
|
||||||
import { getTestBed } from '@angular/core/testing';
|
|
||||||
import {
|
|
||||||
BrowserDynamicTestingModule,
|
|
||||||
platformBrowserDynamicTesting
|
|
||||||
} from '@angular/platform-browser-dynamic/testing';
|
|
||||||
|
|
||||||
// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
|
|
||||||
declare var __karma__: any;
|
|
||||||
declare var require: any;
|
|
||||||
|
|
||||||
// Prevent Karma from running prematurely.
|
|
||||||
__karma__.loaded = function () {};
|
|
||||||
|
|
||||||
// First, initialize the Angular testing environment.
|
|
||||||
getTestBed().initTestEnvironment(
|
|
||||||
BrowserDynamicTestingModule,
|
|
||||||
platformBrowserDynamicTesting()
|
|
||||||
);
|
|
||||||
// Then we find all the tests.
|
|
||||||
const context = require.context('./', true, /\.spec\.ts$/);
|
|
||||||
// And load the modules.
|
|
||||||
context.keys().map(context);
|
|
||||||
// Finally, start Karma to run the tests.
|
|
||||||
__karma__.start();
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "",
|
|
||||||
"declaration": false,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"lib": [
|
|
||||||
"es2016",
|
|
||||||
"dom"
|
|
||||||
],
|
|
||||||
"mapRoot": "./",
|
|
||||||
"module": "es2015",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"outDir": "../dist/out-tsc",
|
|
||||||
"sourceMap": true,
|
|
||||||
"target": "es5",
|
|
||||||
"typeRoots": [
|
|
||||||
"../node_modules/@types"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
{
|
|
||||||
"rulesDirectory": [
|
|
||||||
"node_modules/codelyzer"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"callable-types": true,
|
|
||||||
"class-name": true,
|
|
||||||
"comment-format": [
|
|
||||||
true,
|
|
||||||
"check-space"
|
|
||||||
],
|
|
||||||
"curly": true,
|
|
||||||
"eofline": true,
|
|
||||||
"forin": true,
|
|
||||||
"import-blacklist": [true, "rxjs"],
|
|
||||||
"import-spacing": true,
|
|
||||||
"indent": [
|
|
||||||
true,
|
|
||||||
"spaces"
|
|
||||||
],
|
|
||||||
"interface-over-type-literal": true,
|
|
||||||
"label-position": true,
|
|
||||||
"max-line-length": [
|
|
||||||
true,
|
|
||||||
140
|
|
||||||
],
|
|
||||||
"member-access": false,
|
|
||||||
"member-ordering": [
|
|
||||||
true,
|
|
||||||
"static-before-instance",
|
|
||||||
"variables-before-functions"
|
|
||||||
],
|
|
||||||
"no-arg": true,
|
|
||||||
"no-bitwise": true,
|
|
||||||
"no-console": [
|
|
||||||
true,
|
|
||||||
"debug",
|
|
||||||
"info",
|
|
||||||
"time",
|
|
||||||
"timeEnd",
|
|
||||||
"trace"
|
|
||||||
],
|
|
||||||
"no-construct": true,
|
|
||||||
"no-debugger": true,
|
|
||||||
"no-duplicate-variable": true,
|
|
||||||
"no-empty": false,
|
|
||||||
"no-empty-interface": true,
|
|
||||||
"no-eval": true,
|
|
||||||
"no-inferrable-types": [true, "ignore-params"],
|
|
||||||
"no-shadowed-variable": true,
|
|
||||||
"no-string-literal": false,
|
|
||||||
"no-string-throw": true,
|
|
||||||
"no-switch-case-fall-through": true,
|
|
||||||
"no-trailing-whitespace": true,
|
|
||||||
"no-unused-expression": true,
|
|
||||||
"no-use-before-declare": true,
|
|
||||||
"no-var-keyword": true,
|
|
||||||
"object-literal-sort-keys": false,
|
|
||||||
"one-line": [
|
|
||||||
true,
|
|
||||||
"check-open-brace",
|
|
||||||
"check-catch",
|
|
||||||
"check-else",
|
|
||||||
"check-whitespace"
|
|
||||||
],
|
|
||||||
"prefer-const": true,
|
|
||||||
"quotemark": [
|
|
||||||
true,
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"radix": true,
|
|
||||||
"semicolon": [
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"triple-equals": [
|
|
||||||
true,
|
|
||||||
"allow-null-check"
|
|
||||||
],
|
|
||||||
"typedef-whitespace": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"call-signature": "nospace",
|
|
||||||
"index-signature": "nospace",
|
|
||||||
"parameter": "nospace",
|
|
||||||
"property-declaration": "nospace",
|
|
||||||
"variable-declaration": "nospace"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"typeof-compare": true,
|
|
||||||
"unified-signatures": true,
|
|
||||||
"variable-name": false,
|
|
||||||
"whitespace": [
|
|
||||||
true,
|
|
||||||
"check-branch",
|
|
||||||
"check-decl",
|
|
||||||
"check-operator",
|
|
||||||
"check-separator",
|
|
||||||
"check-type"
|
|
||||||
],
|
|
||||||
|
|
||||||
"directive-selector": [true, "attribute", "app", "camelCase"],
|
|
||||||
"component-selector": [true, "element", "app", "kebab-case"],
|
|
||||||
"use-input-property-decorator": true,
|
|
||||||
"use-output-property-decorator": true,
|
|
||||||
"use-host-property-decorator": true,
|
|
||||||
"no-input-rename": true,
|
|
||||||
"no-output-rename": true,
|
|
||||||
"use-life-cycle-interface": true,
|
|
||||||
"use-pipe-transform-interface": true,
|
|
||||||
"component-class-suffix": true,
|
|
||||||
"directive-class-suffix": true,
|
|
||||||
"no-access-missing-member": true,
|
|
||||||
"templates-use-public": true,
|
|
||||||
"invoke-injectable": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -51,7 +51,6 @@
|
||||||
<module>example-ear</module>
|
<module>example-ear</module>
|
||||||
<module>admin-access-app</module>
|
<module>admin-access-app</module>
|
||||||
<module>angular-product-app</module>
|
<module>angular-product-app</module>
|
||||||
<module>angular2-product-app</module>
|
|
||||||
<module>database-service</module>
|
<module>database-service</module>
|
||||||
<module>third-party</module>
|
<module>third-party</module>
|
||||||
<module>third-party-cdi</module>
|
<module>third-party-cdi</module>
|
||||||
|
|
|
@ -50,13 +50,6 @@
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-deploy-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<skip>true</skip>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
|
|
@ -3,6 +3,8 @@ Test Cross-Data-Center scenario (test with external JDG server)
|
||||||
|
|
||||||
These are temporary notes. This docs should be removed once we have cross-DC support finished and properly documented.
|
These are temporary notes. This docs should be removed once we have cross-DC support finished and properly documented.
|
||||||
|
|
||||||
|
Note that these steps are already automated, see Cross-DC tests section in [HOW-TO-RUN.md](../testsuite/integration-arquillian/HOW-TO-RUN.md) document.
|
||||||
|
|
||||||
What is working right now is:
|
What is working right now is:
|
||||||
- Propagating of invalidation messages for "realms" and "users" caches
|
- Propagating of invalidation messages for "realms" and "users" caches
|
||||||
- All the other things provided by ClusterProvider, which is:
|
- All the other things provided by ClusterProvider, which is:
|
||||||
|
@ -18,7 +20,7 @@ Basic setup
|
||||||
|
|
||||||
This is setup with 2 keycloak nodes, which are NOT in cluster. They just share the same database and they will be configured with "work" infinispan cache with remoteStore, which will point
|
This is setup with 2 keycloak nodes, which are NOT in cluster. They just share the same database and they will be configured with "work" infinispan cache with remoteStore, which will point
|
||||||
to external JDG server.
|
to external JDG server.
|
||||||
|
|
||||||
JDG Server setup
|
JDG Server setup
|
||||||
----------------
|
----------------
|
||||||
- Download JDG 7.0 server and unzip to some folder
|
- Download JDG 7.0 server and unzip to some folder
|
||||||
|
|
|
@ -154,7 +154,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
|
|
||||||
if (clustered) {
|
if (clustered) {
|
||||||
String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
|
String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
|
||||||
configureTransport(gcb, nodeName);
|
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
|
||||||
|
configureTransport(gcb, nodeName, jgroupsUdpMcastAddr);
|
||||||
}
|
}
|
||||||
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
|
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
|
||||||
|
|
||||||
|
@ -317,24 +318,40 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
return cb.build();
|
return cb.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName) {
|
private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object();
|
||||||
|
|
||||||
|
protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String jgroupsUdpMcastAddr) {
|
||||||
if (nodeName == null) {
|
if (nodeName == null) {
|
||||||
gcb.transport().defaultTransport();
|
gcb.transport().defaultTransport();
|
||||||
} else {
|
} else {
|
||||||
FileLookup fileLookup = FileLookupFactory.newInstance();
|
FileLookup fileLookup = FileLookupFactory.newInstance();
|
||||||
|
|
||||||
try {
|
synchronized (CHANNEL_INIT_SYNCHRONIZER) {
|
||||||
// Compatibility with Wildfly
|
String originalMcastAddr = System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
|
||||||
JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader()));
|
if (jgroupsUdpMcastAddr == null) {
|
||||||
channel.setName(nodeName);
|
System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
|
||||||
JGroupsTransport transport = new JGroupsTransport(channel);
|
} else {
|
||||||
|
System.setProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR, jgroupsUdpMcastAddr);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Compatibility with Wildfly
|
||||||
|
JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader()));
|
||||||
|
channel.setName(nodeName);
|
||||||
|
JGroupsTransport transport = new JGroupsTransport(channel);
|
||||||
|
|
||||||
gcb.transport().nodeName(nodeName);
|
gcb.transport().nodeName(nodeName);
|
||||||
gcb.transport().transport(transport);
|
gcb.transport().transport(transport);
|
||||||
|
|
||||||
logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
|
logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
if (originalMcastAddr == null) {
|
||||||
|
System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
|
||||||
|
} else {
|
||||||
|
System.setProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR, originalMcastAddr);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ public interface InfinispanConnectionProvider extends Provider {
|
||||||
|
|
||||||
// System property used on Wildfly to identify distributedCache address and sticky session route
|
// System property used on Wildfly to identify distributedCache address and sticky session route
|
||||||
String JBOSS_NODE_NAME = "jboss.node.name";
|
String JBOSS_NODE_NAME = "jboss.node.name";
|
||||||
|
String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr";
|
||||||
|
|
||||||
|
|
||||||
<K, V> Cache<K, V> getCache(String name);
|
<K, V> Cache<K, V> getCache(String name);
|
||||||
|
|
|
@ -220,7 +220,7 @@ public abstract class CacheManager {
|
||||||
|
|
||||||
addInvalidationsFromEvent(event, invalidations);
|
addInvalidationsFromEvent(event, invalidations);
|
||||||
|
|
||||||
getLogger().debugf("Invalidating %d cache items after received event %s", invalidations.size(), event);
|
getLogger().debugf("[%s] Invalidating %d cache items after received event %s", cache.getCacheManager().getAddress(), invalidations.size(), event);
|
||||||
|
|
||||||
for (String invalidation : invalidations) {
|
for (String invalidation : invalidations) {
|
||||||
invalidateObject(invalidation);
|
invalidateObject(invalidation);
|
||||||
|
|
|
@ -164,9 +164,8 @@ public class RealmCacheSession implements CacheRealmProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public void clear() {
|
||||||
cache.clear();
|
|
||||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||||
cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
|
cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,21 +17,19 @@
|
||||||
|
|
||||||
package org.keycloak.models.cache.infinispan.authorization;
|
package org.keycloak.models.cache.infinispan.authorization;
|
||||||
|
|
||||||
|
import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authorization.AuthorizationProvider;
|
|
||||||
import org.keycloak.cluster.ClusterEvent;
|
import org.keycloak.cluster.ClusterEvent;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.cache.CacheRealmProvider;
|
|
||||||
import org.keycloak.models.cache.CacheRealmProviderFactory;
|
|
||||||
import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
|
import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
|
||||||
import org.keycloak.models.cache.authorization.CachedStoreProviderFactory;
|
import org.keycloak.models.cache.authorization.CachedStoreProviderFactory;
|
||||||
import org.keycloak.models.cache.infinispan.RealmCacheManager;
|
import org.keycloak.models.cache.infinispan.ClearCacheEvent;
|
||||||
import org.keycloak.models.cache.infinispan.RealmCacheSession;
|
|
||||||
import org.keycloak.models.cache.infinispan.entities.Revisioned;
|
import org.keycloak.models.cache.infinispan.entities.Revisioned;
|
||||||
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
||||||
|
|
||||||
|
@ -59,21 +57,17 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr
|
||||||
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
|
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
|
||||||
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME);
|
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME);
|
||||||
storeCache = new StoreFactoryCacheManager(cache, revisions);
|
storeCache = new StoreFactoryCacheManager(cache, revisions);
|
||||||
|
|
||||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||||
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
|
|
||||||
|
|
||||||
|
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
|
||||||
if (event instanceof InvalidationEvent) {
|
if (event instanceof InvalidationEvent) {
|
||||||
InvalidationEvent invalidationEvent = (InvalidationEvent) event;
|
InvalidationEvent invalidationEvent = (InvalidationEvent) event;
|
||||||
storeCache.invalidationEventReceived(invalidationEvent);
|
storeCache.invalidationEventReceived(invalidationEvent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
|
cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear());
|
||||||
|
cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear());
|
||||||
storeCache.clear();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
log.debug("Registered cluster listeners");
|
log.debug("Registered cluster listeners");
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
|
||||||
@Override
|
@Override
|
||||||
public Policy getDelegateForUpdate() {
|
public Policy getDelegateForUpdate() {
|
||||||
if (updated == null) {
|
if (updated == null) {
|
||||||
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getResourceServerId());
|
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), cached.getResourceServerId());
|
||||||
updated = cacheSession.getPolicyStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
|
updated = cacheSession.getPolicyStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
|
||||||
if (updated == null) throw new IllegalStateException("Not found in database");
|
if (updated == null) throw new IllegalStateException("Not found in database");
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
|
||||||
@Override
|
@Override
|
||||||
public void setName(String name) {
|
public void setName(String name) {
|
||||||
getDelegateForUpdate();
|
getDelegateForUpdate();
|
||||||
cacheSession.registerPolicyInvalidation(cached.getId(), name, cached.getResourcesIds(), cached.getResourceServerId());
|
cacheSession.registerPolicyInvalidation(cached.getId(), name, cached.getResourcesIds(), cached.getScopesIds(), cached.getResourceServerId());
|
||||||
updated.setName(name);
|
updated.setName(name);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -235,7 +235,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
|
||||||
getDelegateForUpdate();
|
getDelegateForUpdate();
|
||||||
HashSet<String> resources = new HashSet<>();
|
HashSet<String> resources = new HashSet<>();
|
||||||
resources.add(resource.getId());
|
resources.add(resource.getId());
|
||||||
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getResourceServerId());
|
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getScopesIds(), cached.getResourceServerId());
|
||||||
updated.addResource(resource);
|
updated.addResource(resource);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -245,7 +245,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
|
||||||
getDelegateForUpdate();
|
getDelegateForUpdate();
|
||||||
HashSet<String> resources = new HashSet<>();
|
HashSet<String> resources = new HashSet<>();
|
||||||
resources.add(resource.getId());
|
resources.add(resource.getId());
|
||||||
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getResourceServerId());
|
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getScopesIds(), cached.getResourceServerId());
|
||||||
updated.removeResource(resource);
|
updated.removeResource(resource);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ public class StoreFactoryCacheManager extends CacheManager {
|
||||||
addInvalidations(InResourcePredicate.create().resource(id), invalidations);
|
addInvalidations(InResourcePredicate.create().resource(id), invalidations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void policyUpdated(String id, String name, Set<String> resources, String serverId, Set<String> invalidations) {
|
public void policyUpdated(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId, Set<String> invalidations) {
|
||||||
invalidations.add(id);
|
invalidations.add(id);
|
||||||
invalidations.add(StoreFactoryCacheSession.getPolicyByNameCacheKey(name, serverId));
|
invalidations.add(StoreFactoryCacheSession.getPolicyByNameCacheKey(name, serverId));
|
||||||
|
|
||||||
|
@ -111,10 +111,22 @@ public class StoreFactoryCacheManager extends CacheManager {
|
||||||
invalidations.add(StoreFactoryCacheSession.getPolicyByResource(resource, serverId));
|
invalidations.add(StoreFactoryCacheSession.getPolicyByResource(resource, serverId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resourceTypes != null) {
|
||||||
|
for (String type : resourceTypes) {
|
||||||
|
invalidations.add(StoreFactoryCacheSession.getPolicyByResourceType(type, serverId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopes != null) {
|
||||||
|
for (String scope : scopes) {
|
||||||
|
invalidations.add(StoreFactoryCacheSession.getPolicyByScope(scope, serverId));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void policyRemoval(String id, String name, Set<String> resources, String serverId, Set<String> invalidations) {
|
public void policyRemoval(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId, Set<String> invalidations) {
|
||||||
policyUpdated(id, name, resources, serverId, invalidations);
|
policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
@ -252,12 +253,30 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
|
||||||
invalidationEvents.add(ResourceUpdatedEvent.create(id, name, type, uri, scopes, serverId));
|
invalidationEvents.add(ResourceUpdatedEvent.create(id, name, type, uri, scopes, serverId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerPolicyInvalidation(String id, String name, Set<String> resources, String serverId) {
|
public void registerPolicyInvalidation(String id, String name, Set<String> resources, Set<String> scopes, String serverId) {
|
||||||
cache.policyUpdated(id, name, resources, serverId, invalidations);
|
Set<String> resourceTypes = getResourceTypes(resources, serverId);
|
||||||
|
cache.policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations);
|
||||||
PolicyAdapter adapter = managedPolicies.get(id);
|
PolicyAdapter adapter = managedPolicies.get(id);
|
||||||
if (adapter != null) adapter.invalidateFlag();
|
if (adapter != null) adapter.invalidateFlag();
|
||||||
|
|
||||||
invalidationEvents.add(PolicyUpdatedEvent.create(id, name, resources, serverId));
|
invalidationEvents.add(PolicyUpdatedEvent.create(id, name, resources, resourceTypes, scopes, serverId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getResourceTypes(Set<String> resources, String serverId) {
|
||||||
|
if (resources == null) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources.stream().map(resourceId -> {
|
||||||
|
Resource resource = getResourceStore().findById(resourceId, serverId);
|
||||||
|
String type = resource.getType();
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceServerStore getResourceServerStoreDelegate() {
|
public ResourceServerStore getResourceServerStoreDelegate() {
|
||||||
|
@ -626,7 +645,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
|
||||||
@Override
|
@Override
|
||||||
public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
|
public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
|
||||||
Policy policy = getPolicyStoreDelegate().create(representation, resourceServer);
|
Policy policy = getPolicyStoreDelegate().create(representation, resourceServer);
|
||||||
registerPolicyInvalidation(policy.getId(), policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), resourceServer.getId());
|
registerPolicyInvalidation(policy.getId(), representation.getName(), representation.getResources(), representation.getScopes(), resourceServer.getId());
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -637,8 +656,12 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
|
||||||
if (policy == null) return;
|
if (policy == null) return;
|
||||||
|
|
||||||
cache.invalidateObject(id);
|
cache.invalidateObject(id);
|
||||||
invalidationEvents.add(PolicyRemovedEvent.create(id, policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), policy.getResourceServer().getId()));
|
Set<String> resources = policy.getResources().stream().map(resource -> resource.getId()).collect(Collectors.toSet());
|
||||||
cache.policyRemoval(id, policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), policy.getResourceServer().getId(), invalidations);
|
ResourceServer resourceServer = policy.getResourceServer();
|
||||||
|
Set<String> resourceTypes = getResourceTypes(resources, resourceServer.getId());
|
||||||
|
Set<String> scopes = policy.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet());
|
||||||
|
invalidationEvents.add(PolicyRemovedEvent.create(id, policy.getName(), resources, resourceTypes, scopes, resourceServer.getId()));
|
||||||
|
cache.policyRemoval(id, policy.getName(), resources, resourceTypes, scopes, resourceServer.getId(), invalidations);
|
||||||
getPolicyStoreDelegate().delete(id);
|
getPolicyStoreDelegate().delete(id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,17 @@ public class PolicyRemovedEvent extends InvalidationEvent implements Authorizati
|
||||||
private String id;
|
private String id;
|
||||||
private String name;
|
private String name;
|
||||||
private Set<String> resources;
|
private Set<String> resources;
|
||||||
|
private Set<String> resourceTypes;
|
||||||
|
private Set<String> scopes;
|
||||||
private String serverId;
|
private String serverId;
|
||||||
|
|
||||||
public static PolicyRemovedEvent create(String id, String name, Set<String> resources, String serverId) {
|
public static PolicyRemovedEvent create(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId) {
|
||||||
PolicyRemovedEvent event = new PolicyRemovedEvent();
|
PolicyRemovedEvent event = new PolicyRemovedEvent();
|
||||||
event.id = id;
|
event.id = id;
|
||||||
event.name = name;
|
event.name = name;
|
||||||
event.resources = resources;
|
event.resources = resources;
|
||||||
|
event.resourceTypes = resourceTypes;
|
||||||
|
event.scopes = scopes;
|
||||||
event.serverId = serverId;
|
event.serverId = serverId;
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
@ -53,6 +57,6 @@ public class PolicyRemovedEvent extends InvalidationEvent implements Authorizati
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
|
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
|
||||||
cache.policyRemoval(id, name, resources, serverId, invalidations);
|
cache.policyRemoval(id, name, resources, resourceTypes, scopes, serverId, invalidations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,17 @@ public class PolicyUpdatedEvent extends InvalidationEvent implements Authorizati
|
||||||
private String id;
|
private String id;
|
||||||
private String name;
|
private String name;
|
||||||
private static Set<String> resources;
|
private static Set<String> resources;
|
||||||
|
private Set<String> resourceTypes;
|
||||||
|
private Set<String> scopes;
|
||||||
private String serverId;
|
private String serverId;
|
||||||
|
|
||||||
public static PolicyUpdatedEvent create(String id, String name, Set<String> resources, String serverId) {
|
public static PolicyUpdatedEvent create(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId) {
|
||||||
PolicyUpdatedEvent event = new PolicyUpdatedEvent();
|
PolicyUpdatedEvent event = new PolicyUpdatedEvent();
|
||||||
event.id = id;
|
event.id = id;
|
||||||
event.name = name;
|
event.name = name;
|
||||||
event.resources = resources;
|
event.resources = resources;
|
||||||
|
event.resourceTypes = resourceTypes;
|
||||||
|
event.scopes = scopes;
|
||||||
event.serverId = serverId;
|
event.serverId = serverId;
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
@ -53,6 +57,6 @@ public class PolicyUpdatedEvent extends InvalidationEvent implements Authorizati
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
|
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
|
||||||
cache.policyUpdated(id, name, resources, serverId, invalidations);
|
cache.policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.models.cache.infinispan;
|
package org.keycloak.models.cache.infinispan.events;
|
||||||
|
|
||||||
import org.keycloak.cluster.ClusterEvent;
|
import org.keycloak.cluster.ClusterEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
|
import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
|
|
@ -14,7 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.models.cache.infinispan;
|
package org.keycloak.models.cache.infinispan.events;
|
||||||
|
|
||||||
import org.keycloak.cluster.ClusterEvent;
|
import org.keycloak.cluster.ClusterEvent;
|
||||||
|
|
|
@ -19,8 +19,8 @@ package org.keycloak.models.sessions.infinispan;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
|
|
||||||
import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent;
|
import org.keycloak.models.cache.infinispan.events.AddInvalidatedActionTokenEvent;
|
||||||
import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent;
|
import org.keycloak.models.cache.infinispan.events.RemoveActionTokensSpecificEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
|
import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -58,7 +58,12 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
|
||||||
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(notes);
|
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(notes);
|
||||||
|
|
||||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||||
this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue), false);
|
AddInvalidatedActionTokenEvent event = new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue);
|
||||||
|
this.tx.notify(cluster, generateActionTokenEventId(), event, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateActionTokenEventId() {
|
||||||
|
return InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS + "/" + UUID.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -93,6 +98,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
|
||||||
}
|
}
|
||||||
|
|
||||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||||
this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new RemoveActionTokensSpecificEvent(userId, actionId), false);
|
this.tx.notify(cluster, generateActionTokenEventId(), new RemoveActionTokensSpecificEvent(userId, actionId), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,14 +23,16 @@ import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
|
|
||||||
import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent;
|
import org.keycloak.models.cache.infinispan.events.AddInvalidatedActionTokenEvent;
|
||||||
import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent;
|
import org.keycloak.models.cache.infinispan.events.RemoveActionTokensSpecificEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
|
import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.infinispan.context.Flag;
|
import org.infinispan.context.Flag;
|
||||||
|
import org.infinispan.remoting.transport.Address;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -38,6 +40,10 @@ import org.infinispan.context.Flag;
|
||||||
*/
|
*/
|
||||||
public class InfinispanActionTokenStoreProviderFactory implements ActionTokenStoreProviderFactory {
|
public class InfinispanActionTokenStoreProviderFactory implements ActionTokenStoreProviderFactory {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(InfinispanActionTokenStoreProviderFactory.class);
|
||||||
|
|
||||||
|
private volatile Cache<ActionTokenReducedKey, ActionTokenValueEntity> actionTokenCache;
|
||||||
|
|
||||||
public static final String ACTION_TOKEN_EVENTS = "ACTION_TOKEN_EVENTS";
|
public static final String ACTION_TOKEN_EVENTS = "ACTION_TOKEN_EVENTS";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,34 +55,7 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ActionTokenStoreProvider create(KeycloakSession session) {
|
public ActionTokenStoreProvider create(KeycloakSession session) {
|
||||||
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
return new InfinispanActionTokenStoreProvider(session, this.actionTokenCache);
|
||||||
Cache<ActionTokenReducedKey, ActionTokenValueEntity> actionTokenCache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE);
|
|
||||||
|
|
||||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
|
||||||
|
|
||||||
cluster.registerListener(ACTION_TOKEN_EVENTS, event -> {
|
|
||||||
if (event instanceof RemoveActionTokensSpecificEvent) {
|
|
||||||
RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
|
|
||||||
|
|
||||||
actionTokenCache
|
|
||||||
.getAdvancedCache()
|
|
||||||
.withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD)
|
|
||||||
.keySet()
|
|
||||||
.stream()
|
|
||||||
.filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
|
|
||||||
.forEach(actionTokenCache::remove);
|
|
||||||
} else if (event instanceof AddInvalidatedActionTokenEvent) {
|
|
||||||
AddInvalidatedActionTokenEvent e = (AddInvalidatedActionTokenEvent) event;
|
|
||||||
|
|
||||||
if (e.getExpirationInSecs() == DEFAULT_CACHE_EXPIRATION) {
|
|
||||||
actionTokenCache.put(e.getKey(), e.getTokenValue());
|
|
||||||
} else {
|
|
||||||
actionTokenCache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new InfinispanActionTokenStoreProvider(session, actionTokenCache);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -84,8 +63,57 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Cache<ActionTokenReducedKey, ActionTokenValueEntity> initActionTokenCache(KeycloakSession session) {
|
||||||
|
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
||||||
|
Cache<ActionTokenReducedKey, ActionTokenValueEntity> cache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE);
|
||||||
|
final Address cacheAddress = cache.getCacheManager().getAddress();
|
||||||
|
|
||||||
|
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||||
|
|
||||||
|
cluster.registerListener(ClusterProvider.ALL, event -> {
|
||||||
|
if (event instanceof RemoveActionTokensSpecificEvent) {
|
||||||
|
RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
|
||||||
|
|
||||||
|
LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId());
|
||||||
|
|
||||||
|
cache
|
||||||
|
.getAdvancedCache()
|
||||||
|
.withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD)
|
||||||
|
.keySet()
|
||||||
|
.stream()
|
||||||
|
.filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
|
||||||
|
.forEach(cache::remove);
|
||||||
|
} else if (event instanceof AddInvalidatedActionTokenEvent) {
|
||||||
|
AddInvalidatedActionTokenEvent e = (AddInvalidatedActionTokenEvent) event;
|
||||||
|
|
||||||
|
LOG.debugf("[%s] Invalidating token %s", cacheAddress, e.getKey());
|
||||||
|
if (e.getExpirationInSecs() == DEFAULT_CACHE_EXPIRATION) {
|
||||||
|
cache.put(e.getKey(), e.getTokenValue());
|
||||||
|
} else {
|
||||||
|
cache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LOG.debugf("[%s] Registered cluster listeners", cacheAddress);
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
Cache<ActionTokenReducedKey, ActionTokenValueEntity> cache = this.actionTokenCache;
|
||||||
|
|
||||||
|
// It is necessary to put the cache initialization here, otherwise the cache would be initialized lazily, that
|
||||||
|
// means also listeners will start only after first cache initialization - that would be too late
|
||||||
|
if (cache == null) {
|
||||||
|
synchronized (this) {
|
||||||
|
cache = this.actionTokenCache;
|
||||||
|
if (cache == null) {
|
||||||
|
this.actionTokenCache = initActionTokenCache(factory.create());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -92,7 +92,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||||
cluster.registerListener(AUTHENTICATION_SESSION_EVENTS, this::updateAuthNotes);
|
cluster.registerListener(AUTHENTICATION_SESSION_EVENTS, this::updateAuthNotes);
|
||||||
|
|
||||||
log.debug("Registered cluster listeners");
|
log.debugf("[%s] Registered cluster listeners", authSessionsCache.getCacheManager().getAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,11 @@ public class ActionTokenReducedKey implements Serializable {
|
||||||
&& Objects.equals(this.actionVerificationNonce, other.getActionVerificationNonce());
|
&& Objects.equals(this.actionVerificationNonce, other.getActionVerificationNonce());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "userId=" + userId + ", actionId=" + actionId + ", actionVerificationNonce=" + actionVerificationNonce;
|
||||||
|
}
|
||||||
|
|
||||||
public static class ExternalizerImpl implements Externalizer<ActionTokenReducedKey> {
|
public static class ExternalizerImpl implements Externalizer<ActionTokenReducedKey> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -53,7 +53,7 @@ public class ActionTokenValueEntity implements ActionTokenValueModel {
|
||||||
public void writeObject(ObjectOutput output, ActionTokenValueEntity t) throws IOException {
|
public void writeObject(ObjectOutput output, ActionTokenValueEntity t) throws IOException {
|
||||||
output.writeByte(VERSION_1);
|
output.writeByte(VERSION_1);
|
||||||
|
|
||||||
output.writeBoolean(! t.notes.isEmpty());
|
output.writeBoolean(t.notes.isEmpty());
|
||||||
if (! t.notes.isEmpty()) {
|
if (! t.notes.isEmpty()) {
|
||||||
output.writeObject(t.notes);
|
output.writeObject(t.notes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -355,6 +355,24 @@ public class JpaRealmProvider implements RealmProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GroupModel.GroupRemovedEvent event = new GroupModel.GroupRemovedEvent() {
|
||||||
|
@Override
|
||||||
|
public RealmModel getRealm() {
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GroupModel getGroup() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakSession getKeycloakSession() {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
session.getKeycloakSessionFactory().publish(event);
|
||||||
|
|
||||||
session.users().preRemove(realm, group);
|
session.users().preRemove(realm, group);
|
||||||
|
|
||||||
realm.removeDefaultGroup(group);
|
realm.removeDefaultGroup(group);
|
||||||
|
|
|
@ -21,14 +21,17 @@ package org.keycloak.authorization;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.authorization.model.Policy;
|
import org.keycloak.authorization.model.Policy;
|
||||||
|
import org.keycloak.authorization.model.Resource;
|
||||||
import org.keycloak.authorization.model.ResourceServer;
|
import org.keycloak.authorization.model.ResourceServer;
|
||||||
|
import org.keycloak.authorization.model.Scope;
|
||||||
import org.keycloak.authorization.permission.evaluator.Evaluators;
|
import org.keycloak.authorization.permission.evaluator.Evaluators;
|
||||||
import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
|
import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
|
||||||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||||
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
||||||
import org.keycloak.authorization.store.AuthorizationStoreFactory;
|
|
||||||
import org.keycloak.authorization.store.PolicyStore;
|
import org.keycloak.authorization.store.PolicyStore;
|
||||||
import org.keycloak.authorization.store.ResourceServerStore;
|
import org.keycloak.authorization.store.ResourceServerStore;
|
||||||
import org.keycloak.authorization.store.ResourceStore;
|
import org.keycloak.authorization.store.ResourceStore;
|
||||||
|
@ -37,7 +40,6 @@ import org.keycloak.authorization.store.StoreFactory;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
|
import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
|
||||||
import org.keycloak.models.cache.authorization.CachedStoreProviderFactory;
|
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
||||||
|
@ -143,6 +145,61 @@ public final class AuthorizationProvider implements Provider {
|
||||||
return new PolicyStore() {
|
return new PolicyStore() {
|
||||||
@Override
|
@Override
|
||||||
public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
|
public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
|
||||||
|
Set<String> resources = representation.getResources();
|
||||||
|
|
||||||
|
if (resources != null) {
|
||||||
|
representation.setResources(resources.stream().map(id -> {
|
||||||
|
Resource resource = getResourceStore().findById(id, resourceServer.getId());
|
||||||
|
|
||||||
|
if (resource == null) {
|
||||||
|
resource = getResourceStore().findByName(id, resourceServer.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource == null) {
|
||||||
|
throw new RuntimeException("Resource [" + id + "] does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource.getId();
|
||||||
|
}).collect(Collectors.toSet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> scopes = representation.getScopes();
|
||||||
|
|
||||||
|
if (scopes != null) {
|
||||||
|
representation.setScopes(scopes.stream().map(id -> {
|
||||||
|
Scope scope = getScopeStore().findById(id, resourceServer.getId());
|
||||||
|
|
||||||
|
if (scope == null) {
|
||||||
|
scope = getScopeStore().findByName(id, resourceServer.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope == null) {
|
||||||
|
throw new RuntimeException("Scope [" + id + "] does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return scope.getId();
|
||||||
|
}).collect(Collectors.toSet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Set<String> policies = representation.getPolicies();
|
||||||
|
|
||||||
|
if (policies != null) {
|
||||||
|
representation.setPolicies(policies.stream().map(id -> {
|
||||||
|
Policy policy = getPolicyStore().findById(id, resourceServer.getId());
|
||||||
|
|
||||||
|
if (policy == null) {
|
||||||
|
policy = getPolicyStore().findByName(id, resourceServer.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (policy == null) {
|
||||||
|
throw new RuntimeException("Policy [" + id + "] does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return policy.getId();
|
||||||
|
}).collect(Collectors.toSet()));
|
||||||
|
}
|
||||||
|
|
||||||
return RepresentationToModel.toModel(representation, AuthorizationProvider.this, policyStore.create(representation, resourceServer));
|
return RepresentationToModel.toModel(representation, AuthorizationProvider.this, policyStore.create(representation, resourceServer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
* JBoss, Home of Professional Open Source.
|
|
||||||
* Copyright 2016 Red Hat, Inc., and individual contributors
|
|
||||||
* as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.keycloak.authorization.permission.evaluator;
|
|
||||||
|
|
||||||
import org.keycloak.authorization.Decision;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
|
||||||
* @see PermissionEvaluator
|
|
||||||
*/
|
|
||||||
class DefaultPermissionEvaluator implements PermissionEvaluator {
|
|
||||||
|
|
||||||
private final PermissionEvaluator publisher;
|
|
||||||
|
|
||||||
DefaultPermissionEvaluator(PermissionEvaluator publisher) {
|
|
||||||
this.publisher = publisher;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void evaluate(Decision decision) {
|
|
||||||
publisher.evaluate(decision);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -42,6 +42,6 @@ public final class Evaluators {
|
||||||
}
|
}
|
||||||
|
|
||||||
public PermissionEvaluator schedule(List<ResourcePermission> permissions, EvaluationContext evaluationContext) {
|
public PermissionEvaluator schedule(List<ResourcePermission> permissions, EvaluationContext evaluationContext) {
|
||||||
return new DefaultPermissionEvaluator(new IterablePermissionEvaluator(permissions.iterator(), evaluationContext, this.policyEvaluator));
|
return new IterablePermissionEvaluator(permissions.iterator(), evaluationContext, this.policyEvaluator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,16 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.authorization.permission.evaluator;
|
package org.keycloak.authorization.permission.evaluator;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import org.keycloak.authorization.Decision;
|
import org.keycloak.authorization.Decision;
|
||||||
import org.keycloak.authorization.permission.ResourcePermission;
|
import org.keycloak.authorization.permission.ResourcePermission;
|
||||||
|
import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
|
||||||
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
|
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
|
||||||
import org.keycloak.authorization.policy.evaluation.PolicyEvaluator;
|
import org.keycloak.authorization.policy.evaluation.PolicyEvaluator;
|
||||||
|
import org.keycloak.authorization.policy.evaluation.Result;
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -50,4 +54,23 @@ class IterablePermissionEvaluator implements PermissionEvaluator {
|
||||||
decision.onError(cause);
|
decision.onError(cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Result> evaluate() {
|
||||||
|
AtomicReference<List<Result>> result = new AtomicReference<>();
|
||||||
|
|
||||||
|
evaluate(new DecisionResultCollector() {
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable cause) {
|
||||||
|
throw new RuntimeException("Failed to evaluate permissions", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onComplete(List<Result> results) {
|
||||||
|
result.set(results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.authorization.permission.evaluator;
|
package org.keycloak.authorization.permission.evaluator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.authorization.Decision;
|
import org.keycloak.authorization.Decision;
|
||||||
|
import org.keycloak.authorization.policy.evaluation.Result;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link PermissionEvaluator} represents a source of {@link org.keycloak.authorization.permission.ResourcePermission}, responsible for emitting these permissions
|
* An {@link PermissionEvaluator} represents a source of {@link org.keycloak.authorization.permission.ResourcePermission}, responsible for emitting these permissions
|
||||||
|
@ -28,4 +31,5 @@ import org.keycloak.authorization.Decision;
|
||||||
public interface PermissionEvaluator {
|
public interface PermissionEvaluator {
|
||||||
|
|
||||||
void evaluate(Decision decision);
|
void evaluate(Decision decision);
|
||||||
|
List<Result> evaluate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
package org.keycloak.authorization.policy.evaluation;
|
package org.keycloak.authorization.policy.evaluation;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -33,7 +34,7 @@ import org.keycloak.representations.idm.authorization.DecisionStrategy;
|
||||||
*/
|
*/
|
||||||
public abstract class DecisionResultCollector implements Decision<DefaultEvaluation> {
|
public abstract class DecisionResultCollector implements Decision<DefaultEvaluation> {
|
||||||
|
|
||||||
private Map<ResourcePermission, Result> results = new HashMap();
|
private Map<ResourcePermission, Result> results = new LinkedHashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDecision(DefaultEvaluation evaluation) {
|
public void onDecision(DefaultEvaluation evaluation) {
|
||||||
|
|
|
@ -60,7 +60,11 @@ public interface PolicyProviderFactory<R extends AbstractPolicyRepresentation> e
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
|
||||||
|
representation.setConfig(policy.getConfig());
|
||||||
|
}
|
||||||
|
|
||||||
default PolicyProviderAdminService getAdminResource(ResourceServer resourceServer, AuthorizationProvider authorization) {
|
default PolicyProviderAdminService getAdminResource(ResourceServer resourceServer, AuthorizationProvider authorization) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -37,12 +37,14 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
||||||
private final String providerId;
|
private final String providerId;
|
||||||
|
|
||||||
private final String pbkdf2Algorithm;
|
private final String pbkdf2Algorithm;
|
||||||
|
private int defaultIterations;
|
||||||
|
|
||||||
public static final int DERIVED_KEY_SIZE = 512;
|
public static final int DERIVED_KEY_SIZE = 512;
|
||||||
|
|
||||||
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm) {
|
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm, int defaultIterations) {
|
||||||
this.providerId = providerId;
|
this.providerId = providerId;
|
||||||
this.pbkdf2Algorithm = pbkdf2Algorithm;
|
this.pbkdf2Algorithm = pbkdf2Algorithm;
|
||||||
|
this.defaultIterations = defaultIterations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -52,6 +54,10 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void encode(String rawPassword, int iterations, CredentialModel credential) {
|
public void encode(String rawPassword, int iterations, CredentialModel credential) {
|
||||||
|
if (iterations == -1) {
|
||||||
|
iterations = defaultIterations;
|
||||||
|
}
|
||||||
|
|
||||||
byte[] salt = getSalt();
|
byte[] salt = getSalt();
|
||||||
String encodedPassword = encode(rawPassword, iterations, salt);
|
String encodedPassword = encode(rawPassword, iterations, salt);
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,11 @@ public class Pbkdf2PasswordHashProviderFactory implements PasswordHashProviderFa
|
||||||
|
|
||||||
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
|
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
|
||||||
|
|
||||||
|
public static final int DEFAULT_ITERATIONS = 20000;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PasswordHashProvider create(KeycloakSession session) {
|
public PasswordHashProvider create(KeycloakSession session) {
|
||||||
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM);
|
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, 20000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -15,9 +15,11 @@ public class Pbkdf2Sha256PasswordHashProviderFactory implements PasswordHashProv
|
||||||
|
|
||||||
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256";
|
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256";
|
||||||
|
|
||||||
|
public static final int DEFAULT_ITERATIONS = 27500;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PasswordHashProvider create(KeycloakSession session) {
|
public PasswordHashProvider create(KeycloakSession session) {
|
||||||
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM);
|
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -15,9 +15,11 @@ public class Pbkdf2Sha512PasswordHashProviderFactory implements PasswordHashProv
|
||||||
|
|
||||||
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA512";
|
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA512";
|
||||||
|
|
||||||
|
public static final int DEFAULT_ITERATIONS = 30000;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PasswordHashProvider create(KeycloakSession session) {
|
public PasswordHashProvider create(KeycloakSession session) {
|
||||||
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM);
|
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -156,5 +156,25 @@ public enum ResourceType {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
, COMPONENT;
|
, COMPONENT
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
, AUTHORIZATION_RESOURCE_SERVER
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
, AUTHORIZATION_RESOURCE
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
, AUTHORIZATION_SCOPE
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
, AUTHORIZATION_POLICY;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,17 +23,27 @@ import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
import org.keycloak.migration.ModelVersion;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.PasswordPolicy;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class MigrateTo3_2_0 implements Migration {
|
public class MigrateTo3_2_0 implements Migration {
|
||||||
public static final ModelVersion VERSION = new ModelVersion("3.1.0");
|
|
||||||
|
public static final ModelVersion VERSION = new ModelVersion("3.2.0");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void migrate(KeycloakSession session) {
|
public void migrate(KeycloakSession session) {
|
||||||
for (RealmModel realm : session.realms().getRealms()) {
|
for (RealmModel realm : session.realms().getRealms()) {
|
||||||
|
PasswordPolicy.Builder builder = realm.getPasswordPolicy().toBuilder();
|
||||||
|
if (!builder.contains(PasswordPolicy.HASH_ALGORITHM_ID) && "20000".equals(builder.get(PasswordPolicy.HASH_ITERATIONS_ID))) {
|
||||||
|
realm.setPasswordPolicy(builder.remove(PasswordPolicy.HASH_ITERATIONS_ID).build(session));
|
||||||
|
}
|
||||||
|
|
||||||
ClientModel realmAccess = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
|
ClientModel realmAccess = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
|
||||||
if (realmAccess != null) {
|
if (realmAccess != null) {
|
||||||
addRoles(realmAccess);
|
addRoles(realmAccess);
|
||||||
|
|
|
@ -771,11 +771,7 @@ public class ModelToRepresentation {
|
||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ScopeRepresentation toRepresentation(Scope model, AuthorizationProvider authorizationProvider) {
|
public static ScopeRepresentation toRepresentation(Scope model) {
|
||||||
return toRepresentation(model, authorizationProvider, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ScopeRepresentation toRepresentation(Scope model, AuthorizationProvider authorizationProvider, boolean deep) {
|
|
||||||
ScopeRepresentation scope = new ScopeRepresentation();
|
ScopeRepresentation scope = new ScopeRepresentation();
|
||||||
|
|
||||||
scope.setId(model.getId());
|
scope.setId(model.getId());
|
||||||
|
@ -798,6 +794,10 @@ public class ModelToRepresentation {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <R extends AbstractPolicyRepresentation> R toRepresentation(Policy policy, Class<R> representationType, AuthorizationProvider authorization) {
|
public static <R extends AbstractPolicyRepresentation> R toRepresentation(Policy policy, Class<R> representationType, AuthorizationProvider authorization) {
|
||||||
|
return toRepresentation(policy, representationType, authorization, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <R extends AbstractPolicyRepresentation> R toRepresentation(Policy policy, Class<R> representationType, AuthorizationProvider authorization, boolean export) {
|
||||||
R representation;
|
R representation;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -816,7 +816,11 @@ public class ModelToRepresentation {
|
||||||
representation.setLogic(policy.getLogic());
|
representation.setLogic(policy.getLogic());
|
||||||
|
|
||||||
if (representation instanceof PolicyRepresentation) {
|
if (representation instanceof PolicyRepresentation) {
|
||||||
PolicyRepresentation.class.cast(representation).setConfig(policy.getConfig());
|
if (providerFactory != null && export) {
|
||||||
|
providerFactory.onExport(policy, PolicyRepresentation.class.cast(representation), authorization);
|
||||||
|
} else {
|
||||||
|
PolicyRepresentation.class.cast(representation).setConfig(policy.getConfig());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
representation = (R) providerFactory.toRepresentation(policy, representation);
|
representation = (R) providerFactory.toRepresentation(policy, representation);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2171,13 +2171,13 @@ public class RepresentationToModel {
|
||||||
private static void updateResources(Set<String> resourceIds, Policy policy, StoreFactory storeFactory) {
|
private static void updateResources(Set<String> resourceIds, Policy policy, StoreFactory storeFactory) {
|
||||||
if (resourceIds != null) {
|
if (resourceIds != null) {
|
||||||
if (resourceIds.isEmpty()) {
|
if (resourceIds.isEmpty()) {
|
||||||
for (Scope scope : new HashSet<Scope>(policy.getScopes())) {
|
for (Resource resource : new HashSet<>(policy.getResources())) {
|
||||||
policy.removeScope(scope);
|
policy.removeResource(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (String resourceId : resourceIds) {
|
for (String resourceId : resourceIds) {
|
||||||
boolean hasResource = false;
|
boolean hasResource = false;
|
||||||
for (Resource resourceModel : new HashSet<Resource>(policy.getResources())) {
|
for (Resource resourceModel : new HashSet<>(policy.getResources())) {
|
||||||
if (resourceModel.getId().equals(resourceId) || resourceModel.getName().equals(resourceId)) {
|
if (resourceModel.getId().equals(resourceId) || resourceModel.getName().equals(resourceId)) {
|
||||||
hasResource = true;
|
hasResource = true;
|
||||||
}
|
}
|
||||||
|
@ -2196,7 +2196,7 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Resource resourceModel : new HashSet<Resource>(policy.getResources())) {
|
for (Resource resourceModel : new HashSet<>(policy.getResources())) {
|
||||||
boolean hasResource = false;
|
boolean hasResource = false;
|
||||||
|
|
||||||
for (String resourceId : resourceIds) {
|
for (String resourceId : resourceIds) {
|
||||||
|
|
|
@ -60,7 +60,7 @@ public class HashIterationsPasswordPolicyProviderFactory implements PasswordPoli
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object parseConfig(String value) {
|
public Object parseConfig(String value) {
|
||||||
return value != null ? Integer.parseInt(value) : PasswordPolicy.HASH_ITERATIONS_DEFAULT;
|
return parseInteger(value, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.models;
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import org.keycloak.provider.ProviderEvent;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -26,6 +28,11 @@ import java.util.Set;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public interface GroupModel extends RoleMapperModel {
|
public interface GroupModel extends RoleMapperModel {
|
||||||
|
interface GroupRemovedEvent extends ProviderEvent {
|
||||||
|
RealmModel getRealm();
|
||||||
|
GroupModel getGroup();
|
||||||
|
KeycloakSession getKeycloakSession();
|
||||||
|
}
|
||||||
String getId();
|
String getId();
|
||||||
|
|
||||||
String getName();
|
String getName();
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.keycloak.policy.PasswordPolicyProvider;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -32,62 +33,33 @@ public class PasswordPolicy implements Serializable {
|
||||||
|
|
||||||
public static final String HASH_ALGORITHM_ID = "hashAlgorithm";
|
public static final String HASH_ALGORITHM_ID = "hashAlgorithm";
|
||||||
|
|
||||||
public static final String HASH_ALGORITHM_DEFAULT = "pbkdf2";
|
public static final String HASH_ALGORITHM_DEFAULT = "pbkdf2-sha256";
|
||||||
|
|
||||||
public static final String HASH_ITERATIONS_ID = "hashIterations";
|
public static final String HASH_ITERATIONS_ID = "hashIterations";
|
||||||
|
|
||||||
public static final int HASH_ITERATIONS_DEFAULT = 20000;
|
public static final int HASH_ITERATIONS_DEFAULT = 27500;
|
||||||
|
|
||||||
public static final String PASSWORD_HISTORY_ID = "passwordHistory";
|
public static final String PASSWORD_HISTORY_ID = "passwordHistory";
|
||||||
|
|
||||||
public static final String FORCE_EXPIRED_ID = "forceExpiredPasswordChange";
|
public static final String FORCE_EXPIRED_ID = "forceExpiredPasswordChange";
|
||||||
|
|
||||||
private String policyString;
|
|
||||||
private Map<String, Object> policyConfig;
|
private Map<String, Object> policyConfig;
|
||||||
|
private Builder builder;
|
||||||
|
|
||||||
public static PasswordPolicy empty() {
|
public static PasswordPolicy empty() {
|
||||||
return new PasswordPolicy(null, new HashMap<>());
|
return new PasswordPolicy(null, new HashMap<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PasswordPolicy parse(KeycloakSession session, String policyString) {
|
public static Builder build() {
|
||||||
Map<String, Object> policyConfig = new HashMap<>();
|
return new Builder();
|
||||||
|
|
||||||
if (policyString != null && !policyString.trim().isEmpty()) {
|
|
||||||
for (String policy : policyString.split(" and ")) {
|
|
||||||
policy = policy.trim();
|
|
||||||
|
|
||||||
String key;
|
|
||||||
String config = null;
|
|
||||||
|
|
||||||
int i = policy.indexOf('(');
|
|
||||||
if (i == -1) {
|
|
||||||
key = policy.trim();
|
|
||||||
} else {
|
|
||||||
key = policy.substring(0, i).trim();
|
|
||||||
config = policy.substring(i + 1, policy.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, key);
|
|
||||||
if (provider == null) {
|
|
||||||
throw new PasswordPolicyConfigException("Password policy not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
Object o;
|
|
||||||
try {
|
|
||||||
o = provider.parseConfig(config);
|
|
||||||
} catch (PasswordPolicyConfigException e) {
|
|
||||||
throw new ModelException("Invalid config for " + key + ": " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
policyConfig.put(key, o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PasswordPolicy(policyString, policyConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PasswordPolicy(String policyString, Map<String, Object> policyConfig) {
|
public static PasswordPolicy parse(KeycloakSession session, String policyString) {
|
||||||
this.policyString = policyString;
|
return new Builder(policyString).build(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PasswordPolicy(Builder builder, Map<String, Object> policyConfig) {
|
||||||
|
this.builder = builder;
|
||||||
this.policyConfig = policyConfig;
|
this.policyConfig = policyConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +83,7 @@ public class PasswordPolicy implements Serializable {
|
||||||
if (policyConfig.containsKey(HASH_ITERATIONS_ID)) {
|
if (policyConfig.containsKey(HASH_ITERATIONS_ID)) {
|
||||||
return getPolicyConfig(HASH_ITERATIONS_ID);
|
return getPolicyConfig(HASH_ITERATIONS_ID);
|
||||||
} else {
|
} else {
|
||||||
return HASH_ITERATIONS_DEFAULT;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +105,117 @@ public class PasswordPolicy implements Serializable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return policyString;
|
return builder.asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder toBuilder() {
|
||||||
|
return builder.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private LinkedHashMap<String, String> map;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
this.map = new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder(LinkedHashMap<String, String> map) {
|
||||||
|
this.map = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder(String policyString) {
|
||||||
|
map = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
if (policyString != null && !policyString.trim().isEmpty()) {
|
||||||
|
for (String policy : policyString.split(" and ")) {
|
||||||
|
policy = policy.trim();
|
||||||
|
|
||||||
|
String key;
|
||||||
|
String config = null;
|
||||||
|
|
||||||
|
int i = policy.indexOf('(');
|
||||||
|
if (i == -1) {
|
||||||
|
key = policy.trim();
|
||||||
|
} else {
|
||||||
|
key = policy.substring(0, i).trim();
|
||||||
|
config = policy.substring(i + 1, policy.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.put(key, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(String key) {
|
||||||
|
return map.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String get(String key) {
|
||||||
|
return map.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String key, String value) {
|
||||||
|
map.put(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder remove(String key) {
|
||||||
|
map.remove(key);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordPolicy build(KeycloakSession session) {
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
for (Map.Entry<String, String> e : map.entrySet()) {
|
||||||
|
|
||||||
|
PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, e.getKey());
|
||||||
|
if (provider == null) {
|
||||||
|
throw new PasswordPolicyConfigException("Password policy not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Object o;
|
||||||
|
try {
|
||||||
|
o = provider.parseConfig(e.getValue());
|
||||||
|
} catch (PasswordPolicyConfigException ex) {
|
||||||
|
throw new ModelException("Invalid config for " + e.getKey() + ": " + ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
config.put(e.getKey(), o);
|
||||||
|
}
|
||||||
|
return new PasswordPolicy(this, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String asString() {
|
||||||
|
if (map.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
boolean first = true;
|
||||||
|
for (Map.Entry<String, String> e : map.entrySet()) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
sb.append(" and ");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(e.getKey());
|
||||||
|
|
||||||
|
String c = e.getValue();
|
||||||
|
if (c != null && !c.trim().isEmpty()) {
|
||||||
|
sb.append("(");
|
||||||
|
sb.append(c);
|
||||||
|
sb.append(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clone() {
|
||||||
|
return new Builder((LinkedHashMap<String, String>) map.clone());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ import java.util.Map;
|
||||||
* <li>{@code realm} the {@link RealmModel}</li>
|
* <li>{@code realm} the {@link RealmModel}</li>
|
||||||
* <li>{@code user} the current {@link UserModel}</li>
|
* <li>{@code user} the current {@link UserModel}</li>
|
||||||
* <li>{@code session} the active {@link KeycloakSession}</li>
|
* <li>{@code session} the active {@link KeycloakSession}</li>
|
||||||
* <li>{@code clientSession} the current {@link org.keycloak.sessions.AuthenticationSessionModel}</li>
|
* <li>{@code authenticationSession} the current {@link org.keycloak.sessions.AuthenticationSessionModel}</li>
|
||||||
* <li>{@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}</li>
|
* <li>{@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}</li>
|
||||||
* <li>{@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
|
* <li>{@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
|
||||||
* </ol>
|
* </ol>
|
||||||
|
@ -160,7 +160,7 @@ public class ScriptBasedAuthenticator implements Authenticator {
|
||||||
bindings.put("user", context.getUser());
|
bindings.put("user", context.getUser());
|
||||||
bindings.put("session", context.getSession());
|
bindings.put("session", context.getSession());
|
||||||
bindings.put("httpRequest", context.getHttpRequest());
|
bindings.put("httpRequest", context.getHttpRequest());
|
||||||
bindings.put("clientSession", context.getAuthenticationSession());
|
bindings.put("authenticationSession", context.getAuthenticationSession());
|
||||||
bindings.put("LOG", LOGGER);
|
bindings.put("LOG", LOGGER);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En
|
||||||
}
|
}
|
||||||
script.setDefaultValue(scriptTemplate);
|
script.setDefaultValue(scriptTemplate);
|
||||||
script.setHelpText("The script used to authenticate. Scripts must at least define a function with the name 'authenticate(context)' that accepts a context (AuthenticationFlowContext) parameter.\n" +
|
script.setHelpText("The script used to authenticate. Scripts must at least define a function with the name 'authenticate(context)' that accepts a context (AuthenticationFlowContext) parameter.\n" +
|
||||||
"This authenticator exposes the following additional variables: 'script', 'realm', 'user', 'session', 'httpRequest', 'LOG'");
|
"This authenticator exposes the following additional variables: 'script', 'realm', 'user', 'session', 'authenticationSession', 'httpRequest', 'LOG'");
|
||||||
|
|
||||||
return asList(name, description, script);
|
return asList(name, description, script);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.authorization.model.ResourceServer;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
||||||
|
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
|
||||||
|
@ -37,27 +38,29 @@ public class AuthorizationService {
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private final ResourceServer resourceServer;
|
private final ResourceServer resourceServer;
|
||||||
private final AuthorizationProvider authorization;
|
private final AuthorizationProvider authorization;
|
||||||
|
private final AdminEventBuilder adminEvent;
|
||||||
|
|
||||||
public AuthorizationService(KeycloakSession session, ClientModel client, AdminPermissionEvaluator auth) {
|
public AuthorizationService(KeycloakSession session, ClientModel client, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.authorization = session.getProvider(AuthorizationProvider.class);
|
this.authorization = session.getProvider(AuthorizationProvider.class);
|
||||||
|
this.adminEvent = adminEvent;
|
||||||
this.resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findByClient(this.client.getId());
|
this.resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findByClient(this.client.getId());
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("/resource-server")
|
@Path("/resource-server")
|
||||||
public ResourceServerService resourceServer() {
|
public ResourceServerService resourceServer() {
|
||||||
ResourceServerService resource = new ResourceServerService(this.authorization, this.resourceServer, this.client, this.auth);
|
ResourceServerService resource = new ResourceServerService(this.authorization, this.resourceServer, this.client, this.auth, adminEvent);
|
||||||
|
|
||||||
ResteasyProviderFactory.getInstance().injectProperties(resource);
|
ResteasyProviderFactory.getInstance().injectProperties(resource);
|
||||||
|
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enable() {
|
public void enable(boolean newClient) {
|
||||||
if (!isEnabled()) {
|
if (!isEnabled()) {
|
||||||
resourceServer().create();
|
resourceServer().create(newClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,24 +23,25 @@ import org.keycloak.authorization.AuthorizationProvider;
|
||||||
import org.keycloak.authorization.model.Policy;
|
import org.keycloak.authorization.model.Policy;
|
||||||
import org.keycloak.authorization.model.ResourceServer;
|
import org.keycloak.authorization.model.ResourceServer;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
public class PermissionService extends PolicyService {
|
public class PermissionService extends PolicyService {
|
||||||
|
|
||||||
public PermissionService(ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth) {
|
public PermissionService(ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
super(resourceServer, authorization, auth);
|
super(resourceServer, authorization, auth, adminEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected PolicyResourceService doCreatePolicyResource(Policy policy) {
|
protected PolicyResourceService doCreatePolicyResource(Policy policy) {
|
||||||
return new PolicyTypeResourceService(policy, resourceServer, authorization, auth);
|
return new PolicyTypeResourceService(policy, resourceServer, authorization, auth, adminEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected PolicyTypeService doCreatePolicyTypeResource(String type) {
|
protected PolicyTypeService doCreatePolicyTypeResource(String type) {
|
||||||
return new PolicyTypeService(type, resourceServer, authorization, auth) {
|
return new PolicyTypeService(type, resourceServer, authorization, auth, adminEvent) {
|
||||||
@Override
|
@Override
|
||||||
protected List<Object> doSearch(Integer firstResult, Integer maxResult, Map<String, String[]> filters) {
|
protected List<Object> doSearch(Integer firstResult, Integer maxResult, Map<String, String[]> filters) {
|
||||||
filters.put("permission", new String[] {Boolean.TRUE.toString()});
|
filters.put("permission", new String[] {Boolean.TRUE.toString()});
|
||||||
|
|
|
@ -35,12 +35,11 @@ import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.authorization.AuthorizationProvider;
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
import org.keycloak.authorization.util.Permissions;
|
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
|
||||||
import org.keycloak.representations.idm.authorization.PolicyEvaluationRequest;
|
|
||||||
import org.keycloak.authorization.admin.representation.PolicyEvaluationResponseBuilder;
|
import org.keycloak.authorization.admin.representation.PolicyEvaluationResponseBuilder;
|
||||||
import org.keycloak.authorization.attribute.Attributes;
|
import org.keycloak.authorization.attribute.Attributes;
|
||||||
import org.keycloak.authorization.common.KeycloakEvaluationContext;
|
import org.keycloak.authorization.common.KeycloakEvaluationContext;
|
||||||
|
@ -54,6 +53,7 @@ import org.keycloak.authorization.policy.evaluation.EvaluationContext;
|
||||||
import org.keycloak.authorization.policy.evaluation.Result;
|
import org.keycloak.authorization.policy.evaluation.Result;
|
||||||
import org.keycloak.authorization.store.ScopeStore;
|
import org.keycloak.authorization.store.ScopeStore;
|
||||||
import org.keycloak.authorization.store.StoreFactory;
|
import org.keycloak.authorization.store.StoreFactory;
|
||||||
|
import org.keycloak.authorization.util.Permissions;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -61,11 +61,14 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.idm.authorization.PolicyEvaluationRequest;
|
||||||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||||
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
@ -118,18 +121,18 @@ public class PolicyEvaluationService {
|
||||||
this.auth.realm().requireViewAuthorization();
|
this.auth.realm().requireViewAuthorization();
|
||||||
CloseableKeycloakIdentity identity = createIdentity(evaluationRequest);
|
CloseableKeycloakIdentity identity = createIdentity(evaluationRequest);
|
||||||
try {
|
try {
|
||||||
EvaluationContext evaluationContext = createEvaluationContext(evaluationRequest, identity);
|
return Response.ok(PolicyEvaluationResponseBuilder.build(evaluate(evaluationRequest, createEvaluationContext(evaluationRequest, identity)), resourceServer, authorization, identity)).build();
|
||||||
Decision decisionCollector = new Decision();
|
} catch (Exception e) {
|
||||||
authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization), evaluationContext).evaluate(decisionCollector);
|
throw new ErrorResponseException(OAuthErrorException.SERVER_ERROR, "Error while evaluating permissions.", Status.INTERNAL_SERVER_ERROR);
|
||||||
if (decisionCollector.error != null) {
|
|
||||||
throw decisionCollector.error;
|
|
||||||
}
|
|
||||||
return Response.ok(PolicyEvaluationResponseBuilder.build(decisionCollector.results, resourceServer, authorization, identity)).build();
|
|
||||||
} finally {
|
} finally {
|
||||||
identity.close();
|
identity.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Result> evaluate(PolicyEvaluationRequest evaluationRequest, EvaluationContext evaluationContext) {
|
||||||
|
return authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization), evaluationContext).evaluate();
|
||||||
|
}
|
||||||
|
|
||||||
private EvaluationContext createEvaluationContext(PolicyEvaluationRequest representation, KeycloakIdentity identity) {
|
private EvaluationContext createEvaluationContext(PolicyEvaluationRequest representation, KeycloakIdentity identity) {
|
||||||
return new KeycloakEvaluationContext(identity, this.authorization.getKeycloakSession()) {
|
return new KeycloakEvaluationContext(identity, this.authorization.getKeycloakSession()) {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -26,8 +26,10 @@ import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.keycloak.authorization.AuthorizationProvider;
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
|
@ -36,6 +38,8 @@ import org.keycloak.authorization.model.ResourceServer;
|
||||||
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
||||||
import org.keycloak.authorization.store.PolicyStore;
|
import org.keycloak.authorization.store.PolicyStore;
|
||||||
import org.keycloak.authorization.store.StoreFactory;
|
import org.keycloak.authorization.store.StoreFactory;
|
||||||
|
import org.keycloak.events.admin.OperationType;
|
||||||
|
import org.keycloak.events.admin.ResourceType;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
||||||
|
@ -43,6 +47,7 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,19 +59,21 @@ public class PolicyResourceService {
|
||||||
protected final ResourceServer resourceServer;
|
protected final ResourceServer resourceServer;
|
||||||
protected final AuthorizationProvider authorization;
|
protected final AuthorizationProvider authorization;
|
||||||
protected final AdminPermissionEvaluator auth;
|
protected final AdminPermissionEvaluator auth;
|
||||||
|
private final AdminEventBuilder adminEvent;
|
||||||
|
|
||||||
public PolicyResourceService(Policy policy, ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth) {
|
public PolicyResourceService(Policy policy, ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
this.policy = policy;
|
this.policy = policy;
|
||||||
this.resourceServer = resourceServer;
|
this.resourceServer = resourceServer;
|
||||||
this.authorization = authorization;
|
this.authorization = authorization;
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
|
this.adminEvent = adminEvent.resource(ResourceType.AUTHORIZATION_POLICY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes("application/json")
|
@Consumes("application/json")
|
||||||
@Produces("application/json")
|
@Produces("application/json")
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response update(String payload) {
|
public Response update(@Context UriInfo uriInfo,String payload) {
|
||||||
this.auth.realm().requireManageAuthorization();
|
this.auth.realm().requireManageAuthorization();
|
||||||
|
|
||||||
AbstractPolicyRepresentation representation = doCreateRepresentation(payload);
|
AbstractPolicyRepresentation representation = doCreateRepresentation(payload);
|
||||||
|
@ -79,11 +86,14 @@ public class PolicyResourceService {
|
||||||
|
|
||||||
RepresentationToModel.toModel(representation, authorization, policy);
|
RepresentationToModel.toModel(representation, authorization, policy);
|
||||||
|
|
||||||
|
|
||||||
|
audit(uriInfo, representation, OperationType.UPDATE);
|
||||||
|
|
||||||
return Response.status(Status.CREATED).build();
|
return Response.status(Status.CREATED).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
public Response delete() {
|
public Response delete(@Context UriInfo uriInfo) {
|
||||||
this.auth.realm().requireManageAuthorization();
|
this.auth.realm().requireManageAuthorization();
|
||||||
|
|
||||||
if (policy == null) {
|
if (policy == null) {
|
||||||
|
@ -98,6 +108,10 @@ public class PolicyResourceService {
|
||||||
|
|
||||||
policyStore.delete(policy.getId());
|
policyStore.delete(policy.getId());
|
||||||
|
|
||||||
|
if (authorization.getRealm().isAdminEventsEnabled()) {
|
||||||
|
audit(uriInfo, toRepresentation(policy, authorization), OperationType.DELETE);
|
||||||
|
}
|
||||||
|
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,4 +239,10 @@ public class PolicyResourceService {
|
||||||
protected Policy getPolicy() {
|
protected Policy getPolicy() {
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void audit(@Context UriInfo uriInfo, AbstractPolicyRepresentation policy, OperationType operation) {
|
||||||
|
if (authorization.getRealm().isAdminEventsEnabled()) {
|
||||||
|
adminEvent.operation(operation).resourcePath(uriInfo).representation(policy).success();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,11 @@ import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
|
@ -46,13 +48,17 @@ import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
|
||||||
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
||||||
import org.keycloak.authorization.store.PolicyStore;
|
import org.keycloak.authorization.store.PolicyStore;
|
||||||
import org.keycloak.authorization.store.StoreFactory;
|
import org.keycloak.authorization.store.StoreFactory;
|
||||||
|
import org.keycloak.events.admin.OperationType;
|
||||||
|
import org.keycloak.events.admin.ResourceType;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
|
import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||||
|
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,11 +69,13 @@ public class PolicyService {
|
||||||
protected final ResourceServer resourceServer;
|
protected final ResourceServer resourceServer;
|
||||||
protected final AuthorizationProvider authorization;
|
protected final AuthorizationProvider authorization;
|
||||||
protected final AdminPermissionEvaluator auth;
|
protected final AdminPermissionEvaluator auth;
|
||||||
|
protected final AdminEventBuilder adminEvent;
|
||||||
|
|
||||||
public PolicyService(ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth) {
|
public PolicyService(ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
this.resourceServer = resourceServer;
|
this.resourceServer = resourceServer;
|
||||||
this.authorization = authorization;
|
this.authorization = authorization;
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
|
this.adminEvent = adminEvent.resource(ResourceType.AUTHORIZATION_POLICY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("{type}")
|
@Path("{type}")
|
||||||
|
@ -84,18 +92,18 @@ public class PolicyService {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected PolicyTypeService doCreatePolicyTypeResource(String type) {
|
protected PolicyTypeService doCreatePolicyTypeResource(String type) {
|
||||||
return new PolicyTypeService(type, resourceServer, authorization, auth);
|
return new PolicyTypeService(type, resourceServer, authorization, auth, adminEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Object doCreatePolicyResource(Policy policy) {
|
protected Object doCreatePolicyResource(Policy policy) {
|
||||||
return new PolicyResourceService(policy, resourceServer, authorization, auth);
|
return new PolicyResourceService(policy, resourceServer, authorization, auth, adminEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response create(String payload) {
|
public Response create(@Context UriInfo uriInfo, String payload) {
|
||||||
this.auth.realm().requireManageAuthorization();
|
this.auth.realm().requireManageAuthorization();
|
||||||
|
|
||||||
AbstractPolicyRepresentation representation = doCreateRepresentation(payload);
|
AbstractPolicyRepresentation representation = doCreateRepresentation(payload);
|
||||||
|
@ -103,6 +111,8 @@ public class PolicyService {
|
||||||
|
|
||||||
representation.setId(policy.getId());
|
representation.setId(policy.getId());
|
||||||
|
|
||||||
|
audit(uriInfo, representation, representation.getId(), OperationType.CREATE);
|
||||||
|
|
||||||
return Response.status(Status.CREATED).entity(representation).build();
|
return Response.status(Status.CREATED).entity(representation).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,4 +290,14 @@ public class PolicyService {
|
||||||
findAssociatedPolicies(associated, policies);
|
findAssociatedPolicies(associated, policies);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void audit(@Context UriInfo uriInfo, AbstractPolicyRepresentation resource, String id, OperationType operation) {
|
||||||
|
if (authorization.getRealm().isAdminEventsEnabled()) {
|
||||||
|
if (id != null) {
|
||||||
|
adminEvent.operation(operation).resourcePath(uriInfo, id).representation(resource).success();
|
||||||
|
} else {
|
||||||
|
adminEvent.operation(operation).resourcePath(uriInfo).representation(resource).success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue