Merge pull request #1998 from ssilvert/partial-import

KEYCLOAK-1979: Partial import
This commit is contained in:
Stian Thorgersen 2016-01-11 09:22:09 +01:00
commit f1602b3b8d
28 changed files with 2063 additions and 211 deletions

View file

@ -0,0 +1,103 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.keycloak.representations.idm;
import java.util.List;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
/**
* Used for partial import of users, clients, roles, and identity providers.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
@JsonIgnoreProperties(ignoreUnknown=true)
public class PartialImportRepresentation {
public enum Policy { SKIP, OVERWRITE, FAIL };
protected Policy policy = Policy.FAIL;
protected String ifResourceExists = "";
protected List<UserRepresentation> users;
protected List<ClientRepresentation> clients;
protected List<IdentityProviderRepresentation> identityProviders;
protected RolesRepresentation roles;
public boolean hasUsers() {
return (users != null) && !users.isEmpty();
}
public boolean hasClients() {
return (clients != null) && !clients.isEmpty();
}
public boolean hasIdps() {
return (identityProviders != null) && !identityProviders.isEmpty();
}
public boolean hasRealmRoles() {
return (roles != null) && (roles.getRealm() != null) && (!roles.getRealm().isEmpty());
}
public boolean hasClientRoles() {
return (roles != null) && (roles.getClient() != null) && (!roles.getClient().isEmpty());
}
public String getIfResourceExists() {
return ifResourceExists;
}
public void setIfResourceExists(String ifResourceExists) {
this.ifResourceExists = ifResourceExists;
this.policy = Policy.valueOf(ifResourceExists);
}
public Policy getPolicy() {
return this.policy;
}
public List<UserRepresentation> getUsers() {
return users;
}
public void setUsers(List<UserRepresentation> users) {
this.users = users;
}
public List<ClientRepresentation> getClients() {
return clients;
}
public void setClients(List<ClientRepresentation> clients) {
this.clients = clients;
}
public List<IdentityProviderRepresentation> getIdentityProviders() {
return identityProviders;
}
public void setIdentityProviders(List<IdentityProviderRepresentation> identityProviders) {
this.identityProviders = identityProviders;
}
public RolesRepresentation getRoles() {
return roles;
}
public void setRoles(RolesRepresentation roles) {
this.roles = roles;
}
}

View file

@ -1,114 +1,134 @@
<chapter id="export-import">
<title>Export and Import</title>
<para>
Export/import is useful especially if you want to migrate your whole Keycloak database from one environment to another or migrate to different database (For example from MySQL to Oracle).
You can trigger export/import at startup of Keycloak server and it's configurable with System properties right now. The fact it's done at server startup means that no-one can access Keycloak UI or REST endpoints
and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results.
</para>
<para>
You can export/import your database either to:
<itemizedlist>
<listitem>Directory on local filesystem</listitem>
<listitem>Single JSON file on your filesystem</listitem>
</itemizedlist>
<section>
<title>Startup export/import</title>
<para>
Export/import is useful especially if you want to migrate your whole Keycloak database from one environment to another or migrate to different database (For example from MySQL to Oracle).
You can trigger export/import at startup of Keycloak server and it's configurable with System properties right now. The fact it's done at server startup means that no-one can access Keycloak UI or REST endpoints
and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results.
</para>
<para>
You can export/import your database either to:
<itemizedlist>
<listitem>Directory on local filesystem</listitem>
<listitem>Single JSON file on your filesystem</listitem>
</itemizedlist>
When importing using the "dir" strategy, note that the files need to follow the naming convention specified below.
If you are importing files which were previously exported, the files already follow this convention.
<itemizedlist>
<listitem>{REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs"</listitem>
<listitem>{REALM_NAME}-users-{INDEX}.json, such as "acme-roadrunner-affairs-users-0.json" for the first users file of the realm named "acme-roadrunner-affairs"</listitem>
</itemizedlist>
</para>
<para>
If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have
very large amount of users in your database, you likely don't want to import them into single file as the file might be very big.
Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues.
</para>
<para>
To export into unencrypted directory you can use:
<programlisting><![CDATA[
When importing using the "dir" strategy, note that the files need to follow the naming convention specified below.
If you are importing files which were previously exported, the files already follow this convention.
<itemizedlist>
<listitem>{REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs"</listitem>
<listitem>{REALM_NAME}-users-{INDEX}.json, such as "acme-roadrunner-affairs-users-0.json" for the first users file of the realm named "acme-roadrunner-affairs"</listitem>
</itemizedlist>
</para>
<para>
If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have
very large amount of users in your database, you likely don't want to import them into single file as the file might be very big.
Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues.
</para>
<para>
To export into unencrypted directory you can use:
<programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export
-Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=<DIR TO EXPORT TO>
]]></programlisting>
And similarly for import just use <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> .
</para>
<para>
To export into single JSON file you can use:
<programlisting><![CDATA[
And similarly for import just use <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> .
</para>
<para>
To export into single JSON file you can use:
<programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export
-Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO EXPORT TO>
]]></programlisting>
</para>
<para>
Here's an example of importing:
<programlisting><![CDATA[
</para>
<para>
Here's an example of importing:
<programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=import
-Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO IMPORT>
-Dkeycloak.migration.strategy=OVERWRITE_EXISTING
]]></programlisting>
</para>
<para>
Other available options are:
<variablelist>
<varlistentry>
<term>-Dkeycloak.migration.realmName</term>
<listitem>
<para>
can be used if you want to export just one specified realm instead of all.
If not specified, then all realms will be exported.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.usersExportStrategy</term>
<listitem>
<para>
can be used to specify for Directory providers to specify where to import users.
Possible values are:
<itemizedlist>
<listitem>DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value</listitem>
<listitem>SKIP - exporting of users will be skipped completely</listitem>
<listitem>REALM_FILE - All users will be exported to same file with realm (So file like "foo-realm.json" with both realm data and users)</listitem>
<listitem>SAME_FILE - All users will be exported to same file but different than realm (So file like "foo-realm.json" with realm data and "foo-users.json" with users)</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.usersPerFile</term>
<listitem>
<para>
can be used to specify number of users per file (and also per DB transaction).
It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.strategy</term>
<listitem>
<para>
is used during import. It can be used to specify how to proceed if realm with same name
already exists in the database where you are going to import data. Possible values are:
<itemizedlist>
<listitem>IGNORE_EXISTING - Ignore importing if realm of this name already exists</listitem>
<listitem>OVERWRITE_EXISTING - Remove existing realm and import it again with new data from JSON file.
If you want to fully migrate one environment to another and ensure that the new environment will contain same data
like the old one, you can specify this.
</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
<para>
When importing realm files that weren't exported before, the option <literal>keycloak.import</literal> can be used. If more than one realm
file needs to be imported, a comma separated list of file names can be specified. This is more appropriate than the cases before, as this
will happen only after the master realm has been initialized. Examples:
<itemizedlist>
<listitem>-Dkeycloak.import=/tmp/realm1.json</listitem>
<listitem>-Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json</listitem>
</itemizedlist>
</para>
</para>
<para>
Other available options are:
<variablelist>
<varlistentry>
<term>-Dkeycloak.migration.realmName</term>
<listitem>
<para>
can be used if you want to export just one specified realm instead of all.
If not specified, then all realms will be exported.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.usersExportStrategy</term>
<listitem>
<para>
can be used to specify for Directory providers to specify where to import users.
Possible values are:
<itemizedlist>
<listitem>DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value</listitem>
<listitem>SKIP - exporting of users will be skipped completely</listitem>
<listitem>REALM_FILE - All users will be exported to same file with realm (So file like "foo-realm.json" with both realm data and users)</listitem>
<listitem>SAME_FILE - All users will be exported to same file but different than realm (So file like "foo-realm.json" with realm data and "foo-users.json" with users)</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.usersPerFile</term>
<listitem>
<para>
can be used to specify number of users per file (and also per DB transaction).
It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.strategy</term>
<listitem>
<para>
is used during import. It can be used to specify how to proceed if realm with same name
already exists in the database where you are going to import data. Possible values are:
<itemizedlist>
<listitem>IGNORE_EXISTING - Ignore importing if realm of this name already exists</listitem>
<listitem>OVERWRITE_EXISTING - Remove existing realm and import it again with new data from JSON file.
If you want to fully migrate one environment to another and ensure that the new environment will contain same data
like the old one, you can specify this.
</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
<para>
When importing realm files that weren't exported before, the option <literal>keycloak.import</literal> can be used. If more than one realm
file needs to be imported, a comma separated list of file names can be specified. This is more appropriate than the cases before, as this
will happen only after the master realm has been initialized. Examples:
<itemizedlist>
<listitem>-Dkeycloak.import=/tmp/realm1.json</listitem>
<listitem>-Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json</listitem>
</itemizedlist>
</para>
</section>
<section>
<title>Admin console export/import</title>
<para>
Import of most resources can be performed from the admin console.
Exporting resources will be supported in future versions.
</para>
<para>
The files created during a "startup" export can be used to import from
the admin UI. This way, you can export from one realm and import to
another realm. Or, you can export from one server and import to another.
</para>
<warning>
<para>
The admin console import allows you to "overwrite" resources if you choose.
Use this feature with caution, especially on a production system.
</para>
</warning>
</section>
</chapter>

View file

@ -406,6 +406,16 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmEventsConfigCtrl'
})
.when('/realms/:realm/partial-import', {
templateUrl : resourceUrl + '/partials/partial-import.html',
resolve : {
resourceName : function() { return 'users'},
realm : function(RealmLoader) {
return RealmLoader();
}
},
controller : 'RealmImportCtrl'
})
.when('/create/user/:realm', {
templateUrl : resourceUrl + '/partials/user-detail.html',
resolve : {

View file

@ -2062,14 +2062,210 @@ module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, Clien
};
});
module.controller('RealmImportCtrl', function($scope, realm, $route,
Notifications, $modal, $resource) {
$scope.rawContent = {};
$scope.fileContent = {
enabled: true
};
$scope.changed = false;
$scope.files = [];
$scope.realm = realm;
$scope.overwrite = false;
$scope.skip = false;
$scope.importUsers = false;
$scope.importClients = false;
$scope.importIdentityProviders = false;
$scope.importRealmRoles = false;
$scope.importClientRoles = false;
$scope.ifResourceExists='FAIL';
$scope.isMultiRealm = false;
$scope.results = {};
$scope.currentPage = 0;
var pageSize = 15;
var oldCopy = angular.copy($scope.fileContent);
$scope.importFile = function($fileContent){
var parsed;
try {
parsed = JSON.parse($fileContent);
} catch (e) {
Notifications.error('Unable to parse JSON file.');
return;
}
$scope.rawContent = angular.copy(parsed);
if (($scope.rawContent instanceof Array) && ($scope.rawContent.length > 0)) {
if ($scope.rawContent.length > 1) $scope.isMultiRealm = true;
$scope.fileContent = $scope.rawContent[0];
} else {
$scope.fileContent = $scope.rawContent;
}
$scope.importing = true;
$scope.importUsers = $scope.hasArray('users');
$scope.importClients = $scope.hasArray('clients');
$scope.importIdentityProviders = $scope.hasArray('identityProviders');
$scope.importRealmRoles = $scope.hasRealmRoles();
$scope.importClientRoles = $scope.hasClientRoles();
$scope.results = {};
if (!$scope.hasResources()) {
$scope.nothingToImport();
}
};
$scope.hasResults = function() {
return (Object.keys($scope.results).length > 0) &&
($scope.results.results !== undefined) &&
($scope.results.results.length > 0);
}
$scope.resultsPage = function() {
if (!$scope.hasResults()) return {};
return $scope.results.results.slice(startIndex(), endIndex());
}
function startIndex() {
return pageSize * $scope.currentPage;
}
function endIndex() {
var length = $scope.results.results.length;
var endIndex = startIndex() + pageSize;
if (endIndex > length) endIndex = length;
return endIndex;
}
$scope.setFirstPage = function() {
$scope.currentPage = 0;
}
$scope.setNextPage = function() {
$scope.currentPage++;
}
$scope.setPreviousPage = function() {
$scope.currentPage--;
}
$scope.hasNext = function() {
if (!$scope.hasResults()) return false;
var length = $scope.results.results.length;
//console.log('length=' + length);
var endIndex = startIndex() + pageSize;
//console.log('endIndex=' + endIndex);
return length > endIndex;
}
$scope.hasPrevious = function() {
if (!$scope.hasResults()) return false;
return $scope.currentPage > 0;
}
$scope.viewImportDetails = function() {
$modal.open({
templateUrl: resourceUrl + '/partials/modal/view-object.html',
controller: 'ObjectModalCtrl',
resolve: {
object: function () {
return $scope.fileContent;
}
}
})
};
$scope.hasArray = function(section) {
return ($scope.fileContent !== 'undefined') &&
($scope.fileContent.hasOwnProperty(section)) &&
($scope.fileContent[section] instanceof Array) &&
($scope.fileContent[section].length > 0);
}
$scope.hasRealmRoles = function() {
return $scope.hasRoles() &&
($scope.fileContent.roles.hasOwnProperty('realm')) &&
($scope.fileContent.roles.realm instanceof Array) &&
($scope.fileContent.roles.realm.length > 0);
}
$scope.hasRoles = function() {
return ($scope.fileContent !== 'undefined') &&
($scope.fileContent.hasOwnProperty('roles')) &&
($scope.fileContent.roles !== 'undefined');
}
$scope.hasClientRoles = function() {
return $scope.hasRoles() &&
($scope.fileContent.roles.hasOwnProperty('client')) &&
(Object.keys($scope.fileContent.roles.client).length > 0);
}
$scope.itemCount = function(section) {
if (!$scope.importing) return 0;
if ($scope.hasRealmRoles() && (section === 'roles.realm')) return $scope.fileContent.roles.realm.length;
if ($scope.hasClientRoles() && (section === 'roles.client')) return Object.keys($scope.fileContent.roles.client).length;
if (!$scope.fileContent.hasOwnProperty(section)) return 0;
return $scope.fileContent[section].length;
}
$scope.hasResources = function() {
return ($scope.importUsers && $scope.hasArray('users')) ||
($scope.importClients && $scope.hasArray('clients')) ||
($scope.importIdentityProviders && $scope.hasArray('identityProviders')) ||
($scope.importRealmRoles && $scope.hasRealmRoles()) ||
($scope.importClientRoles && $scope.hasClientRoles());
}
$scope.nothingToImport = function() {
Notifications.error('No resouces specified to import.');
}
$scope.$watch('fileContent', function() {
if (!angular.equals($scope.fileContent, oldCopy)) {
$scope.changed = true;
}
}, true);
$scope.successMessage = function() {
var message = $scope.results.added + ' records added. ';
if ($scope.ifResourceExists === 'SKIP') {
message += $scope.results.skipped + ' records skipped.'
}
if ($scope.ifResourceExists === 'OVERWRITE') {
message += $scope.results.overwritten + ' records overwritten.';
}
return message;
}
$scope.save = function() {
var json = angular.copy($scope.fileContent);
json.ifResourceExists = $scope.ifResourceExists;
if (!$scope.importUsers) delete json.users;
if (!$scope.importIdentityProviders) delete json.identityProviders;
if (!$scope.importClients) delete json.clients;
if (json.hasOwnProperty('roles')) {
if (!$scope.importRealmRoles) delete json.roles.realm;
if (!$scope.importClientRoles) delete json.roles.client;
}
var importFile = $resource(authUrl + '/admin/realms/' + realm.realm + '/partialImport');
$scope.results = importFile.save(json, function() {
Notifications.success($scope.successMessage());
}, function(error) {
if (error.data.errorMessage) {
Notifications.error(error.data.errorMessage);
} else {
Notifications.error('Unexpected error during import');
}
});
};
$scope.reset = function() {
$route.reload();
}
});

View file

@ -0,0 +1,120 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Partial Import</h1>
<form class="form-horizontal" name="partialImportForm" novalidate>
<fieldset class="border-top">
<div class="form-group">
<label for="name" class="col-sm-2 control-label">File</label>
<div class="col-md-6" data-ng-hide="importing">
<label for="import-file" class="btn btn-default">{{:: 'select-file'| translate}} <i class="pficon pficon-import"></i></label>
<input id="import-file" type="file" class="hidden" kc-on-read-file="importFile($fileContent)"/>
</div>
<div class="col-md-6" data-ng-show="importing">
<button class="btn btn-default" data-ng-click="viewImportDetails()">{{:: 'view-details'| translate}}</button>
<button class="btn btn-default" data-ng-click="reset()">{{:: 'clear-import'| translate}}</button>
</div>
</div>
<div class="form-group" data-ng-show="importing && isMultiRealm && !hasResults()">
<label for="fromRealm" class="col-md-2 control-label">Import from realm</label>
<div class="col-md-2">
<div>
<select id="fromRealm" ng-model="fileContent" class="form-control"
ng-options="item as item.realm for item in rawContent">
</select>
</div>
</div>
</div>
<div class="form-group" data-ng-show="importing && hasArray('users') && !hasResults()">
<label class="col-md-2 control-label" for="importUsers">Import Users ({{itemCount('users')}})</label>
<div class="col-sm-6">
<input ng-model="importUsers" name="importUsers" id="importUsers" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="importing && hasArray('clients') && !hasResults()">
<label class="col-md-2 control-label" for="importClients">Import Clients ({{itemCount('clients')}})</label>
<div class="col-sm-6">
<input ng-model="importClients" name="importClients" id="importClients" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="importing && hasArray('identityProviders') && !hasResults()">
<label class="col-md-2 control-label" for="importIdentityProviders">Import Identity Providers ({{itemCount('identityProviders')}})</label>
<div class="col-sm-6">
<input ng-model="importIdentityProviders" name="importIdentityProviders" id="importIdentityProviders" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="importing && hasRealmRoles() && !hasResults()">
<label class="col-md-2 control-label" for="importRealmRoles">Import Realm Roles ({{itemCount('roles.realm')}})</label>
<div class="col-sm-6">
<input ng-model="importRealmRoles" name="importRealmRoles" id="importRealmRoles" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="importing && hasClientRoles() && !hasResults()">
<label class="col-md-2 control-label" for="importClientRoles">Import Client Roles ({{itemCount('roles.client')}})</label>
<div class="col-sm-6">
<input ng-model="importClientRoles" name="importClientRoles" id="importClientRoles" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="importing && hasResources() && !hasResults()">
<label for="ifResourceExists" class="col-md-2 control-label">If a resource exists</label>
<div class="col-md-2">
<div>
<select id="ifResourceExists" ng-model="ifResourceExists" class="form-control">
<option value="FAIL">Fail</option>
<option value="SKIP">Skip</option>
<option value="OVERWRITE">Overwrite</option>
</select>
</div>
</div>
<kc-tooltip>Specify what should be done if you try to import a resource that already exists.</kc-tooltip>
</div>
</fieldset>
<div class="form-group" data-ng-show="importing && hasResources() && !hasResults()">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">{{:: 'import'| translate}}</button>
</div>
</div>
<div class="form-group" data-ng-show="hasResults()">
{{successMessage()}}
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Action</th>
<th>Type</th>
<th>Name</th>
<th>Id</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="result in resultsPage()" >
<td ng-show="result.action == 'OVERWRITTEN'"><span class="label label-danger">{{result.action}}</span></td>
<td ng-show="result.action == 'SKIPPED'"><span class="label label-warning">{{result.action}}</span></td>
<td ng-show="result.action == 'ADDED'"><span class="label label-success">{{result.action}}</span></td>
<td>{{result.resourceType}}</td>
<td>{{result.resourceName}}</td>
<td>{{result.id}}</td>
</tr>
</tbody>
</table>
<div class="table-nav">
<button data-ng-click="setFirstPage()" class="first" ng-disabled="">First page</button>
<button data-ng-click="setPreviousPage()" class="prev" ng-disabled="!hasPrevious()">Previous page</button>
<button data-ng-click="setNextPage()" class="next" ng-disabled="!hasNext()">Next page</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -45,6 +45,7 @@
<li data-ng-show="access.viewUsers" data-ng-class="(path[2] == 'users' || path[1] == 'user') && 'active'"><a href="#/realms/{{realm.realm}}/users"><span class="pficon pficon-user"></span> Users</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/realm"><i class="fa fa-clock-o"></i> Sessions</a></li>
<li data-ng-show="access.viewEvents" data-ng-class="(path[2] == 'events' || path[2] == 'events-settings') && 'active'"><a href="#/realms/{{realm.realm}}/events"><i class="fa fa-calendar"></i> Events</a></li>
<li data-ng-show="access.manageRealm" ng-class="(path[2] =='partial-import') && 'active'"><a href="#/realms/{{realm.realm}}/partial-import"><span class="pficon pficon-import"></span> Import</a></li>
</ul>
</div>
</div>

View file

@ -66,6 +66,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.keycloak.representations.idm.RolesRepresentation;
public class RepresentationToModel {
@ -195,47 +196,7 @@ public class RepresentationToModel {
createClients(session, rep, newRealm);
}
if (rep.getRoles() != null) {
if (rep.getRoles().getRealm() != null) { // realm roles
for (RoleRepresentation roleRep : rep.getRoles().getRealm()) {
createRole(newRealm, roleRep);
}
}
if (rep.getRoles().getClient() != null) {
for (Map.Entry<String, List<RoleRepresentation>> entry : rep.getRoles().getClient().entrySet()) {
ClientModel client = newRealm.getClientByClientId(entry.getKey());
if (client == null) {
throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
}
for (RoleRepresentation roleRep : entry.getValue()) {
// Application role may already exists (for example if it is defaultRole)
RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName());
role.setDescription(roleRep.getDescription());
boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired();
role.setScopeParamRequired(scopeParamRequired);
}
}
}
// now that all roles are created, re-iterate and set up composites
if (rep.getRoles().getRealm() != null) { // realm roles
for (RoleRepresentation roleRep : rep.getRoles().getRealm()) {
RoleModel role = newRealm.getRole(roleRep.getName());
addComposites(role, roleRep, newRealm);
}
}
if (rep.getRoles().getClient() != null) {
for (Map.Entry<String, List<RoleRepresentation>> entry : rep.getRoles().getClient().entrySet()) {
ClientModel client = newRealm.getClientByClientId(entry.getKey());
if (client == null) {
throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
}
for (RoleRepresentation roleRep : entry.getValue()) {
RoleModel role = client.getRole(roleRep.getName());
addComposites(role, roleRep, newRealm);
}
}
}
}
importRoles(rep.getRoles(), newRealm);
// Setup realm default roles
if (rep.getDefaultRoles() != null) {
@ -356,6 +317,50 @@ public class RepresentationToModel {
}
}
public static void importRoles(RolesRepresentation realmRoles, RealmModel realm) {
if (realmRoles == null) return;
if (realmRoles.getRealm() != null) { // realm roles
for (RoleRepresentation roleRep : realmRoles.getRealm()) {
createRole(realm, roleRep);
}
}
if (realmRoles.getClient() != null) {
for (Map.Entry<String, List<RoleRepresentation>> entry : realmRoles.getClient().entrySet()) {
ClientModel client = realm.getClientByClientId(entry.getKey());
if (client == null) {
throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
}
for (RoleRepresentation roleRep : entry.getValue()) {
// Application role may already exists (for example if it is defaultRole)
RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName());
role.setDescription(roleRep.getDescription());
boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired();
role.setScopeParamRequired(scopeParamRequired);
}
}
}
// now that all roles are created, re-iterate and set up composites
if (realmRoles.getRealm() != null) { // realm roles
for (RoleRepresentation roleRep : realmRoles.getRealm()) {
RoleModel role = realm.getRole(roleRep.getName());
addComposites(role, roleRep, realm);
}
}
if (realmRoles.getClient() != null) {
for (Map.Entry<String, List<RoleRepresentation>> entry : realmRoles.getClient().entrySet()) {
ClientModel client = realm.getClientByClientId(entry.getKey());
if (client == null) {
throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
}
for (RoleRepresentation roleRep : entry.getValue()) {
RoleModel role = client.getRole(roleRep.getName());
addComposites(role, roleRep, realm);
}
}
}
}
public static void importGroups(RealmModel realm, RealmRepresentation rep) {
List<GroupRepresentation> groups = rep.getGroups();
if (groups == null) return;
@ -639,15 +644,15 @@ public class RepresentationToModel {
if (rep.getAccountTheme() != null) realm.setAccountTheme(rep.getAccountTheme());
if (rep.getAdminTheme() != null) realm.setAdminTheme(rep.getAdminTheme());
if (rep.getEmailTheme() != null) realm.setEmailTheme(rep.getEmailTheme());
if (rep.isEventsEnabled() != null) realm.setEventsEnabled(rep.isEventsEnabled());
if (rep.getEventsExpiration() != null) realm.setEventsExpiration(rep.getEventsExpiration());
if (rep.getEventsListeners() != null) realm.setEventsListeners(new HashSet<>(rep.getEventsListeners()));
if (rep.getEnabledEventTypes() != null) realm.setEnabledEventTypes(new HashSet<>(rep.getEnabledEventTypes()));
if (rep.isAdminEventsEnabled() != null) realm.setAdminEventsEnabled(rep.isAdminEventsEnabled());
if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled());
if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep));

View file

@ -76,9 +76,12 @@ public class JpaUserProvider implements UserProvider {
userModel.joinGroup(g);
}
}
for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) {
if (r.isEnabled() && r.isDefaultAction()) {
userModel.addRequiredAction(r.getAlias());
if (addDefaultRequiredActions){
for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) {
if (r.isEnabled() && r.isDefaultAction()) {
userModel.addRequiredAction(r.getAlias());
}
}
}

View file

@ -1059,6 +1059,7 @@ public class RealmAdapter implements RealmModel {
em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate();
em.remove(roleEntity);
em.flush();
return true;
}
@ -1217,7 +1218,7 @@ public class RealmAdapter implements RealmModel {
realm.setEventsListeners(listeners);
em.flush();
}
@Override
public Set<String> getEnabledEventTypes() {
return realm.getEnabledEventTypes();
@ -1228,7 +1229,7 @@ public class RealmAdapter implements RealmModel {
realm.setEnabledEventTypes(enabledEventTypes);
em.flush();
}
@Override
public boolean isAdminEventsEnabled() {
return realm.isAdminEventsEnabled();
@ -1250,7 +1251,7 @@ public class RealmAdapter implements RealmModel {
realm.setAdminEventsDetailsEnabled(enabled);
em.flush();
}
@Override
public ClientModel getMasterAdminClient() {
ClientEntity masterAdminClient = realm.getMasterAdminClient();

View file

@ -0,0 +1,132 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.services.ErrorResponse;
/**
* Base PartialImport for most resource types.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public abstract class AbstractPartialImport<T> implements PartialImport<T> {
protected static Logger logger = Logger.getLogger(AbstractPartialImport.class);
protected final Set<T> toOverwrite = new HashSet<>();
protected final Set<T> toSkip = new HashSet<>();
public abstract List<T> getRepList(PartialImportRepresentation partialImportRep);
public abstract String getName(T resourceRep);
public abstract String getModelId(RealmModel realm, KeycloakSession session, T resourceRep);
public abstract boolean exists(RealmModel realm, KeycloakSession session, T resourceRep);
public abstract String existsMessage(T resourceRep);
public abstract ResourceType getResourceType();
public abstract void remove(RealmModel realm, KeycloakSession session, T resourceRep);
public abstract void create(RealmModel realm, KeycloakSession session, T resourceRep);
@Override
public void prepare(PartialImportRepresentation partialImportRep,
RealmModel realm,
KeycloakSession session) throws ErrorResponseException {
List<T> repList = getRepList(partialImportRep);
if ((repList == null) || repList.isEmpty()) return;
for (T resourceRep : getRepList(partialImportRep)) {
if (exists(realm, session, resourceRep)) {
switch (partialImportRep.getPolicy()) {
case SKIP: toSkip.add(resourceRep); break;
case OVERWRITE: toOverwrite.add(resourceRep); break;
default: throw existsError(existsMessage(resourceRep));
}
}
}
}
protected ErrorResponseException existsError(String message) {
Response error = ErrorResponse.exists(message);
return new ErrorResponseException(error);
}
protected PartialImportResult overwritten(String modelId, T resourceRep){
return PartialImportResult.overwritten(getResourceType(), getName(resourceRep), modelId, resourceRep);
}
protected PartialImportResult skipped(String modelId, T resourceRep) {
return PartialImportResult.skipped(getResourceType(), getName(resourceRep), modelId, resourceRep);
}
protected PartialImportResult added(String modelId, T resourceRep) {
return PartialImportResult.added(getResourceType(), getName(resourceRep), modelId, resourceRep);
}
@Override
public void removeOverwrites(RealmModel realm, KeycloakSession session) {
for (T resourceRep : toOverwrite) {
remove(realm, session, resourceRep);
}
}
@Override
public PartialImportResults doImport(PartialImportRepresentation partialImportRep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
PartialImportResults results = new PartialImportResults();
List<T> repList = getRepList(partialImportRep);
if ((repList == null) || repList.isEmpty()) return results;
for (T resourceRep : toOverwrite) {
try {
create(realm, session, resourceRep);
} catch (Exception e) {
logger.error("Error overwriting " + getName(resourceRep), e);
throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR));
}
String modelId = getModelId(realm, session, resourceRep);
results.addResult(overwritten(modelId, resourceRep));
}
for (T resourceRep : toSkip) {
String modelId = getModelId(realm, session, resourceRep);
results.addResult(skipped(modelId, resourceRep));
}
for (T resourceRep : repList) {
if (toOverwrite.contains(resourceRep)) continue;
if (toSkip.contains(resourceRep)) continue;
try {
create(realm, session, resourceRep);
String modelId = getModelId(realm, session, resourceRep);
results.addResult(added(modelId, resourceRep));
} catch (Exception e) {
logger.error("Error creating " + getName(resourceRep), e);
throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR));
}
}
return results;
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
/**
* Enum for actions taken by PartialImport.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public enum Action {
ADDED, SKIPPED, OVERWRITTEN
}

View file

@ -0,0 +1,162 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.Response;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.services.ErrorResponse;
/**
* Partial Import handler for Client Roles.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class ClientRolesPartialImport {
private final Map<String, Set<RoleRepresentation>> toOverwrite = new HashMap<>();
private final Map<String, Set<RoleRepresentation>> toSkip = new HashMap<>();
public Map<String, Set<RoleRepresentation>> getToOverwrite() {
return this.toOverwrite;
}
public Map<String, Set<RoleRepresentation>> getToSkip() {
return this.toSkip;
}
public Map<String, List<RoleRepresentation>> getRepList(PartialImportRepresentation partialImportRep) {
if (partialImportRep.getRoles() == null) return null;
return partialImportRep.getRoles().getClient();
}
public String getName(RoleRepresentation roleRep) {
if (roleRep.getName() == null)
throw new IllegalStateException("Client role to import does not have a name");
return roleRep.getName();
}
public String getCombinedName(String clientId, RoleRepresentation roleRep) {
return clientId + "-->" + getName(roleRep);
}
public boolean exists(RealmModel realm, KeycloakSession session, String clientId, RoleRepresentation roleRep) {
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) return false;
for (RoleModel role : client.getRoles()) {
if (getName(roleRep).equals(role.getName())) return true;
}
return false;
}
// check if client currently exists or will exists as a result of this partial import
private boolean clientExists(PartialImportRepresentation partialImportRep, RealmModel realm, String clientId) {
if (realm.getClientByClientId(clientId) != null) return true;
if (partialImportRep.getClients() == null) return false;
for (ClientRepresentation client : partialImportRep.getClients()) {
if (clientId.equals(client.getClientId())) return true;
}
return false;
}
public String existsMessage(String clientId, RoleRepresentation roleRep) {
return "Client role '" + getName(roleRep) + "' for client '" + clientId + "' already exists.";
}
public ResourceType getResourceType() {
return ResourceType.CLIENT_ROLE;
}
public void deleteRole(RealmModel realm, String clientId, RoleRepresentation roleRep) {
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) {
// client might have been removed as part of this partial import
return;
}
RoleModel role = client.getRole(getName(roleRep));
client.removeRole(role);
}
public void prepare(PartialImportRepresentation partialImportRep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
Map<String, List<RoleRepresentation>> repList = getRepList(partialImportRep);
if (repList == null || repList.isEmpty()) return;
for (String clientId : repList.keySet()) {
if (!clientExists(partialImportRep, realm, clientId)) {
throw noClientFound(clientId);
}
toOverwrite.put(clientId, new HashSet<RoleRepresentation>());
toSkip.put(clientId, new HashSet<RoleRepresentation>());
for (RoleRepresentation roleRep : repList.get(clientId)) {
if (exists(realm, session, clientId, roleRep)) {
switch (partialImportRep.getPolicy()) {
case SKIP:
toSkip.get(clientId).add(roleRep);
break;
case OVERWRITE:
toOverwrite.get(clientId).add(roleRep);
break;
default:
throw exists(existsMessage(clientId, roleRep));
}
}
}
}
}
protected ErrorResponseException exists(String message) {
Response error = ErrorResponse.exists(message);
return new ErrorResponseException(error);
}
protected ErrorResponseException noClientFound(String clientId) {
String message = "Can not import client roles for nonexistent client named " + clientId;
Response error = ErrorResponse.error(message, Response.Status.PRECONDITION_FAILED);
return new ErrorResponseException(error);
}
public PartialImportResult overwritten(String clientId, String modelId, RoleRepresentation roleRep) {
return PartialImportResult.overwritten(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep);
}
public PartialImportResult skipped(String clientId, String modelId, RoleRepresentation roleRep) {
return PartialImportResult.skipped(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep);
}
public PartialImportResult added(String clientId, String modelId, RoleRepresentation roleRep) {
return PartialImportResult.added(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep);
}
public String getModelId(RealmModel realm, String clientId) {
return realm.getClientByClientId(clientId).getId();
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.List;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
/**
* PartialImport handler for Clients.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class ClientsPartialImport extends AbstractPartialImport<ClientRepresentation> {
@Override
public List<ClientRepresentation> getRepList(PartialImportRepresentation partialImportRep) {
return partialImportRep.getClients();
}
@Override
public String getName(ClientRepresentation clientRep) {
return clientRep.getClientId();
}
@Override
public String getModelId(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) {
return realm.getClientByClientId(getName(clientRep)).getId();
}
@Override
public boolean exists(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) {
return realm.getClientByClientId(getName(clientRep)) != null;
}
@Override
public String existsMessage(ClientRepresentation clientRep) {
return "Client id '" + getName(clientRep) + "' already exists";
}
@Override
public ResourceType getResourceType() {
return ResourceType.CLIENT;
}
@Override
public void remove(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) {
ClientModel clientModel = realm.getClientByClientId(getName(clientRep));
new ClientManager(new RealmManager(session)).removeClient(realm, clientModel);
}
@Override
public void create(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) {
clientRep.setId(KeycloakModelUtils.generateId());
List<ProtocolMapperRepresentation> mappers = clientRep.getProtocolMappers();
if (mappers != null) {
for (ProtocolMapperRepresentation mapper : mappers) {
mapper.setId(KeycloakModelUtils.generateId());
}
}
RepresentationToModel.createClient(session, realm, clientRep, true);
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import javax.ws.rs.core.Response;
/**
* An exception that can hold a Response object.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class ErrorResponseException extends Exception {
private final Response response;
public ErrorResponseException(Response response) {
this.response = response;
}
public Response getResponse() {
return response;
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.List;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.PartialImportRepresentation;
/**
* PartialImport handler for Identitiy Providers.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class IdentityProvidersPartialImport extends AbstractPartialImport<IdentityProviderRepresentation> {
@Override
public List<IdentityProviderRepresentation> getRepList(PartialImportRepresentation partialImportRep) {
return partialImportRep.getIdentityProviders();
}
@Override
public String getName(IdentityProviderRepresentation idpRep) {
return idpRep.getAlias();
}
@Override
public String getModelId(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) {
return realm.getIdentityProviderByAlias(getName(idpRep)).getInternalId();
}
@Override
public boolean exists(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) {
return realm.getIdentityProviderByAlias(getName(idpRep)) != null;
}
@Override
public String existsMessage(IdentityProviderRepresentation idpRep) {
return "Identity Provider '" + getName(idpRep) + "' already exists.";
}
@Override
public ResourceType getResourceType() {
return ResourceType.IDP;
}
@Override
public void remove(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) {
realm.removeIdentityProviderByAlias(getName(idpRep));
}
@Override
public void create(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) {
idpRep.setInternalId(KeycloakModelUtils.generateId());
IdentityProviderModel identityProvider = RepresentationToModel.toModel(realm, idpRep);
realm.addIdentityProvider(identityProvider);
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.PartialImportRepresentation;
/**
* Main interface for PartialImport handlers.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public interface PartialImport<T> {
/**
* Find which resources will need to be skipped or overwritten. Also,
* do a preliminary check for errors.
*
* @param rep Everything in the PartialImport request.
* @param realm Realm to be imported into.
* @param session The KeycloakSession.
* @throws ErrorResponseException If the PartialImport can not be performed,
* throw this exception.
*/
public void prepare(PartialImportRepresentation rep,
RealmModel realm,
KeycloakSession session) throws ErrorResponseException;
/**
* Delete resources that will be overwritten. This is done separately so
* that it can be called for all resource types before calling all the doImports.
*
* It was found that doing delete/add per resource causes errors because of
* cascading deletes.
*
* @param realm Realm to be imported into.
* @param session The KeycloakSession
*/
public void removeOverwrites(RealmModel realm, KeycloakSession session);
/**
* Create (or re-create) all the imported resources.
*
* @param rep Everything in the PartialImport request.
* @param realm Realm to be imported into.
* @param session The KeycloakSession.
* @return The final results of the PartialImport request.
* @throws ErrorResponseException if an error was detected trying to doImport a resource.
*/
public PartialImportResults doImport(PartialImportRepresentation rep,
RealmModel realm,
KeycloakSession session) throws ErrorResponseException;
}

