app importer
This commit is contained in:
parent
971f0f5c16
commit
e3609cc85b
22 changed files with 574 additions and 20 deletions
|
@ -610,10 +610,26 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
applications : function(ApplicationListLoader) {
|
||||
return ApplicationListLoader();
|
||||
},
|
||||
serverInfo : function(ServerInfoLoader) {
|
||||
return ServerInfoLoader();
|
||||
}
|
||||
|
||||
},
|
||||
controller : 'ApplicationListCtrl'
|
||||
})
|
||||
.when('/import/application/:realm', {
|
||||
templateUrl : 'partials/application-import.html',
|
||||
resolve : {
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
},
|
||||
serverInfo : function(ServerInfoLoader) {
|
||||
return ServerInfoLoader();
|
||||
}
|
||||
},
|
||||
controller : 'ApplicationImportCtrl'
|
||||
})
|
||||
|
||||
// OAUTH Client
|
||||
|
||||
|
|
|
@ -366,9 +366,62 @@ module.controller('ApplicationRoleDetailCtrl', function($scope, realm, applicati
|
|||
|
||||
});
|
||||
|
||||
module.controller('ApplicationListCtrl', function($scope, realm, applications, Application, $location) {
|
||||
module.controller('ApplicationImportCtrl', function($scope, $location, $upload, realm, serverInfo, Notifications) {
|
||||
|
||||
$scope.realm = realm;
|
||||
$scope.configFormats = serverInfo.applicationImporters;
|
||||
$scope.configFormat = null;
|
||||
|
||||
$scope.files = [];
|
||||
|
||||
$scope.onFileSelect = function($files) {
|
||||
$scope.files = $files;
|
||||
};
|
||||
|
||||
$scope.clearFileSelect = function() {
|
||||
$scope.files = null;
|
||||
}
|
||||
|
||||
$scope.uploadFile = function() {
|
||||
//$files: an array of files selected, each file has name, size, and type.
|
||||
for (var i = 0; i < $scope.files.length; i++) {
|
||||
var $file = $scope.files[i];
|
||||
$scope.upload = $upload.upload({
|
||||
url: authUrl + '/admin/realms/' + realm.realm + '/application-importers/' + $scope.configFormat.id + '/upload',
|
||||
// method: POST or PUT,
|
||||
// headers: {'headerKey': 'headerValue'}, withCredential: true,
|
||||
data: {myObj: ""},
|
||||
file: $file
|
||||
/* set file formData name for 'Content-Desposition' header. Default: 'file' */
|
||||
//fileFormDataName: myFile,
|
||||
/* customize how data is added to formData. See #40#issuecomment-28612000 for example */
|
||||
//formDataAppender: function(formData, key, val){}
|
||||
}).progress(function(evt) {
|
||||
console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
|
||||
}).success(function(data, status, headers) {
|
||||
Notifications.success("Uploaded successfully.");
|
||||
$location.url("/realms/" + realm.realm + "/applications");
|
||||
})
|
||||
.error(function() {
|
||||
Notifications.error("The file can not be uploaded. Please verify the file.");
|
||||
|
||||
});
|
||||
//.then(success, error, progress);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch(function() {
|
||||
return $location.path();
|
||||
}, function() {
|
||||
$scope.path = $location.path().substring(1).split("/");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.controller('ApplicationListCtrl', function($scope, realm, applications, Application, serverInfo, $location) {
|
||||
$scope.realm = realm;
|
||||
$scope.applications = applications;
|
||||
$scope.importButton = serverInfo.applicationImporters.length > 0;
|
||||
$scope.$watch(function() {
|
||||
return $location.path();
|
||||
}, function() {
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<div class="bs-sidebar col-md-3 clearfix" data-ng-include data-src="'partials/realm-menu.html'"></div>
|
||||
<div id="content-area" class="col-md-9" role="main">
|
||||
<ul class="nav nav-tabs nav-tabs-pf">
|
||||
<li class="active"><a href="">Application Import</a></li>
|
||||
</ul>
|
||||
<h2></h2>
|
||||
<div id="content">
|
||||
<h2><span>{{application.name}}</span> Application Import <span tooltip-placement="right" tooltip="Helper utility for importing application definitions from various formats." class="fa fa-info-circle"></span></h2>
|
||||
<form class="form-horizontal" name="realmForm" novalidate>
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group input-select">
|
||||
<label class="col-sm-2 control-label" for="configFormats">Format Option</label>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<div class="select-kc">
|
||||
<select id="configFormats" name="configFormats" ng-model="configFormat" ng-options="format.name for format in configFormats">
|
||||
<option value="" selected> Select a Format </option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Import File </label>
|
||||
<div class="col-sm-4">
|
||||
<div class="controls kc-button-input-file" data-ng-show="!files || files.length == 0">
|
||||
<a href="#" class="btn btn-default"><span class="kc-icon-upload">Icon: Upload</span>Choose a File...</a>
|
||||
<input id="import-file" type="file" class="transparent" ng-file-select="onFileSelect($files)">
|
||||
</div>
|
||||
<span class="kc-uploaded-file" data-ng-show="files.length > 0">
|
||||
{{files[0].name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right form-actions" data-ng-show="files.length > 0">
|
||||
<button type="submit" data-ng-click="clearFileSelect()" class="btn btn-lg btn-default">Cancel</button>
|
||||
<button type="submit" data-ng-click="uploadFile()" class="btn btn-lg btn-primary">Import</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -19,7 +19,8 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-primary" href="#/create/application/{{realm.realm}}">Add Application</a>
|
||||
<a class="btn btn-primary" href="#/import/application/{{realm.realm}}" data-ng-show="importButton">Import</a>
|
||||
<a class="btn btn-primary" href="#/create/application/{{realm.realm}}">Create</a>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-multipart-provider</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.keycloak.exportimport.ApplicationImporter;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.resources.admin.RealmAuth;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class EntityDescriptorImporter implements ApplicationImporter {
|
||||
@Override
|
||||
public Object createJaxrsService(RealmModel realm, RealmAuth auth) {
|
||||
return new EntityDescriptorImporterService(realm, auth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.exportimport.ApplicationImporter;
|
||||
import org.keycloak.exportimport.ApplicationImporterFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class EntityDescriptorImporterFactory implements ApplicationImporterFactory {
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "SAML 2.0 Entity Descriptor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApplicationImporter create(KeycloakSession session) {
|
||||
return new EntityDescriptorImporter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "saml2-entity-descriptor";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
|
||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.services.resources.admin.RealmAuth;
|
||||
import org.picketlink.common.constants.JBossSAMLURIConstants;
|
||||
import org.picketlink.common.exceptions.ConfigurationException;
|
||||
import org.picketlink.common.exceptions.ParsingException;
|
||||
import org.picketlink.common.exceptions.ProcessingException;
|
||||
import org.picketlink.identity.federation.core.parsers.saml.SAMLParser;
|
||||
import org.picketlink.identity.federation.core.saml.v2.util.SAMLMetadataUtil;
|
||||
import org.picketlink.identity.federation.core.util.CoreConfigUtil;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EndpointType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EntitiesDescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EntityDescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.KeyDescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.KeyTypes;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.SPSSODescriptorType;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class EntityDescriptorImporterService {
|
||||
protected RealmModel realm;
|
||||
protected RealmAuth auth;
|
||||
|
||||
public EntityDescriptorImporterService(RealmModel realm, RealmAuth auth) {
|
||||
this.realm = realm;
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("upload")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public void updateEntityDescriptor(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
|
||||
auth.requireManage();
|
||||
|
||||
Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
|
||||
List<InputPart> inputParts = uploadForm.get("file");
|
||||
|
||||
InputStream is = inputParts.get(0).getBody(InputStream.class, null);
|
||||
|
||||
loadEntityDescriptors(is, realm);
|
||||
|
||||
}
|
||||
|
||||
public static void loadEntityDescriptors(InputStream is, RealmModel realm) {
|
||||
Object metadata = null;
|
||||
try {
|
||||
metadata = new SAMLParser().parse(is);
|
||||
} catch (ParsingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
EntitiesDescriptorType entities;
|
||||
|
||||
if (EntitiesDescriptorType.class.isInstance(metadata)) {
|
||||
entities = (EntitiesDescriptorType) metadata;
|
||||
} else {
|
||||
entities = new EntitiesDescriptorType();
|
||||
entities.addEntityDescriptor(metadata);
|
||||
}
|
||||
|
||||
for (Object o : entities.getEntityDescriptor()) {
|
||||
EntityDescriptorType entity = (EntityDescriptorType)o;
|
||||
String entityId = entity.getEntityID();
|
||||
ApplicationModel app = realm.addApplication(entityId);
|
||||
app.setFullScopeAllowed(true);
|
||||
app.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||
app.setAttribute(SamlProtocol.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true
|
||||
app.setAttribute(SamlProtocol.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString());
|
||||
app.setAttribute(SamlProtocol.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
|
||||
SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity);
|
||||
if (spDescriptorType.isWantAssertionsSigned()) {
|
||||
app.setAttribute(SamlProtocol.SAML_ASSERTION_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
|
||||
}
|
||||
String adminUrl = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
|
||||
if (adminUrl != null) app.setManagementUrl(adminUrl);
|
||||
|
||||
String urlPattern = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
|
||||
if (urlPattern == null) {
|
||||
urlPattern = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
|
||||
}
|
||||
if (urlPattern != null) {
|
||||
app.addRedirectUri(urlPattern);
|
||||
}
|
||||
|
||||
for (KeyDescriptorType keyDescriptor : spDescriptorType.getKeyDescriptor()) {
|
||||
X509Certificate cert = null;
|
||||
try {
|
||||
cert = SAMLMetadataUtil.getCertificate(keyDescriptor);
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
String certPem = KeycloakModelUtils.getPemFromCertificate(cert);
|
||||
if (keyDescriptor.getUse() == KeyTypes.SIGNING) {
|
||||
app.setAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
|
||||
app.setAttribute(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, certPem);
|
||||
} else if (keyDescriptor.getUse() == KeyTypes.ENCRYPTION) {
|
||||
app.setAttribute(SamlProtocol.SAML_ENCRYPT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
|
||||
app.setAttribute(SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, certPem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLogoutLocation(SPSSODescriptorType idp, String bindingURI) {
|
||||
String logoutResponseLocation = null;
|
||||
|
||||
List<EndpointType> endpoints = idp.getSingleLogoutService();
|
||||
for (EndpointType endpoint : endpoints) {
|
||||
if (endpoint.getBinding().toString().equals(bindingURI)) {
|
||||
if (endpoint.getLocation() != null) {
|
||||
logoutResponseLocation = endpoint.getLocation().toString();
|
||||
} else {
|
||||
logoutResponseLocation = null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return logoutResponseLocation;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -17,8 +17,8 @@ import org.keycloak.protocol.LoginProtocol;
|
|||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
import org.keycloak.util.PemUtils;
|
||||
import org.picketlink.common.constants.GeneralConstants;
|
||||
import org.picketlink.common.constants.JBossSAMLURIConstants;
|
||||
import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants;
|
||||
|
@ -35,6 +35,13 @@ import java.security.PublicKey;
|
|||
*/
|
||||
public class SamlProtocol implements LoginProtocol {
|
||||
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
|
||||
|
||||
|
||||
public static final String ATTRIBUTE_TRUE_VALUE = "true";
|
||||
public static final String ATTRIBUTE_FALSE_VALUE = "false";
|
||||
public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
|
||||
public static final String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE;
|
||||
public static final String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature";
|
||||
public static final String LOGIN_PROTOCOL = "saml";
|
||||
public static final String SAML_BINDING = "saml_binding";
|
||||
public static final String SAML_POST_BINDING = "post";
|
||||
|
@ -46,7 +53,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
|
||||
public static final String SAML_ENCRYPT = "saml.encrypt";
|
||||
public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
|
||||
public static final String REQUEST_ID = "REQUEST_ID";
|
||||
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
|
||||
|
||||
protected KeycloakSession session;
|
||||
|
||||
|
@ -114,7 +121,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
|
||||
ClientSessionModel clientSession = accessCode.getClientSession();
|
||||
ClientModel client = clientSession.getClient();
|
||||
String requestID = clientSession.getNote(REQUEST_ID);
|
||||
String requestID = clientSession.getNote(SAML_REQUEST_ID);
|
||||
String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE);
|
||||
String redirectUri = clientSession.getRedirectUri();
|
||||
String responseIssuer = getResponseIssuer(realm);
|
||||
|
|
|
@ -2,17 +2,13 @@ package org.keycloak.protocol.saml;
|
|||
|
||||
import org.keycloak.VerificationException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
|
||||
import org.keycloak.util.PemUtils;
|
||||
import org.picketlink.common.exceptions.ProcessingException;
|
||||
import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -20,11 +16,8 @@ import java.security.cert.X509Certificate;
|
|||
*/
|
||||
public class SamlProtocolUtils {
|
||||
|
||||
public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
|
||||
public static final String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE;
|
||||
|
||||
public static void verifyDocumentSignature(ClientModel client, Document document) throws VerificationException {
|
||||
if (!"true".equals(client.getAttribute("saml.client.signature"))) {
|
||||
if (!"true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) {
|
||||
return;
|
||||
}
|
||||
SAML2Signature saml2Signature = new SAML2Signature();
|
||||
|
@ -39,11 +32,11 @@ public class SamlProtocolUtils {
|
|||
}
|
||||
|
||||
public static PublicKey getSignatureValidationKey(ClientModel client) throws VerificationException {
|
||||
return getPublicKey(client, SAML_SIGNING_CERTIFICATE_ATTRIBUTE);
|
||||
return getPublicKey(client, SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE);
|
||||
}
|
||||
|
||||
public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
|
||||
return getPublicKey(client, SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
|
||||
return getPublicKey(client, SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
|
||||
}
|
||||
|
||||
public static PublicKey getPublicKey(ClientModel client, String attribute) throws VerificationException {
|
||||
|
|
|
@ -198,7 +198,7 @@ public class SamlService {
|
|||
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
|
||||
clientSession.setNote(SamlProtocol.SAML_BINDING, getBindingType());
|
||||
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
|
||||
clientSession.setNote(SamlProtocol.REQUEST_ID, requestAbstractType.getID());
|
||||
clientSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
|
||||
|
||||
Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event);
|
||||
if (response != null) return response;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.protocol.saml.EntityDescriptorImporterFactory
|
26
services/src/main/java/org/keycloak/exportimport/ApplicationImportSpi.java
Executable file
26
services/src/main/java/org/keycloak/exportimport/ApplicationImportSpi.java
Executable file
|
@ -0,0 +1,26 @@
|
|||
package org.keycloak.exportimport;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ApplicationImportSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "application-import";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ApplicationImporter.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ApplicationImporterFactory.class;
|
||||
}
|
||||
}
|
15
services/src/main/java/org/keycloak/exportimport/ApplicationImporter.java
Executable file
15
services/src/main/java/org/keycloak/exportimport/ApplicationImporter.java
Executable file
|
@ -0,0 +1,15 @@
|
|||
package org.keycloak.exportimport;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.services.resources.admin.RealmAuth;
|
||||
|
||||
/**
|
||||
* Provider plugin interface for importing applications from an arbitrary configuration format
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface ApplicationImporter extends Provider {
|
||||
public Object createJaxrsService(RealmModel realm, RealmAuth auth);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.keycloak.exportimport;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* Provider plugin interface for importing applications from an arbitrary configuration format
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface ApplicationImporterFactory extends ProviderFactory<ApplicationImporter> {
|
||||
public String getDisplayName();
|
||||
}
|
|
@ -8,6 +8,7 @@ import org.keycloak.events.Event;
|
|||
import org.keycloak.events.EventQuery;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.exportimport.ApplicationImporter;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
|
@ -73,6 +74,17 @@ public class RealmAdminResource {
|
|||
auth.init(RealmAuth.Resource.REALM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path for importing applications under this realm.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("application-importers/{formatId}")
|
||||
public Object getApplicationImporter(@PathParam("formatId") String formatId) {
|
||||
ApplicationImporter importer = session.getProvider(ApplicationImporter.class, formatId);
|
||||
return importer.createJaxrsService(realm, auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path for managing applications under this realm.
|
||||
*
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.keycloak.services.resources.admin;
|
|||
|
||||
import org.keycloak.Version;
|
||||
import org.keycloak.events.EventListenerProvider;
|
||||
import org.keycloak.exportimport.ApplicationImporter;
|
||||
import org.keycloak.exportimport.ApplicationImporterFactory;
|
||||
import org.keycloak.freemarker.Theme;
|
||||
import org.keycloak.freemarker.ThemeProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -41,6 +43,7 @@ public class ServerInfoAdminResource {
|
|||
setThemes(info);
|
||||
setEventListeners(info);
|
||||
setProtocols(info);
|
||||
setApplicationImporters(info);
|
||||
return info;
|
||||
}
|
||||
|
||||
|
@ -82,7 +85,16 @@ public class ServerInfoAdminResource {
|
|||
Collections.sort(info.protocols);
|
||||
}
|
||||
|
||||
|
||||
private void setApplicationImporters(ServerInfoRepresentation info) {
|
||||
info.applicationImporters = new LinkedList<Map<String, String>>();
|
||||
for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(ApplicationImporter.class)) {
|
||||
ApplicationImporterFactory factory = (ApplicationImporterFactory)p;
|
||||
Map<String, String> data = new HashMap<String, String>();
|
||||
data.put("id", factory.getId());
|
||||
data.put("name", factory.getDisplayName());
|
||||
info.applicationImporters.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ServerInfoRepresentation {
|
||||
|
||||
|
@ -92,7 +104,7 @@ public class ServerInfoAdminResource {
|
|||
|
||||
private List<String> socialProviders;
|
||||
private List<String> protocols;
|
||||
private List<String> applicationImporters;
|
||||
private List<Map<String, String>> applicationImporters;
|
||||
|
||||
|
||||
private List<String> eventListeners;
|
||||
|
@ -124,7 +136,7 @@ public class ServerInfoAdminResource {
|
|||
return protocols;
|
||||
}
|
||||
|
||||
public List<String> getApplicationImporters() {
|
||||
public List<Map<String, String>> getApplicationImporters() {
|
||||
return applicationImporters;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
org.keycloak.protocol.LoginProtocolSpi
|
||||
org.keycloak.exportimport.ApplicationImportSpi
|
|
@ -1,15 +1,40 @@
|
|||
package org.keycloak.testsuite.saml;
|
||||
|
||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
|
||||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resources.admin.AdminRoot;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.rule.WebResource;
|
||||
import org.keycloak.testsuite.rule.WebRule;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
import javax.ws.rs.client.ClientRequestContext;
|
||||
import javax.ws.rs.client.ClientRequestFilter;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
|
@ -24,10 +49,12 @@ public class SamlBindingTest {
|
|||
|
||||
initializeSamlSecuredWar("/saml/simple-post", "/sales-post", "post.war", classLoader);
|
||||
initializeSamlSecuredWar("/saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
|
||||
initializeSamlSecuredWar("/saml/signed-metadata", "/sales-metadata", "post-metadata.war", classLoader);
|
||||
initializeSamlSecuredWar("/saml/signed-get", "/employee-sig", "employee-sig.war", classLoader);
|
||||
initializeSamlSecuredWar("/saml/bad-client-signed-post", "/bad-client-sales-post-sig", "bad-client-post-sig.war", classLoader);
|
||||
initializeSamlSecuredWar("/saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-post-sig.war", classLoader);
|
||||
initializeSamlSecuredWar("/saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
|
||||
uploadSP();
|
||||
|
||||
}
|
||||
|
||||
|
@ -113,5 +140,64 @@ public class SamlBindingTest {
|
|||
Assert.assertTrue(driver.getPageSource().contains("null"));
|
||||
}
|
||||
|
||||
private static String createToken() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
|
||||
RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
|
||||
ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
|
||||
TokenManager tm = new TokenManager();
|
||||
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
|
||||
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false);
|
||||
AccessToken token = tm.createClientAccessToken(tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession);
|
||||
return tm.encodeToken(adminRealm, token);
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMetadataPostSignedLoginLogout() throws Exception {
|
||||
|
||||
driver.navigate().to("http://localhost:8081/sales-metadata/");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
|
||||
loginPage.login("bburke", "password");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/sales-metadata/");
|
||||
String pageSource = driver.getPageSource();
|
||||
Assert.assertTrue(pageSource.contains("bburke"));
|
||||
driver.navigate().to("http://localhost:8081/sales-metadata?GLO=true");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
|
||||
|
||||
}
|
||||
|
||||
public static void uploadSP() {
|
||||
String token = createToken();
|
||||
final String authHeader = "Bearer " + token;
|
||||
ClientRequestFilter authFilter = new ClientRequestFilter() {
|
||||
@Override
|
||||
public void filter(ClientRequestContext requestContext) throws IOException {
|
||||
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
|
||||
}
|
||||
};
|
||||
Client client = ClientBuilder.newBuilder().register(authFilter).build();
|
||||
UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
|
||||
WebTarget adminRealms = client.target(AdminRoot.realmsUrl(authBase));
|
||||
|
||||
|
||||
MultipartFormDataOutput formData = new MultipartFormDataOutput();
|
||||
InputStream is = SamlBindingTest.class.getResourceAsStream("/saml/sp-metadata.xml");
|
||||
Assert.assertNotNull(is);
|
||||
formData.addFormData("file", is, MediaType.APPLICATION_XML_TYPE);
|
||||
|
||||
WebTarget upload = adminRealms.path("demo/application-importers/saml2-entity-descriptor/upload");
|
||||
System.out.println(upload.getUri());
|
||||
Response response = upload.request().post(Entity.entity(formData, MediaType.MULTIPART_FORM_DATA));
|
||||
Assert.assertEquals(204, response.getStatus());
|
||||
response.close();
|
||||
client.close();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,31 @@
|
|||
<PicketLink xmlns="urn:picketlink:identity-federation:config:2.1">
|
||||
<PicketLinkSP xmlns="urn:picketlink:identity-federation:config:2.1"
|
||||
ServerEnvironment="tomcat" BindingType="POST" SupportsSignatures="true">
|
||||
<IdentityURL>${idp-sig.url::http://localhost:8081/auth/realms/demo/protocol/saml}
|
||||
</IdentityURL>
|
||||
<ServiceURL>${sales-post-sig.url::http://localhost:8081/sales-metadata/}
|
||||
</ServiceURL>
|
||||
<KeyProvider
|
||||
ClassName="org.picketlink.identity.federation.core.impl.KeyStoreKeyManager">
|
||||
<Auth Key="KeyStoreURL" Value="saml/signed-post/WEB-INF/keystore.jks" />
|
||||
<Auth Key="KeyStorePass" Value="store123" />
|
||||
<Auth Key="SigningKeyPass" Value="test123" />
|
||||
<Auth Key="SigningKeyAlias" Value="http://localhost:8080/sales-post-sig/" />
|
||||
<ValidatingAlias Key="localhost" Value="demo" />
|
||||
<ValidatingAlias Key="127.0.0.1" Value="demo" />
|
||||
</KeyProvider>
|
||||
|
||||
</PicketLinkSP>
|
||||
<Handlers xmlns="urn:picketlink:identity-federation:handler:config:2.1">
|
||||
<Handler
|
||||
class="org.picketlink.identity.federation.web.handlers.saml2.SAML2LogOutHandler" />
|
||||
<Handler
|
||||
class="org.picketlink.identity.federation.web.handlers.saml2.SAML2AuthenticationHandler" />
|
||||
<Handler
|
||||
class="org.picketlink.identity.federation.web.handlers.saml2.RolesGenerationHandler" />
|
||||
<Handler
|
||||
class="org.picketlink.identity.federation.web.handlers.saml2.SAML2SignatureGenerationHandler" />
|
||||
<Handler
|
||||
class="org.picketlink.identity.federation.web.handlers.saml2.SAML2SignatureValidationHandler" />
|
||||
</Handlers>
|
||||
</PicketLink>
|
38
testsuite/integration/src/test/resources/saml/sp-metadata.xml
Executable file
38
testsuite/integration/src/test/resources/saml/sp-metadata.xml
Executable file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<EntitiesDescriptor Name="urn:mace:shibboleth:testshib:two"
|
||||
xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||
>
|
||||
<EntityDescriptor entityID="http://localhost:8081/sales-metadata/">
|
||||
<SPSSODescriptor AuthnRequestsSigned="true"
|
||||
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext">
|
||||
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
||||
</NameIDFormat>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/sales-metadata/"/>
|
||||
<AssertionConsumerService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/sales-metadata/"
|
||||
index="1" isDefault="true" />
|
||||
<KeyDescriptor use="signing">
|
||||
<dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
|
||||
<dsig:X509Data>
|
||||
<dsig:X509Certificate>
|
||||
MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==
|
||||
</dsig:X509Certificate>
|
||||
</dsig:X509Data>
|
||||
</dsig:KeyInfo>
|
||||
</KeyDescriptor>
|
||||
</SPSSODescriptor>
|
||||
<Organization>
|
||||
<OrganizationName xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||
xml:lang="en">JBoss</OrganizationName>
|
||||
<OrganizationDisplayName xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||
xml:lang="en">JBoss by Red Hat</OrganizationDisplayName>
|
||||
<OrganizationURL xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||
xml:lang="en">http://localhost:8080/sales-metadata/</OrganizationURL>
|
||||
</Organization>
|
||||
<ContactPerson contactType="technical">
|
||||
<GivenName>The</GivenName>
|
||||
<SurName>Admin</SurName>
|
||||
<EmailAddress>admin@mycompany.com</EmailAddress>
|
||||
</ContactPerson>
|
||||
</EntityDescriptor>
|
||||
</EntitiesDescriptor>
|
Loading…
Reference in a new issue