View file

@ -0,0 +1,107 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.core.Response;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.services.resources.admin.AdminEventBuilder;
/**
* This class manages the PartialImport handlers.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class PartialImportManager {
private final List<PartialImport> partialImports = new ArrayList<>();
private final PartialImportRepresentation rep;
private final KeycloakSession session;
private final RealmModel realm;
private final AdminEventBuilder adminEvent;
public PartialImportManager(PartialImportRepresentation rep, KeycloakSession session,
RealmModel realm, AdminEventBuilder adminEvent) {
this.rep = rep;
this.session = session;
this.realm = realm;
this.adminEvent = adminEvent;
// Do not change the order of these!!!
partialImports.add(new ClientsPartialImport());
partialImports.add(new RolesPartialImport());
partialImports.add(new IdentityProvidersPartialImport());
partialImports.add(new UsersPartialImport());
}
public Response saveResources() {
PartialImportResults results = new PartialImportResults();
for (PartialImport partialImport : partialImports) {
try {
partialImport.prepare(rep, realm, session);
} catch (ErrorResponseException error) {
if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly();
return error.getResponse();
}
}
for (PartialImport partialImport : partialImports) {
try {
partialImport.removeOverwrites(realm, session);
results.addAllResults(partialImport.doImport(rep, realm, session));
} catch (ErrorResponseException error) {
if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly();
return error.getResponse();
}
}
for (PartialImportResult result : results.getResults()) {
switch (result.getAction()) {
case ADDED : addedEvent(result); break;
case OVERWRITTEN: overwrittenEvent(result); break;
}
}
if (session.getTransaction().isActive()) {
session.getTransaction().commit();
}
return Response.ok(results).build();
}
private void addedEvent(PartialImportResult result) {
adminEvent.operation(OperationType.CREATE)
.resourcePath(result.getResourceType().getPath(), result.getId())
.representation(result.getRepresentation())
.success();
};
private void overwrittenEvent(PartialImportResult result) {
adminEvent.operation(OperationType.UPDATE)
.resourcePath(result.getResourceType().getPath(), result.getId())
.representation(result.getRepresentation())
.success();
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import org.codehaus.jackson.annotate.JsonIgnore;
/**
* This class represents a single result for a resource imported.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class PartialImportResult {
private final Action action;
private final ResourceType resourceType;
private final String resourceName;
private final String id;
private final Object representation;
private PartialImportResult(Action action, ResourceType resourceType, String resourceName, String id, Object representation) {
this.action = action;
this.resourceType = resourceType;
this.resourceName = resourceName;
this.id = id;
this.representation = representation;
};
public static PartialImportResult skipped(ResourceType resourceType, String resourceName, String id, Object representation) {
return new PartialImportResult(Action.SKIPPED, resourceType, resourceName, id, representation);
}
public static PartialImportResult added(ResourceType resourceType, String resourceName, String id, Object representation) {
return new PartialImportResult(Action.ADDED, resourceType, resourceName, id, representation);
}
public static PartialImportResult overwritten(ResourceType resourceType, String resourceName, String id, Object representation) {
return new PartialImportResult(Action.OVERWRITTEN, resourceType, resourceName, id, representation);
}
public Action getAction() {
return action;
}
public ResourceType getResourceType() {
return resourceType;
}
public String getResourceName() {
return resourceName;
}
public String getId() {
return id;
}
@JsonIgnore
public Object getRepresentation() {
return representation;
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.HashSet;
import java.util.Set;
/**
* Aggregates all the PartialImportResult objects.
* These results are used in the admin UI and for creating admin events.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class PartialImportResults {
private final Set<PartialImportResult> importResults = new HashSet<>();
public void addResult(PartialImportResult result) {
importResults.add(result);
}
public void addAllResults(PartialImportResults results) {
importResults.addAll(results.getResults());
}
public int getAdded() {
int added = 0;
for (PartialImportResult result : importResults) {
if (result.getAction() == Action.ADDED) added++;
}
return added;
}
public int getOverwritten() {
int overwritten = 0;
for (PartialImportResult result : importResults) {
if (result.getAction() == Action.OVERWRITTEN) overwritten++;
}
return overwritten;
}
public int getSkipped() {
int skipped = 0;
for (PartialImportResult result : importResults) {
if (result.getAction() == Action.SKIPPED) skipped++;
}
return skipped;
}
public Set<PartialImportResult> getResults() {
return importResults;
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.List;
import java.util.Set;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.services.resources.admin.RoleResource;
/**
* PartialImport handler for Realm Roles.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class RealmRolesPartialImport extends AbstractPartialImport<RoleRepresentation> {
public Set<RoleRepresentation> getToOverwrite() {
return this.toOverwrite;
}
public Set<RoleRepresentation> getToSkip() {
return this.toSkip;
}
@Override
public List<RoleRepresentation> getRepList(PartialImportRepresentation partialImportRep) {
if (partialImportRep.getRoles() == null) return null;
return partialImportRep.getRoles().getRealm();
}
@Override
public String getName(RoleRepresentation roleRep) {
if (roleRep.getName() == null)
throw new IllegalStateException("Realm role to import does not have a name");
return roleRep.getName();
}
@Override
public String getModelId(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) {
for (RoleModel role : realm.getRoles()) {
if (getName(roleRep).equals(role.getName())) return role.getId();
}
return null;
}
@Override
public boolean exists(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) {
for (RoleModel role : realm.getRoles()) {
if (getName(roleRep).equals(role.getName())) return true;
}
return false;
}
@Override
public String existsMessage(RoleRepresentation roleRep) {
return "Realm role '" + getName(roleRep) + "' already exists.";
}
@Override
public ResourceType getResourceType() {
return ResourceType.REALM_ROLE;
}
@Override
public void remove(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) {
RoleModel role = realm.getRole(getName(roleRep));
RoleHelper helper = new RoleHelper(realm);
helper.deleteRole(role);
}
@Override
public void create(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) {
realm.addRole(getName(roleRep));
}
public static class RoleHelper extends RoleResource {
public RoleHelper(RealmModel realm) {
super(realm);
}
@Override
protected void deleteRole(RoleModel role) {
super.deleteRole(role);
}
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
/**
* Enum for each resource type that can be partially imported.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public enum ResourceType {
USER, CLIENT, IDP, REALM_ROLE, CLIENT_ROLE;
/**
* Used to create the admin path in events.
*
* @return The resource portion of the path.
*/
public String getPath() {
switch(this) {
case USER: return "users";
case CLIENT: return "clients";
case IDP: return "identity-provider-settings";
case REALM_ROLE: return "realms";
case CLIENT_ROLE: return "clients";
default: return "";
}
}
@Override
public String toString() {
switch(this) {
case USER: return "User";
case CLIENT: return "Client";
case IDP: return "Identity Provider";
case REALM_ROLE: return "Realm Role";
case CLIENT_ROLE: return "Client Role";
default: return super.toString();
}
}
}

View file

@ -0,0 +1,233 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;
import org.keycloak.services.ErrorResponse;
/**
* This class handles both realm roles and client roles. It delegates to
* RealmRolesPartialImport and ClientRolesPartialImport, which are no longer used
* directly by the PartialImportManager.
*
* The strategy is to utilize RepresentationToModel.importRoles(). That way,
* the complex code for bulk creation of roles is kept in one place. To do this, the
* logic for skip needs to remove the roles that are going to be skipped so that
* importRoles() doesn't know about them. The logic for overwrite needs to delete
* the overwritten roles before importRoles() is called.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class RolesPartialImport implements PartialImport<RolesRepresentation> {
protected static Logger logger = Logger.getLogger(RolesPartialImport.class);
private Set<RoleRepresentation> realmRolesToOverwrite;
private Set<RoleRepresentation> realmRolesToSkip;
private Map<String, Set<RoleRepresentation>> clientRolesToOverwrite;
private Map<String, Set<RoleRepresentation>> clientRolesToSkip;
private final RealmRolesPartialImport realmRolesPI = new RealmRolesPartialImport();
private final ClientRolesPartialImport clientRolesPI = new ClientRolesPartialImport();
@Override
public void prepare(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
prepareRealmRoles(rep, realm, session);
prepareClientRoles(rep, realm, session);
}
private void prepareRealmRoles(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
if (!rep.hasRealmRoles()) return;
realmRolesPI.prepare(rep, realm, session);
this.realmRolesToOverwrite = realmRolesPI.getToOverwrite();
this.realmRolesToSkip = realmRolesPI.getToSkip();
}
private void prepareClientRoles(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
if (!rep.hasClientRoles()) return;
clientRolesPI.prepare(rep, realm, session);
this.clientRolesToOverwrite = clientRolesPI.getToOverwrite();
this.clientRolesToSkip = clientRolesPI.getToSkip();
}
@Override
public void removeOverwrites(RealmModel realm, KeycloakSession session) {
deleteClientRoleOverwrites(realm);
deleteRealmRoleOverwrites(realm, session);
}
@Override
public PartialImportResults doImport(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
PartialImportResults results = new PartialImportResults();
if (!rep.hasRealmRoles() && !rep.hasClientRoles()) return results;
// finalize preparation and add results for skips
removeRealmRoleSkips(results, rep, realm, session);
removeClientRoleSkips(results, rep, realm);
if (rep.hasRealmRoles()) setUniqueIds(rep.getRoles().getRealm());
if (rep.hasClientRoles()) setUniqueIds(rep.getRoles().getClient());
try {
RepresentationToModel.importRoles(rep.getRoles(), realm);
} catch (Exception e) {
logger.error("Error importing roles", e);
throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR));
}
// add "add" results for new roles created
realmRoleAdds(results, rep, realm, session);
clientRoleAdds(results, rep, realm);
// add "overwritten" results for roles overwritten
addResultsForOverwrittenRealmRoles(results, realm, session);
addResultsForOverwrittenClientRoles(results, realm);
return results;
}
private void setUniqueIds(List<RoleRepresentation> realmRoles) {
for (RoleRepresentation realmRole : realmRoles) {
realmRole.setId(KeycloakModelUtils.generateId());
}
}
private void setUniqueIds(Map<String, List<RoleRepresentation>> clientRoles) {
for (String clientId : clientRoles.keySet()) {
for (RoleRepresentation clientRole : clientRoles.get(clientId)) {
clientRole.setId(KeycloakModelUtils.generateId());
}
}
}
private void removeRealmRoleSkips(PartialImportResults results,
PartialImportRepresentation rep,
RealmModel realm,
KeycloakSession session) {
if (isEmpty(realmRolesToSkip)) return;
for (RoleRepresentation roleRep : realmRolesToSkip) {
rep.getRoles().getRealm().remove(roleRep);
String modelId = realmRolesPI.getModelId(realm, session, roleRep);
results.addResult(realmRolesPI.skipped(modelId, roleRep));
}
}
private void removeClientRoleSkips(PartialImportResults results,
PartialImportRepresentation rep,
RealmModel realm) {
if (isEmpty(clientRolesToSkip)) return;
for (String clientId : clientRolesToSkip.keySet()) {
for (RoleRepresentation roleRep : clientRolesToSkip.get(clientId)) {
rep.getRoles().getClient().get(clientId).remove(roleRep);
String modelId = clientRolesPI.getModelId(realm, clientId);
results.addResult(clientRolesPI.skipped(clientId, modelId, roleRep));
}
}
}
private void deleteRealmRoleOverwrites(RealmModel realm, KeycloakSession session) {
if (isEmpty(realmRolesToOverwrite)) return;
for (RoleRepresentation roleRep : realmRolesToOverwrite) {
realmRolesPI.remove(realm, session, roleRep);
}
}
private void addResultsForOverwrittenRealmRoles(PartialImportResults results, RealmModel realm, KeycloakSession session) {
if (isEmpty(realmRolesToOverwrite)) return;
for (RoleRepresentation roleRep : realmRolesToOverwrite) {
String modelId = realmRolesPI.getModelId(realm, session, roleRep);
results.addResult(realmRolesPI.overwritten(modelId, roleRep));
}
}
private void deleteClientRoleOverwrites(RealmModel realm) {
if (isEmpty(clientRolesToOverwrite)) return;
for (String clientId : clientRolesToOverwrite.keySet()) {
for (RoleRepresentation roleRep : clientRolesToOverwrite.get(clientId)) {
clientRolesPI.deleteRole(realm, clientId, roleRep);
}
}
}
private void addResultsForOverwrittenClientRoles(PartialImportResults results, RealmModel realm) {
if (isEmpty(clientRolesToOverwrite)) return;
for (String clientId : clientRolesToOverwrite.keySet()) {
for (RoleRepresentation roleRep : clientRolesToOverwrite.get(clientId)) {
String modelId = clientRolesPI.getModelId(realm, clientId);
results.addResult(clientRolesPI.overwritten(clientId, modelId, roleRep));
}
}
}
private boolean isEmpty(Set set) {
return (set == null) || (set.isEmpty());
}
private boolean isEmpty(Map map) {
return (map == null) || (map.isEmpty());
}
private void realmRoleAdds(PartialImportResults results,
PartialImportRepresentation rep,
RealmModel realm,
KeycloakSession session) {
if (!rep.hasRealmRoles()) return;
for (RoleRepresentation roleRep : rep.getRoles().getRealm()) {
if (realmRolesToOverwrite.contains(roleRep)) continue;
if (realmRolesToSkip.contains(roleRep)) continue;
String modelId = realmRolesPI.getModelId(realm, session, roleRep);
results.addResult(realmRolesPI.added(modelId, roleRep));
}
}
private void clientRoleAdds(PartialImportResults results,
PartialImportRepresentation rep,
RealmModel realm) {
if (!rep.hasClientRoles()) return;
Map<String, List<RoleRepresentation>> repList = clientRolesPI.getRepList(rep);
for (String clientId : repList.keySet()) {
for (RoleRepresentation roleRep : repList.get(clientId)) {
if (clientRolesToOverwrite.get(clientId).contains(roleRep)) continue;
if (clientRolesToSkip.get(clientId).contains(roleRep)) continue;
String modelId = clientRolesPI.getModelId(realm, clientId);
results.addResult(clientRolesPI.added(clientId, modelId, roleRep));
}
}
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.partialimport;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.UserManager;
/**
* PartialImport handler for users.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
public class UsersPartialImport extends AbstractPartialImport<UserRepresentation> {
// Sometimes session.users().getUserByUsername() doesn't work right after create,
// so we cache the created id here.
private final Map<String, String> createdIds = new HashMap<>();
@Override
public List<UserRepresentation> getRepList(PartialImportRepresentation partialImportRep) {
return partialImportRep.getUsers();
}
@Override
public String getName(UserRepresentation user) {
if (user.getUsername() != null) return user.getUsername();
return user.getEmail();
}
@Override
public String getModelId(RealmModel realm, KeycloakSession session, UserRepresentation user) {
if (createdIds.containsKey(getName(user))) return createdIds.get(getName(user));
String userName = user.getUsername();
if (userName != null) {
return session.users().getUserByUsername(userName, realm).getId();
} else {
String email = user.getEmail();
return session.users().getUserByEmail(email, realm).getId();
}
}
@Override
public boolean exists(RealmModel realm, KeycloakSession session, UserRepresentation user) {
return userNameExists(realm, session, user) || userEmailExists(realm, session, user);
}
private boolean userNameExists(RealmModel realm, KeycloakSession session, UserRepresentation user) {
return session.users().getUserByUsername(user.getUsername(), realm) != null;
}
private boolean userEmailExists(RealmModel realm, KeycloakSession session, UserRepresentation user) {
return (user.getEmail() != null) &&
(session.users().getUserByEmail(user.getEmail(), realm) != null);
}
@Override
public String existsMessage(UserRepresentation user) {
if (user.getEmail() == null) {
return "User with user name " + getName(user) + " already exists.";
}
return "User with user name " + getName(user) + " or with email " + user.getEmail() + " already exists.";
}
@Override
public ResourceType getResourceType() {
return ResourceType.USER;
}
@Override
public void remove(RealmModel realm, KeycloakSession session, UserRepresentation user) {
UserModel userModel = session.users().getUserByUsername(user.getUsername(), realm);
if (userModel == null) {
userModel = session.users().getUserByEmail(user.getEmail(), realm);
}
boolean success = new UserManager(session).removeUser(realm, userModel);
if (!success) throw new RuntimeException("Unable to overwrite user " + getName(user));
}
@Override
public void create(RealmModel realm, KeycloakSession session, UserRepresentation user) {
Map<String, ClientModel> apps = realm.getClientNameMap();
user.setId(KeycloakModelUtils.generateId());
UserModel userModel = RepresentationToModel.createUser(session, realm, user, apps);
if (userModel == null) throw new RuntimeException("Unable to create user " + getName(user));
createdIds.put(getName(user), userModel.getId());
}
}

View file

@ -21,7 +21,7 @@ import org.keycloak.common.util.Time;
import javax.ws.rs.core.UriInfo;
public class AdminEventBuilder {
private static final Logger log = Logger.getLogger(AdminEventBuilder.class);
private EventStoreProvider store;
@ -59,17 +59,17 @@ public class AdminEventBuilder {
authUser(auth.getUser());
authIpAddress(clientConnection.getRemoteAddr());
}
public AdminEventBuilder realm(RealmModel realm) {
adminEvent.setRealmId(realm.getId());
return this;
}
public AdminEventBuilder realm(String realmId) {
adminEvent.setRealmId(realmId);
return this;
}
public AdminEventBuilder operation(OperationType e) {
adminEvent.setOperationType(e);
return this;
@ -123,6 +123,18 @@ public class AdminEventBuilder {
return this;
}
public AdminEventBuilder resourcePath(String... pathElements) {
StringBuilder sb = new StringBuilder();
for (String element : pathElements) {
sb.append("/");
sb.append(element);
}
if (pathElements.length > 0) sb.deleteCharAt(0); // remove leading '/'
adminEvent.setResourcePath(sb.toString());
return this;
}
public AdminEventBuilder resourcePath(UriInfo uriInfo) {
String path = getResourcePath(uriInfo);
adminEvent.setResourcePath(path);
@ -155,7 +167,7 @@ public class AdminEventBuilder {
adminEvent.setError(error);
send();
}
public AdminEventBuilder representation(Object value) {
if (value == null || value.equals("")) {
return this;
@ -167,7 +179,7 @@ public class AdminEventBuilder {
}
return this;
}
public AdminEvent getEvent() {
return adminEvent;
}
@ -190,7 +202,7 @@ public class AdminEventBuilder {
log.error("Failed to save event", t);
}
}
if (listeners != null) {
for (EventListenerProvider l : listeners) {
try {

View file

@ -68,7 +68,7 @@ public class ClientResource {
private AdminEventBuilder adminEvent;
protected ClientModel client;
protected KeycloakSession session;
@Context
protected UriInfo uriInfo;
@ -107,11 +107,7 @@ public class ClientResource {
auth.requireManage();
try {
if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) {
new ClientManager(new RealmManager(session)).enableServiceAccount(client);;
}
RepresentationToModel.updateClient(rep, client);
updateClientFromRep(rep, client, session);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
return Response.noContent().build();
} catch (ModelDuplicateException e) {
@ -119,6 +115,13 @@ public class ClientResource {
}
}
public static void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException {
if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) {
new ClientManager(new RealmManager(session)).enableServiceAccount(client);
}
RepresentationToModel.updateClient(rep, client);
}
/**
* Get representation of the client
@ -381,9 +384,9 @@ public class ClientResource {
auth.requireManage();
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
return new ResourceAdminManager(session).pushClientRevocationPolicy(uriInfo.getRequestUri(), realm, client);
}
/**
* Get application session count
*

View file

@ -58,7 +58,7 @@ public class IdentityProviderResource {
private final KeycloakSession session;
private final IdentityProviderModel identityProviderModel;
private final AdminEventBuilder adminEvent;
@Context private UriInfo uriInfo;
public IdentityProviderResource(RealmAuth auth, RealmModel realm, KeycloakSession session, IdentityProviderModel identityProviderModel, AdminEventBuilder adminEvent) {
@ -94,9 +94,9 @@ public class IdentityProviderResource {
this.auth.requireManage();
this.realm.removeIdentityProviderByAlias(this.identityProviderModel.getAlias());
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
return Response.noContent().build();
}
@ -113,30 +113,34 @@ public class IdentityProviderResource {
try {
this.auth.requireManage();
String internalId = providerRep.getInternalId();
String newProviderId = providerRep.getAlias();
String oldProviderId = getProviderIdByInternalId(this.realm, internalId);
updateIdpFromRep(providerRep, realm, session);
this.realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep));
if (oldProviderId != null && !oldProviderId.equals(newProviderId)) {
// Admin changed the ID (alias) of identity provider. We must update all clients and users
logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId);
updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm, false), oldProviderId, newProviderId);
}
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success();
return Response.noContent().build();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("Identity Provider " + providerRep.getAlias() + " already exists");
}
}
public static void updateIdpFromRep(IdentityProviderRepresentation providerRep, RealmModel realm, KeycloakSession session) {
String internalId = providerRep.getInternalId();
String newProviderId = providerRep.getAlias();
String oldProviderId = getProviderIdByInternalId(realm, internalId);
realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep));
if (oldProviderId != null && !oldProviderId.equals(newProviderId)) {
// Admin changed the ID (alias) of identity provider. We must update all clients and users
logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId);
updateUsersAfterProviderAliasChange(session.users().getUsers(realm, false), oldProviderId, newProviderId, realm, session);
}
}
// return ID of IdentityProvider from realm based on internalId of this provider
private String getProviderIdByInternalId(RealmModel realm, String providerInternalId) {
private static String getProviderIdByInternalId(RealmModel realm, String providerInternalId) {
List<IdentityProviderModel> providerModels = realm.getIdentityProviders();
for (IdentityProviderModel providerModel : providerModels) {
if (providerModel.getInternalId().equals(providerInternalId)) {
@ -147,17 +151,17 @@ public class IdentityProviderResource {
return null;
}
private void updateUsersAfterProviderAliasChange(List<UserModel> users, String oldProviderId, String newProviderId) {
private static void updateUsersAfterProviderAliasChange(List<UserModel> users, String oldProviderId, String newProviderId, RealmModel realm, KeycloakSession session) {
for (UserModel user : users) {
FederatedIdentityModel federatedIdentity = this.session.users().getFederatedIdentity(user, oldProviderId, this.realm);
FederatedIdentityModel federatedIdentity = session.users().getFederatedIdentity(user, oldProviderId, realm);
if (federatedIdentity != null) {
// Remove old link first
this.session.users().removeFederatedIdentity(this.realm, user, oldProviderId);
session.users().removeFederatedIdentity(realm, user, oldProviderId);
// And create new
FederatedIdentityModel newFederatedIdentity = new FederatedIdentityModel(newProviderId, federatedIdentity.getUserId(), federatedIdentity.getUserName(),
federatedIdentity.getToken());
this.session.users().addFederatedIdentity(this.realm, user, newFederatedIdentity);
session.users().addFederatedIdentity(realm, user, newFederatedIdentity);
}
}
}
@ -263,10 +267,10 @@ public class IdentityProviderResource {
auth.requireManage();
IdentityProviderMapperModel model = RepresentationToModel.toModel(mapper);
model = realm.addIdentityProviderMapper(model);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId())
.representation(mapper).success();
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
}

View file

@ -66,6 +66,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.PatternSyntaxException;
import org.keycloak.partialimport.PartialImportManager;
import org.keycloak.representations.idm.PartialImportRepresentation;
/**
* Base resource class for the admin REST api of one realm
@ -241,7 +243,7 @@ public class RealmAdminResource {
for (final UserFederationProviderModel fedProvider : federationProviders) {
usersSyncManager.refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, realm.getId());
}
adminEvent.operation(OperationType.UPDATE).representation(rep).success();
return Response.noContent().build();
} catch (PatternSyntaxException e) {
@ -466,7 +468,7 @@ public class RealmAdminResource {
if (user != null) {
query.user(user);
}
if(dateFrom != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date from = null;
@ -477,7 +479,7 @@ public class RealmAdminResource {
}
query.fromDate(from);
}
if(dateTo != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date to = null;
@ -501,7 +503,7 @@ public class RealmAdminResource {
return query.getResultList();
}
/**
* Get admin events
*
@ -540,15 +542,15 @@ public class RealmAdminResource {
if (authClient != null) {
query.authClient(authClient);
}
if (authUser != null) {
query.authUser(authUser);
}
if (authIpAddress != null) {
query.authIpAddress(authIpAddress);
}
if (resourcePath != null) {
query.resourcePath(resourcePath);
}
@ -561,7 +563,7 @@ public class RealmAdminResource {
}
query.operation(t);
}
if(dateFrom != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date from = null;
@ -572,7 +574,7 @@ public class RealmAdminResource {
}
query.fromTime(from);
}
if(dateTo != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date to = null;
@ -606,7 +608,7 @@ public class RealmAdminResource {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clear(realm.getId());
}
/**
* Delete all admin events
*
@ -709,5 +711,18 @@ public class RealmAdminResource {
return ModelToRepresentation.toGroupHierarchy(found, true);
}
/**
* Partial import from a JSON file to an existing realm.
*
* @param rep
* @return
*/
@Path("partialImport")
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response partialImport(PartialImportRepresentation rep) {
auth.requireManage();
PartialImportManager partialImport = new PartialImportManager(rep, session, realm, adminEvent);
return partialImport.saveResources();
}
}

View file

@ -150,7 +150,7 @@ public class UsersResource {
}
}
updateUserFromRep(user, rep, attrsToRemove);
updateUserFromRep(user, rep, attrsToRemove, realm, session);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
if (session.getTransaction().isActive()) {
@ -189,7 +189,7 @@ public class UsersResource {
try {
UserModel user = session.users().addUser(realm, rep.getUsername());
Set<String> emptySet = Collections.emptySet();
updateUserFromRep(user, rep, emptySet);
updateUserFromRep(user, rep, emptySet, realm, session);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success();
@ -206,7 +206,7 @@ public class UsersResource {
}
}
private void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove) {
public static void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove, RealmModel realm, KeycloakSession session) {
if (realm.isEditUsernameAllowed()) {
user.setUsername(rep.getUsername());
}