Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2015-11-18 15:24:45 -05:00
commit 6989589e72
66 changed files with 1622 additions and 450 deletions

View file

@ -2,14 +2,14 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<artifactId>keycloak-client-registration-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.7.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-client-api</artifactId>
<name>Keycloak Client API</name>
<artifactId>keycloak-client-registration-api</artifactId>
<name>Keycloak Client Registration API</name>
<description/>
<dependencies>
@ -21,11 +21,6 @@
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -3,6 +3,7 @@ package org.keycloak.client.registration;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.keycloak.common.util.Base64;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
/**
@ -16,6 +17,11 @@ public abstract class Auth {
return new BearerTokenAuth(token);
}
public static Auth token(ClientInitialAccessPresentation initialAccess) {
return new BearerTokenAuth(initialAccess.getToken());
}
public static Auth token(ClientRepresentation client) {
return new BearerTokenAuth(client.getRegistrationAccessToken());
}

View file

@ -2,6 +2,7 @@ package org.keycloak.client.registration;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.codehaus.jackson.map.ObjectMapper;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.util.JsonSerialization;
@ -14,6 +15,11 @@ import java.io.InputStream;
*/
public class ClientRegistration {
public static final ObjectMapper outputMapper = new ObjectMapper();
static {
outputMapper.getSerializationConfig().addMixInAnnotations(ClientRepresentation.class, ClientRepresentationMixIn.class);
}
private final String DEFAULT = "default";
private final String INSTALLATION = "install";
@ -69,15 +75,16 @@ public class ClientRegistration {
httpUtil.doDelete(DEFAULT, clientId);
}
private String serialize(ClientRepresentation client) throws ClientRegistrationException {
public static String serialize(ClientRepresentation client) throws ClientRegistrationException {
try {
return JsonSerialization.writeValueAsString(client);
return outputMapper.writeValueAsString(client);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to write json object", e);
}
}
private <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
private static <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
try {
return JsonSerialization.readValue(inputStream, clazz);
} catch (IOException e) {

View file

@ -0,0 +1,13 @@
package org.keycloak.client.registration;
import org.codehaus.jackson.annotate.JsonIgnore;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
abstract class ClientRepresentationMixIn {
@JsonIgnore
String registrationAccessToken;
}

34
client-registration/cli/pom.xml Executable file
View file

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-client-registration-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.7.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-client-registration-cli</artifactId>
<name>Keycloak Client Registration CLI</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-registration-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,27 @@
package org.keycloak.client.registration.cli;
import org.jboss.aesh.console.AeshConsole;
import org.jboss.aesh.console.AeshConsoleBuilder;
import org.jboss.aesh.console.Prompt;
import org.jboss.aesh.console.settings.Settings;
import org.jboss.aesh.console.settings.SettingsBuilder;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationCLI {
public static void main(String[] args) {
Settings settings = new SettingsBuilder().logging(true).create();
AeshConsole aeshConsole = new AeshConsoleBuilder().settings(settings)
.prompt(new Prompt("[aesh@rules]$ "))
// .command()
.create();
aeshConsole.start();
}
}

19
client-registration/pom.xml Executable file
View file

@ -0,0 +1,19 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.7.0.Final-SNAPSHOT</version>
</parent>
<name>Keycloak Client Registration Parent</name>
<description/>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-client-registration-parent</artifactId>
<packaging>pom</packaging>
<modules>
<module>api</module>
<module>cli</module>
</modules>
</project>

View file

@ -59,7 +59,7 @@
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_ROLE" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addColumn tableName="CLIENT">
<column name="REGISTRATION_SECRET" type="VARCHAR(255)"/>
<column name="REGISTRATION_TOKEN" type="VARCHAR(255)"/>
</addColumn>
</changeSet>

View file

@ -0,0 +1,36 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessCreatePresentation {
private Integer expiration;
private Integer count;
public ClientInitialAccessCreatePresentation() {
}
public ClientInitialAccessCreatePresentation(Integer expiration, Integer count) {
this.expiration = expiration;
this.count = count;
}
public Integer getExpiration() {
return expiration;
}
public void setExpiration(Integer expiration) {
this.expiration = expiration;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}

View file

@ -0,0 +1,67 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessPresentation {
private String id;
private String token;
private Integer timestamp;
private Integer expiration;
private Integer count;
private Integer remainingCount;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Integer getTimestamp() {
return timestamp;
}
public void setTimestamp(Integer timestamp) {
this.timestamp = timestamp;
}
public Integer getExpiration() {
return expiration;
}
public void setExpiration(Integer expiration) {
this.expiration = expiration;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(Integer remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -19,6 +19,7 @@ public class TokenUtil {
public static final String TOKEN_TYPE_OFFLINE = "Offline";
public static boolean isOfflineTokenRequested(String scopeParam) {
if (scopeParam == null) {
return false;

View file

@ -109,6 +109,7 @@ realm-tab-email=Email
realm-tab-themes=Themes
realm-tab-cache=Cache
realm-tab-tokens=Tokens
realm-tab-client-initial-access=Initial Access Tokens
realm-tab-security-defenses=Security Defenses
realm-tab-general=General
add-realm=Add Realm
@ -470,3 +471,17 @@ identity-provider-mappers=Identity Provider Mappers
create-identity-provider-mapper=Create Identity Provider Mapper
add-identity-provider-mapper=Add Identity Provider Mapper
client.description.tooltip=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description}
expires=Expires
expiration=Expiration
count=Count
remainingCount=Remaining count
created=Created
back=Back
initial-access-tokens=Initial Access Tokens
add-initial-access-tokens=Add Initial Access Token
initial-access-token=Initial Access Token
initial-access.copyPaste.tooltip=Copy/paste the initial access token before navigating away from this page as it's not posible to retrieve later
continue=Continue
initial-access-token.confirm.title=Copy Initial Access Token
initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later

View file

@ -176,6 +176,27 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmTokenDetailCtrl'
})
.when('/realms/:realm/client-initial-access', {
templateUrl : resourceUrl + '/partials/client-initial-access.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
clientInitialAccess : function(ClientInitialAccessLoader) {
return ClientInitialAccessLoader();
}
},
controller : 'ClientInitialAccessCtrl'
})
.when('/realms/:realm/client-initial-access/create', {
templateUrl : resourceUrl + '/partials/client-initial-access-create.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
}
},
controller : 'ClientInitialAccessCreateCtrl'
})
.when('/realms/:realm/keys-settings', {
templateUrl : resourceUrl + '/partials/realm-keys.html',
resolve : {

View file

@ -1986,6 +1986,65 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
});
module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, ClientInitialAccess, Dialog, Notifications, $route) {
$scope.realm = realm;
$scope.clientInitialAccess = clientInitialAccess;
$scope.remove = function(id) {
Dialog.confirmDelete(id, 'initial access token', function() {
ClientInitialAccess.remove({ realm: realm.realm, id: id }, function() {
Notifications.success("The initial access token was deleted.");
$route.reload();
});
});
}
});
module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location, $translate) {
$scope.expirationUnit = 'Days';
$scope.expiration = TimeUnit.toUnit(0, $scope.expirationUnit);
$scope.count = 1;
$scope.realm = realm;
$scope.$watch('expirationUnit', function(to, from) {
$scope.expiration = TimeUnit.convert($scope.expiration, from, to);
});
$scope.save = function() {
var expiration = TimeUnit.toSeconds($scope.expiration, $scope.expirationUnit);
ClientInitialAccess.save({
realm: realm.realm
}, { expiration: expiration, count: $scope.count}, function (data) {
console.debug(data);
$scope.id = data.id;
$scope.token = data.token;
});
};
$scope.cancel = function() {
$location.url('/realms/' + realm.realm + '/client-initial-access');
};
$scope.done = function() {
var btns = {
ok: {
label: $translate.instant('continue'),
cssClass: 'btn btn-primary'
},
cancel: {
label: $translate.instant('cancel'),
cssClass: 'btn btn-default'
}
}
var title = $translate.instant('initial-access-token.confirm.title');
var message = $translate.instant('initial-access-token.confirm.text');
Dialog.open(title, message, btns, function() {
$location.url('/realms/' + realm.realm + '/client-initial-access');
});
};
});

View file

@ -475,6 +475,13 @@ module.factory('GroupLoader', function(Loader, Group, $route, $q) {
});
});
module.factory('ClientInitialAccessLoader', function(Loader, ClientInitialAccess, $route) {
return Loader.query(ClientInitialAccess, function() {
return {
realm: $route.current.params.realm
}
});
});

View file

@ -102,6 +102,10 @@ module.service('Dialog', function($modal) {
openDialog(title, message, btns, '/templates/kc-modal-message.html').then(success, cancel);
}
dialog.open = function(title, message, btns, success, cancel) {
openDialog(title, message, btns, '/templates/kc-modal.html').then(success, cancel);
}
return dialog
});
@ -284,6 +288,13 @@ module.service('ServerInfo', function($resource, $q, $http) {
}
});
module.factory('ClientInitialAccess', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/clients-initial-access/:id', {
realm : '@realm',
id : '@id'
});
});
module.factory('ClientProtocolMapper', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/clients/:client/protocol-mappers/models/:id', {
@ -1548,11 +1559,4 @@ module.factory('UserGroupMapping', function($resource) {
method : 'PUT'
}
});
});
});

View file

@ -0,0 +1,63 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
<li>{{:: 'add-initial-access-tokens' | translate}}</li>
</ol>
<h1 data-ng-show="create">{{:: 'add-client' | translate}}</h1>
<form class="form-horizontal" name="createForm" novalidate kc-read-only="!access.manageRealm" data-ng-hide="token">
<div class="form-group">
<label class="col-md-2 control-label" for="expiration">{{:: 'expiration' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="0" max="31536000" data-ng-model="expiration" id="expiration"
name="expiration"/>
<select class="form-control" name="expirationUnit" data-ng-model="expirationUnit">
<option data-ng-selected="!expirationUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
<option value="Minutes">{{:: 'minutes' | translate}}</option>
<option value="Hours">{{:: 'hours' | translate}}</option>
<option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
<kc-tooltip>{{:: 'expiration.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="count">{{:: 'count' | translate}} </label>
<div class="col-sm-6">
<input class="form-control" type="text" id="count" name="count" required min="1" max="100" data-ng-model="count">
</div>
<kc-tooltip>{{:: 'count.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button kc-save>{{:: 'save' | translate}}</button>
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
<form name="displayForm" data-ng-show="token">
<div class="form-group">
<label for="initialAccessToken">{{:: 'initial-access-token' | translate}}</label>
<div>
<textarea type="text" id="initialAccessToken" name="initialAccessToken" class="form-control" rows="6" kc-select-action="click">{{token}}</textarea>
</div>
<kc-tooltip>{{:: 'initial-access.copyPaste.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<div>
<button class="btn btn-default" data-ng-click="done()">{{:: 'Back' | translate}}</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -0,0 +1,52 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<kc-tabs-realm></kc-tabs-realm>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="6">
<div class="form-inline">
<div class="form-group">
<div class="input-group">
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.id" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
<div class="input-group-addon">
<i class="fa fa-search" type="submit"></i>
</div>
</div>
</div>
<div class="pull-right" data-ng-show="access.manageRealm">
<a id="createClient" class="btn btn-default" href="#/realms/{{realm.realm}}/client-initial-access/create">{{:: 'create' | translate}}</a>
</div>
</div>
</th>
</tr>
<tr data-ng-hide="clients.length == 0">
<th>{{:: 'id' | translate}}</th>
<th>{{:: 'created' | translate}}</th>
<th>{{:: 'expires' | translate}}</th>
<th>{{:: 'count' | translate}}</th>
<th>{{:: 'remainingCount' | translate}}</th>
<th>{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="ia in clientInitialAccess | filter:search | orderBy:'timestamp'">
<td>{{ia.id}}</td>
<td>{{(ia.timestamp * 1000)|date:'shortDate'}}&nbsp;{{(ia.timestamp * 1000)|date:'mediumTime'}}</td>
<td><span data-ng-show="ia.expiration > 0">{{((ia.timestamp + ia.expiration) * 1000)|date:'shortDate'}}&nbsp;{{((ia.timestamp + ia.expiration) * 1000)|date:'mediumTime'}}</span></td>
<td>{{ia.count}}</td>
<td>{{ia.remainingCount}}</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" data-ng-click="remove(ia.id)">{{:: 'delete' | translate}}</button>
</td>
</tr>
<tr data-ng-show="(clients | filter:search).length == 0">
<td class="text-muted" colspan="3" data-ng-show="search.clientId">{{:: 'no-results' | translate}}</td>
<td class="text-muted" colspan="3" data-ng-hide="search.clientId">{{:: 'no-clients-available' | translate}}</td>
</tr>
</tbody>
</table>
</div>
<kc-menu></kc-menu>

View file

@ -13,6 +13,7 @@
<li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">{{:: 'realm-tab-themes' | translate}}</a></li>
<li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">{{:: 'realm-tab-cache' | translate}}</a></li>
<li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">{{:: 'realm-tab-tokens' | translate}}</a></li>
<li ng-class="{active: path[2] == 'client-initial-access'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'realm-tab-client-initial-access' | translate}}</a></li>
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
</ul>
</div>

View file

@ -0,0 +1,31 @@
package org.keycloak.admin.client.resource;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface ClientInitialAccessResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation rep);
@GET
@Produces(MediaType.APPLICATION_JSON)
List<ClientInitialAccessPresentation> list();
@DELETE
@Path("{id}")
void delete(final @PathParam("id") String id);
}

View file

@ -56,5 +56,8 @@ public interface RealmResource {
@Path("client-session-stats")
@GET
List<Map<String, String>> getClientSessionStats();
@Path("clients-initial-access")
ClientInitialAccessResource clientInitialAccess();
}

View file

@ -0,0 +1,22 @@
package org.keycloak.models;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface ClientInitialAccessModel {
String getId();
RealmModel getRealm();
int getTimestamp();
int getExpiration();
int getCount();
int getRemainingCount();
void decreaseRemainingCount();
}

View file

@ -90,8 +90,8 @@ public interface ClientModel extends RoleContainerModel {
String getSecret();
public void setSecret(String secret);
String getRegistrationSecret();
void setRegistrationSecret(String registrationSecret);
String getRegistrationToken();
void setRegistrationToken(String registrationToken);
boolean isFullScopeAllowed();
void setFullScopeAllowed(boolean value);

View file

@ -63,6 +63,11 @@ public interface UserSessionProvider extends Provider {
UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline);
ClientSessionModel importClientSession(ClientSessionModel persistentClientSession, boolean offline);
ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count);
ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id);
void removeClientInitialAccessModel(RealmModel realm, String id);
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
void close();
}

View file

@ -17,7 +17,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private boolean enabled;
private String clientAuthenticatorType;
private String secret;
private String registrationSecret;
private String registrationToken;
private String protocol;
private int notBefore;
private boolean publicClient;
@ -91,12 +91,12 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.secret = secret;
}
public String getRegistrationSecret() {
return registrationSecret;
public String getRegistrationToken() {
return registrationToken;
}
public void setRegistrationSecret(String registrationSecret) {
this.registrationSecret = registrationSecret;
public void setRegistrationToken(String registrationToken) {
this.registrationToken = registrationToken;
}
public int getNotBefore() {

View file

@ -50,8 +50,6 @@ import java.util.UUID;
*/
public final class KeycloakModelUtils {
private static final int RANDOM_PASSWORD_BYTES = 32;
private KeycloakModelUtils() {
}
@ -189,16 +187,6 @@ public final class KeycloakModelUtils {
return secret;
}
public static void generateRegistrationAccessToken(ClientModel client) {
client.setRegistrationSecret(generatePassword());
}
public static String generatePassword() {
byte[] buf = new byte[RANDOM_PASSWORD_BYTES];
new SecureRandom().nextBytes(buf);
return Base64Url.encode(buf);
}
public static String getDefaultClientAuthenticatorType() {
return "client-secret";
}

View file

@ -418,7 +418,6 @@ public class ModelToRepresentation {
rep.setNotBefore(clientModel.getNotBefore());
rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout());
rep.setClientAuthenticatorType(clientModel.getClientAuthenticatorType());
rep.setRegistrationAccessToken(clientModel.getRegistrationSecret());
Set<String> redirectUris = clientModel.getRedirectUris();
if (redirectUris != null) {

View file

@ -797,8 +797,6 @@ public class RepresentationToModel {
KeycloakModelUtils.generateSecret(client);
}
client.setRegistrationSecret(resourceRep.getRegistrationAccessToken());
if (resourceRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : resourceRep.getAttributes().entrySet()) {
client.setAttribute(entry.getKey(), entry.getValue());
@ -875,7 +873,6 @@ public class RepresentationToModel {
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
if (rep.getClientAuthenticatorType() != null) resource.setClientAuthenticatorType(rep.getClientAuthenticatorType());
if (rep.getRegistrationAccessToken() != null) resource.setRegistrationSecret(rep.getRegistrationAccessToken());
resource.updateClient();
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());

View file

@ -120,14 +120,14 @@ public class ClientAdapter implements ClientModel {
getDelegateForUpdate();
updated.setSecret(secret);
}
public String getRegistrationSecret() {
if (updated != null) return updated.getRegistrationSecret();
return cached.getRegistrationSecret();
public String getRegistrationToken() {
if (updated != null) return updated.getRegistrationToken();
return cached.getRegistrationToken();
}
public void setRegistrationSecret(String registrationsecret) {
public void setRegistrationToken(String registrationToken) {
getDelegateForUpdate();
updated.setRegistrationSecret(registrationsecret);
updated.setRegistrationToken(registrationToken);
}
public boolean isPublicClient() {

View file

@ -31,7 +31,7 @@ public class CachedClient implements Serializable {
private boolean enabled;
private String clientAuthenticatorType;
private String secret;
private String registrationSecret;
private String registrationToken;
private String protocol;
private Map<String, String> attributes = new HashMap<String, String>();
private boolean publicClient;
@ -58,7 +58,7 @@ public class CachedClient implements Serializable {
id = model.getId();
clientAuthenticatorType = model.getClientAuthenticatorType();
secret = model.getSecret();
registrationSecret = model.getRegistrationSecret();
registrationToken = model.getRegistrationToken();
clientId = model.getClientId();
name = model.getName();
description = model.getDescription();
@ -131,8 +131,8 @@ public class CachedClient implements Serializable {
return secret;
}
public String getRegistrationSecret() {
return registrationSecret;
public String getRegistrationToken() {
return registrationToken;
}
public boolean isPublicClient() {

View file

@ -178,13 +178,13 @@ public class ClientAdapter implements ClientModel {
}
@Override
public String getRegistrationSecret() {
return entity.getRegistrationSecret();
public String getRegistrationToken() {
return entity.getRegistrationToken();
}
@Override
public void setRegistrationSecret(String registrationSecret) {
entity.setRegistrationSecret(registrationSecret);
public void setRegistrationToken(String registrationToken) {
entity.setRegistrationToken(registrationToken);
}
@Override

View file

@ -42,8 +42,8 @@ public class ClientEntity {
private boolean enabled;
@Column(name="SECRET")
private String secret;
@Column(name="REGISTRATION_SECRET")
private String registrationSecret;
@Column(name="REGISTRATION_TOKEN")
private String registrationToken;
@Column(name="CLIENT_AUTHENTICATOR_TYPE")
private String clientAuthenticatorType;
@Column(name="NOT_BEFORE")
@ -203,12 +203,12 @@ public class ClientEntity {
this.secret = secret;
}
public String getRegistrationSecret() {
return registrationSecret;
public String getRegistrationToken() {
return registrationToken;
}
public void setRegistrationSecret(String registrationSecret) {
this.registrationSecret = registrationSecret;
public void setRegistrationToken(String registrationToken) {
this.registrationToken = registrationToken;
}
public int getNotBefore() {

View file

@ -178,13 +178,13 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
}
@Override
public String getRegistrationSecret() {
return getMongoEntity().getRegistrationSecret();
public String getRegistrationToken() {
return getMongoEntity().getRegistrationToken();
}
@Override
public void setRegistrationSecret(String registrationSecretsecret) {
getMongoEntity().setRegistrationSecret(registrationSecretsecret);
public void setRegistrationToken(String registrationToken) {
getMongoEntity().setRegistrationToken(registrationToken);
updateMongoEntity();
}

View file

@ -0,0 +1,69 @@
package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessAdapter implements ClientInitialAccessModel {
private final KeycloakSession session;
private final InfinispanUserSessionProvider provider;
private final Cache<String, SessionEntity> cache;
private final RealmModel realm;
private final ClientInitialAccessEntity entity;
public ClientInitialAccessAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, ClientInitialAccessEntity entity) {
this.session = session;
this.provider = provider;
this.cache = cache;
this.realm = realm;
this.entity = entity;
}
@Override
public String getId() {
return entity.getId();
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public int getTimestamp() {
return entity.getTimestamp();
}
@Override
public int getExpiration() {
return entity.getExpiration();
}
@Override
public int getCount() {
return entity.getCount();
}
@Override
public int getRemainingCount() {
return entity.getRemainingCount();
}
@Override
public void decreaseRemainingCount() {
entity.setRemainingCount(entity.getRemainingCount() - 1);
update();
}
void update() {
provider.getTx().replace(cache, entity.getId(), entity);
}
}

View file

@ -3,28 +3,10 @@ package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
import org.infinispan.distexec.mapreduce.MapReduceTask;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserLoginFailureMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
import org.keycloak.models.sessions.infinispan.entities.*;
import org.keycloak.models.sessions.infinispan.mapreduce.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.common.util.Time;
@ -355,6 +337,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
persister.removeClientSession(clientSessionId, true);
}
// Remove expired client initial access
map = new MapReduceTask(sessionCache)
.mappedWith(ClientInitialAccessMapper.create(realm.getId()).time(Time.currentTime()).remainingCount(0).emitKey())
.reducedWith(new FirstResultReducer())
.execute();
for (String id : map.keySet()) {
tx.remove(sessionCache, id);
}
}
@Override
@ -538,11 +529,24 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return models;
}
List<ClientInitialAccessModel> wrapClientInitialAccess(RealmModel realm, Collection<ClientInitialAccessEntity> entities) {
List<ClientInitialAccessModel> models = new LinkedList<>();
for (ClientInitialAccessEntity e : entities) {
models.add(wrap(realm, e));
}
return models;
}
ClientSessionAdapter wrap(RealmModel realm, ClientSessionEntity entity, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline);
return entity != null ? new ClientSessionAdapter(session, this, cache, realm, entity, offline) : null;
}
ClientInitialAccessAdapter wrap(RealmModel realm, ClientInitialAccessEntity entity) {
Cache<String, SessionEntity> cache = getCache(false);
return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null;
}
UsernameLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
return entity != null ? new UsernameLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
@ -680,6 +684,50 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return wrap(clientSession.getRealm(), entity, offline);
}
@Override
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
String id = KeycloakModelUtils.generateId();
ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
entity.setId(id);
entity.setRealm(realm.getId());
entity.setTimestamp(Time.currentTime());
entity.setExpiration(expiration);
entity.setCount(count);
entity.setRemainingCount(count);
tx.put(sessionCache, id, entity);
return wrap(realm, entity);
}
@Override
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
Cache<String, SessionEntity> cache = getCache(false);
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) cache.get(id);
// If created in this transaction
if (entity == null) {
entity = (ClientInitialAccessEntity) tx.get(cache, id);
}
return wrap(realm, entity);
}
@Override
public void removeClientInitialAccessModel(RealmModel realm, String id) {
tx.remove(getCache(false), id);
}
@Override
public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
Map<String, ClientInitialAccessEntity> entities = new MapReduceTask(sessionCache)
.mappedWith(ClientInitialAccessMapper.create(realm.getId()))
.reducedWith(new FirstResultReducer())
.execute();
return wrapClientInitialAccess(realm, entities.values());
}
class InfinispanKeycloakTransaction implements KeycloakTransaction {
private boolean active;

View file

@ -0,0 +1,55 @@
package org.keycloak.models.sessions.infinispan.compat;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientInitialAccessEntity;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessAdapter implements ClientInitialAccessModel {
private final RealmModel realm;
private final ClientInitialAccessEntity entity;
public ClientInitialAccessAdapter(RealmModel realm, ClientInitialAccessEntity entity) {
this.realm = realm;
this.entity = entity;
}
@Override
public String getId() {
return entity.getId();
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public int getTimestamp() {
return entity.getTimestamp();
}
@Override
public int getExpiration() {
return entity.getExpires();
}
@Override
public int getCount() {
return entity.getCount();
}
@Override
public int getRemainingCount() {
return entity.getRemainingCount();
}
@Override
public void decreaseRemainingCount() {
entity.setRemainingCount(entity.getRemainingCount() - 1);
}
}

View file

@ -1,19 +1,8 @@
package org.keycloak.models.sessions.infinispan.compat;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureKey;
import org.keycloak.models.sessions.infinispan.compat.entities.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.common.util.Time;
@ -41,11 +30,12 @@ public class MemUserSessionProvider implements UserSessionProvider {
private final ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions;
private final ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions;
private ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess;
public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap<String, UserSessionEntity> userSessions, ConcurrentHashMap<String, String> userSessionsByBrokerSessionId,
ConcurrentHashMap<String, Set<String>> userSessionsByBrokerUserId, ConcurrentHashMap<String, ClientSessionEntity> clientSessions,
ConcurrentHashMap<UsernameLoginFailureKey, UsernameLoginFailureEntity> loginFailures,
ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions) {
ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions, ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess) {
this.session = session;
this.userSessions = userSessions;
this.clientSessions = clientSessions;
@ -54,6 +44,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
this.userSessionsByBrokerUserId = userSessionsByBrokerUserId;
this.offlineUserSessions = offlineUserSessions;
this.offlineClientSessions = offlineClientSessions;
this.clientInitialAccess = clientInitialAccess;
}
@Override
@ -341,6 +332,15 @@ public class MemUserSessionProvider implements UserSessionProvider {
persister.removeClientSession(s.getId(), true);
}
}
// Remove expired initial access
Iterator<ClientInitialAccessEntity> iaitr = clientInitialAccess.values().iterator();
while (iaitr.hasNext()) {
ClientInitialAccessEntity e = iaitr.next();
if (e.getRealmId().equals(realm.getId()) && (e.getExpires() < Time.currentTime())) {
iaitr.remove();
}
}
}
@Override
@ -574,6 +574,43 @@ public class MemUserSessionProvider implements UserSessionProvider {
return getUserSessions(realm, client, first, max, true);
}
@Override
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
String id = KeycloakModelUtils.generateId();
ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
entity.setId(id);
entity.setRealmId(realm.getId());
entity.setTimestamp(Time.currentTime());
entity.setExpiration(expiration);
entity.setCount(count);
entity.setRemainingCount(count);
clientInitialAccess.put(id, entity);
return new ClientInitialAccessAdapter(realm, entity);
}
@Override
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
ClientInitialAccessEntity entity = clientInitialAccess.get(id);
return entity != null ? new ClientInitialAccessAdapter(realm, entity) : null;
}
@Override
public void removeClientInitialAccessModel(RealmModel realm, String id) {
clientInitialAccess.remove(id);
}
@Override
public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
List<ClientInitialAccessModel> models = new LinkedList<>();
for (ClientInitialAccessEntity e : clientInitialAccess.values()) {
models.add(new ClientInitialAccessAdapter(realm, e));
}
return models;
}
@Override
public void close() {
}

View file

@ -1,14 +1,12 @@
package org.keycloak.models.sessions.infinispan.compat;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureKey;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientInitialAccessEntity;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -29,9 +27,11 @@ public class MemUserSessionProviderFactory {
private ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions = new ConcurrentHashMap<String, UserSessionEntity>();
private ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions = new ConcurrentHashMap<String, ClientSessionEntity>();
private ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess = new ConcurrentHashMap<>();
public UserSessionProvider create(KeycloakSession session) {
return new MemUserSessionProvider(session, userSessions, userSessionsByBrokerSessionId, userSessionsByBrokerUserId, clientSessions, loginFailures,
offlineUserSessions, offlineClientSessions);
offlineUserSessions, offlineClientSessions, clientInitialAccess);
}
public void close() {

View file

@ -0,0 +1,68 @@
package org.keycloak.models.sessions.infinispan.compat.entities;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessEntity {
private String id;
private String realmId;
private int timestamp;
private int expires;
private int count;
private int remainingCount;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public int getExpires() {
return expires;
}
public void setExpiration(int expires) {
this.expires = expires;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(int remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -0,0 +1,48 @@
package org.keycloak.models.sessions.infinispan.entities;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessEntity extends SessionEntity {
private int timestamp;
private int expires;
private int count;
private int remainingCount;
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public int getExpiration() {
return expires;
}
public void setExpiration(int expires) {
this.expires = expires;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(int remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -0,0 +1,80 @@
package org.keycloak.models.sessions.infinispan.mapreduce;
import org.infinispan.distexec.mapreduce.Collector;
import org.infinispan.distexec.mapreduce.Mapper;
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.io.Serializable;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
public ClientInitialAccessMapper(String realm) {
this.realm = realm;
}
private enum EmitValue {
KEY, ENTITY
}
private String realm;
private EmitValue emit = EmitValue.ENTITY;
private Integer time;
private Integer remainingCount;
public static ClientInitialAccessMapper create(String realm) {
return new ClientInitialAccessMapper(realm);
}
public ClientInitialAccessMapper emitKey() {
emit = EmitValue.KEY;
return this;
}
public ClientInitialAccessMapper time(int time) {
this.time = time;
return this;
}
public ClientInitialAccessMapper remainingCount(int remainingCount) {
this.remainingCount = remainingCount;
return this;
}
@Override
public void map(String key, SessionEntity e, Collector collector) {
if (!realm.equals(e.getRealm())) {
return;
}
if (!(e instanceof ClientInitialAccessEntity)) {
return;
}
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e;
if (time != null && entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < time) {
return;
}
if (remainingCount != null && entity.getRemainingCount() == remainingCount) {
return;
}
switch (emit) {
case KEY:
collector.emit(key, key);
break;
case ENTITY:
collector.emit(key, entity);
break;
}
}
}

13
pom.xml
View file

@ -76,6 +76,7 @@
<log4j.version>1.2.17</log4j.version>
<greenmail.version>1.3.1b</greenmail.version>
<xmlsec.version>1.5.1</xmlsec.version>
<aesh.version>0.66</aesh.version>
<enforcer.plugin.version>1.4</enforcer.plugin.version>
<jboss.as.plugin.version>7.5.Final</jboss.as.plugin.version>
@ -135,7 +136,7 @@
<modules>
<module>common</module>
<module>core</module>
<module>client-api</module>
<module>client-registration</module>
<module>connections</module>
<module>dependencies</module>
<module>events</module>
@ -580,6 +581,11 @@
<artifactId>pax-web-runtime</artifactId>
<version>${pax.web.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
<version>${aesh.version}</version>
</dependency>
<!-- keycloak -->
<dependency>
@ -622,6 +628,11 @@
<artifactId>keycloak-connections-http-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-registration-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-mongo-update</artifactId>

View file

@ -2,26 +2,10 @@ package org.keycloak.protocol.saml.clientregistration;
import org.jboss.logging.Logger;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.clientregistration.ClientRegAuth;
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -31,7 +15,7 @@ public class EntityDescriptorClientRegistrationProvider implements ClientRegistr
private KeycloakSession session;
private EventBuilder event;
private ClientRegAuth auth;
private ClientRegistrationAuth auth;
public EntityDescriptorClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@ -67,7 +51,7 @@ public class EntityDescriptorClientRegistrationProvider implements ClientRegistr
}
@Override
public void setAuth(ClientRegAuth auth) {
public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}

View file

@ -1,6 +1,5 @@
package org.keycloak.services.clientregistration;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@ -23,7 +22,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
private KeycloakSession session;
private EventBuilder event;
private ClientRegAuth auth;
private ClientRegistrationAuth auth;
public AdapterInstallationClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@ -51,7 +50,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
}
@Override
public void setAuth(ClientRegAuth auth) {
public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}

View file

@ -1,134 +0,0 @@
package org.keycloak.services.clientregistration;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import javax.ws.rs.core.HttpHeaders;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegAuth {
private KeycloakSession session;
private EventBuilder event;
private String token;
private AccessToken.Access bearerRealmAccess;
private boolean authenticated = false;
private boolean registrationAccessToken = false;
public ClientRegAuth(KeycloakSession session, EventBuilder event) {
this.session = session;
this.event = event;
init();
}
private void init() {
RealmModel realm = session.getContext().getRealm();
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
return;
}
String[] split = authorizationHeader.split(" ");
if (!split[0].equalsIgnoreCase("bearer")) {
return;
}
if (split[1].indexOf('.') == -1) {
token = split[1];
authenticated = true;
registrationAccessToken = true;
} else {
AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateBearerToken(session, realm);
bearerRealmAccess = authResult.getToken().getResourceAccess(Constants.REALM_MANAGEMENT_CLIENT_ID);
authenticated = true;
}
}
public boolean isAuthenticated() {
return authenticated;
}
public boolean isRegistrationAccessToken() {
return registrationAccessToken;
}
public void requireCreate() {
if (!authenticated) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (bearerRealmAccess != null) {
if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.CREATE_CLIENT)) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public void requireView(ClientModel client) {
if (!authenticated) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (bearerRealmAccess != null) {
if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.VIEW_CLIENTS)) {
return;
}
} else if (token != null) {
if (client.getRegistrationSecret() != null && client.getRegistrationSecret().equals(token)) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public void requireUpdate(ClientModel client) {
if (!authenticated) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (bearerRealmAccess != null) {
if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.VIEW_CLIENTS)) {
return;
}
} else if (token != null) {
if (client.getRegistrationSecret() != null && client.getRegistrationSecret().equals(token)) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
}

View file

@ -0,0 +1,182 @@
package org.keycloak.services.clientregistration;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.common.util.Time;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ForbiddenException;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriInfo;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationAuth {
private KeycloakSession session;
private EventBuilder event;
private JsonWebToken jwt;
private ClientInitialAccessModel initialAccessModel;
public ClientRegistrationAuth(KeycloakSession session, EventBuilder event) {
this.session = session;
this.event = event;
init();
}
private void init() {
RealmModel realm = session.getContext().getRealm();
UriInfo uri = session.getContext().getUri();
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
return;
}
String[] split = authorizationHeader.split(" ");
if (!split[0].equalsIgnoreCase("bearer")) {
return;
}
jwt = ClientRegistrationTokenUtils.parseToken(realm, uri, split[1]);
if (isInitialAccessToken()) {
initialAccessModel = session.sessions().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId());
if (initialAccessModel == null) {
throw new ForbiddenException();
}
}
}
public boolean isAuthenticated() {
return jwt != null;
}
public boolean isBearerToken() {
return TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
}
public boolean isInitialAccessToken() {
return ClientRegistrationTokenUtils.TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType());
}
public boolean isRegistrationAccessToken() {
return ClientRegistrationTokenUtils.TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType());
}
public void requireCreate() {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) {
return;
}
} else if (isInitialAccessToken()) {
if (initialAccessModel.getRemainingCount() > 0) {
if (initialAccessModel.getExpiration() == 0 || (initialAccessModel.getTimestamp() + initialAccessModel.getExpiration()) > Time.currentTime()) {
return;
}
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public void requireView(ClientModel client) {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.VIEW_CLIENTS)) {
return;
}
} else if (isRegistrationAccessToken()) {
if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public void requireUpdate(ClientModel client) {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS)) {
return;
}
} else if (isRegistrationAccessToken()) {
if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public ClientInitialAccessModel getInitialAccessModel() {
return initialAccessModel;
}
private boolean hasRole(String... role) {
try {
Map<String, Object> otherClaims = jwt.getOtherClaims();
if (otherClaims != null) {
Map<String, Map<String, List<String>>> resourceAccess = (Map<String, Map<String, List<String>>>) jwt.getOtherClaims().get("resource_access");
if (resourceAccess == null) {
return false;
}
Map<String, List<String>> realmManagement = resourceAccess.get(Constants.REALM_MANAGEMENT_CLIENT_ID);
if (realmManagement == null) {
return false;
}
List<String> resources = realmManagement.get("roles");
if (resources == null) {
return false;
}
for (String r : role) {
if (resources.contains(r)) {
return true;
}
}
}
return false;
} catch (Throwable t) {
return false;
}
}
}

View file

@ -1,7 +1,6 @@
package org.keycloak.services.clientregistration;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
/**
@ -9,7 +8,7 @@ import org.keycloak.provider.Provider;
*/
public interface ClientRegistrationProvider extends Provider {
void setAuth(ClientRegAuth auth);
void setAuth(ClientRegistrationAuth auth);
void setEvent(EventBuilder event);

View file

@ -35,7 +35,7 @@ public class ClientRegistrationService {
}
provider.setEvent(event);
provider.setAuth(new ClientRegAuth(session, event));
provider.setAuth(new ClientRegistrationAuth(session, event));
return provider;
}

View file

@ -0,0 +1,99 @@
package org.keycloak.services.clientregistration;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.Urls;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationTokenUtils {
public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken";
public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken";
public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client) {
return updateRegistrationAccessToken(session.getContext().getRealm(), session.getContext().getUri(), client);
}
public static String updateRegistrationAccessToken(RealmModel realm, UriInfo uri, ClientModel client) {
String id = KeycloakModelUtils.generateId();
client.setRegistrationToken(id);
String token = createToken(realm, uri, id, TYPE_REGISTRATION_ACCESS_TOKEN, 0);
return token;
}
public static String createInitialAccessToken(RealmModel realm, UriInfo uri, ClientInitialAccessModel model) {
return createToken(realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getTimestamp() + model.getExpiration());
}
public static JsonWebToken parseToken(RealmModel realm, UriInfo uri, String token) {
JWSInput input;
try {
input = new JWSInput(token);
} catch (Exception e) {
throw new ForbiddenException(e);
}
if (!RSAProvider.verify(input, realm.getPublicKey())) {
throw new ForbiddenException("Invalid signature");
}
JsonWebToken jwt;
try {
jwt = input.readJsonContent(JsonWebToken.class);
} catch (IOException e) {
throw new ForbiddenException(e);
}
if (!getIssuer(realm, uri).equals(jwt.getIssuer())) {
throw new ForbiddenException("Issuer doesn't match");
}
if (!jwt.isActive()) {
throw new ForbiddenException("Expired token");
}
if (!(TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType()) ||
TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType()) ||
TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType()))) {
throw new ForbiddenException("Invalid token type");
}
return jwt;
}
private static String createToken(RealmModel realm, UriInfo uri, String id, String type, int expiration) {
JsonWebToken jwt = new JsonWebToken();
String issuer = getIssuer(realm, uri);
jwt.type(type);
jwt.id(id);
jwt.issuedAt(Time.currentTime());
jwt.expiration(expiration);
jwt.issuer(issuer);
jwt.audience(issuer);
String token = new JWSBuilder().jsonContent(jwt).rsa256(realm.getPrivateKey());
return token;
}
private static String getIssuer(RealmModel realm, UriInfo uri) {
return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
}
}

View file

@ -2,10 +2,10 @@ package org.keycloak.services.clientregistration;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientRepresentation;
@ -23,7 +23,7 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
private KeycloakSession session;
private EventBuilder event;
private ClientRegAuth auth;
private ClientRegistrationAuth auth;
public DefaultClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@ -39,11 +39,19 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
try {
ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
KeycloakModelUtils.generateRegistrationAccessToken(clientModel);
client = ModelToRepresentation.toRepresentation(clientModel);
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel);
client.setRegistrationAccessToken(registrationAccessToken);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
if (auth.isInitialAccessToken()) {
ClientInitialAccessModel initialAccessModel = auth.getInitialAccessModel();
initialAccessModel.decreaseRemainingCount();
}
event.client(client.getClientId()).success();
return Response.created(uri).entity(client).build();
} catch (ModelDuplicateException e) {
@ -60,12 +68,15 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
auth.requireView(client);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
if (auth.isRegistrationAccessToken()) {
KeycloakModelUtils.generateRegistrationAccessToken(client);
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
rep.setRegistrationAccessToken(registrationAccessToken);
}
event.client(client.getClientId()).success();
return Response.ok(ModelToRepresentation.toRepresentation(client)).build();
return Response.ok(rep).build();
}
@PUT
@ -78,13 +89,13 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
auth.requireUpdate(client);
RepresentationToModel.updateClient(rep, client);
rep = ModelToRepresentation.toRepresentation(client);
if (auth.isRegistrationAccessToken()) {
KeycloakModelUtils.generateRegistrationAccessToken(client);
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
rep.setRegistrationAccessToken(registrationAccessToken);
}
rep = ModelToRepresentation.toRepresentation(client);
event.client(client.getClientId()).success();
return Response.ok(rep).build();
}
@ -106,7 +117,7 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
}
@Override
public void setAuth(ClientRegAuth auth) {
public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}

View file

@ -1,28 +1,11 @@
package org.keycloak.services.clientregistration.oidc;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCClientDescriptionConverter;
import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.clientregistration.ClientRegAuth;
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -32,7 +15,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide
private KeycloakSession session;
private EventBuilder event;
private ClientRegAuth auth;
private ClientRegistrationAuth auth;
public OIDCClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@ -55,7 +38,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide
//
// String registrationAccessToken = TokenGenerator.createRegistrationAccessToken();
//
// clientModel.setRegistrationSecret(registrationAccessToken);
// clientModel.setRegistrationToken(registrationAccessToken);
//
// URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
//
@ -87,7 +70,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide
}
@Override
public void setAuth(ClientRegAuth auth) {
public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}

View file

@ -40,12 +40,7 @@ import javax.ws.rs.ext.Providers;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.*;
import javax.ws.rs.QueryParam;
/**
@ -318,10 +313,10 @@ public class AdminConsole {
}
try {
Properties msgs = AdminMessagesLoader.getMessages(getTheme(), lang);
Properties msgs = getTheme().getMessages("admin-messages", Locale.forLanguageTag(lang));
if (msgs.isEmpty()) {
logger.warn("Message bundle not found for language code '" + lang + "'");
msgs = AdminMessagesLoader.getMessages(getTheme(), "en"); // fall back to en
msgs = getTheme().getMessages("admin-messages", Locale.ENGLISH);
}
if (msgs.isEmpty()) logger.fatal("Message bundle not found for language code 'en'");

View file

@ -1,107 +0,0 @@
/*
* Copyright 2015 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.services.resources.admin;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import org.jboss.logging.Logger;
import org.keycloak.freemarker.Theme;
/**
* Simple loader and cache for message bundles consumed by angular-translate.
*
* Note that these bundles are converted to JSON before being shipped to the UI.
* Also, the content should be formatted such that it can be interpolated by
* angular-translate. This is somewhat different from an ordinary Java bundle.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class AdminMessagesLoader {
protected static final Logger logger = Logger.getLogger(AdminConsole.class);
// theme locale bundle
protected static final Map<String, Map<String, Properties>> allMessages = new HashMap<String, Map<String, Properties>>();
static Properties getMessages(Theme theme, String strLocale) throws IOException {
String themeName = theme.getName();
Map bundlesForTheme = allMessages.get(themeName);
if (bundlesForTheme == null) {
bundlesForTheme = new HashMap<String, Properties>();
allMessages.put(themeName, bundlesForTheme);
}
return findMessagesForTheme(theme, strLocale, bundlesForTheme);
}
private static Properties findMessagesForTheme(Theme theme,
String strLocale,
Map<String, Properties> bundlesForTheme) throws IOException {
Properties messages = bundlesForTheme.get(strLocale);
if (messages != null) return messages; // use cached bundle
// load bundle from theme
Locale locale = Locale.forLanguageTag(strLocale);
messages = theme.getMessages("admin-messages", locale);
String themeName = theme.getName();
if (messages == null) throw new NullPointerException(themeName + ": Unable to find admin-messages bundle for locale=" + strLocale);
if (!bundlesForTheme.isEmpty()) {
// use first bundle as the standard
String standardLocale = bundlesForTheme.keySet().iterator().next();
Properties standardBundle = bundlesForTheme.get(standardLocale);
validateMessages(themeName, standardBundle, standardLocale, messages, strLocale);
}
bundlesForTheme.put(strLocale, messages);
return messages;
}
private static void validateMessages(String themeName, Properties standardBundle, String standardLocale, Properties messages, String strLocale) {
if (standardBundle.keySet().containsAll(messages.keySet()) &&
(messages.keySet().containsAll(standardBundle.keySet()))) {
return; // it all checks out
}
// otherwise, find the offending keys
int warnCount = 0;
for (Object key : standardBundle.keySet()) {
if (!messages.containsKey(key)) {
logger.error(themeName + " theme: Key '" + key + "' not found in admin-messages bundle for locale=" + strLocale +
". However, this key exists in previously loaded bundle for locale=" + standardLocale);
warnCount++;
}
if (warnCount > 4) return; // There could be lots of these. Don't fill up the log.
}
for (Object key : messages.keySet()) {
if (!standardBundle.containsKey(key)) {
logger.error(themeName + " theme: Key '" + key + "' was found in admin-messages bundle for locale=" + strLocale +
". However, this key does not exist in previously loaded bundle for locale=" + standardLocale);
warnCount++;
}
if (warnCount > 4) return; // There could be lots of these. Don't fill up the log.
}
}
}

View file

@ -0,0 +1,102 @@
package org.keycloak.services.resources.admin;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessResource {
private final RealmAuth auth;
private final RealmModel realm;
private final AdminEventBuilder adminEvent;
@Context
protected KeycloakSession session;
@Context
protected UriInfo uriInfo;
public ClientInitialAccessResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
this.auth = auth;
this.realm = realm;
this.adminEvent = adminEvent;
auth.init(RealmAuth.Resource.CLIENT);
}
/**
* Create a new initial access token.
*
* @param config
* @return
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation config, @Context final HttpServletResponse response) {
auth.requireManage();
int expiration = config.getExpiration() != null ? config.getExpiration() : 0;
int count = config.getCount() != null ? config.getCount() : 1;
ClientInitialAccessModel clientInitialAccessModel = session.sessions().createClientInitialAccessModel(realm, expiration, count);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientInitialAccessModel.getId()).representation(config).success();
if (session.getTransaction().isActive()) {
session.getTransaction().commit();
}
ClientInitialAccessPresentation rep = wrap(clientInitialAccessModel);
String token = ClientRegistrationTokenUtils.createInitialAccessToken(realm, uriInfo, clientInitialAccessModel);
rep.setToken(token);
response.setStatus(Response.Status.CREATED.getStatusCode());
response.setHeader(HttpHeaders.LOCATION, uriInfo.getAbsolutePathBuilder().path(clientInitialAccessModel.getId()).build().toString());
return rep;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ClientInitialAccessPresentation> list() {
List<ClientInitialAccessModel> models = session.sessions().listClientInitialAccess(realm);
List<ClientInitialAccessPresentation> reps = new LinkedList<>();
for (ClientInitialAccessModel m : models) {
ClientInitialAccessPresentation r = wrap(m);
reps.add(r);
}
return reps;
}
@DELETE
@Path("{id}")
public void delete(final @PathParam("id") String id) {
session.sessions().removeClientInitialAccessModel(realm, id);
}
private ClientInitialAccessPresentation wrap(ClientInitialAccessModel model) {
ClientInitialAccessPresentation rep = new ClientInitialAccessPresentation();
rep.setId(model.getId());
rep.setTimestamp(model.getTimestamp());
rep.setExpiration(model.getExpiration());
rep.setCount(model.getCount());
rep.setRemainingCount(model.getRemainingCount());
return rep;
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
@ -228,8 +229,11 @@ public class ClientResource {
public ClientRepresentation regenerateRegistrationAccessToken() {
auth.requireManage();
KeycloakModelUtils.generateRegistrationAccessToken(client);
String token = ClientRegistrationTokenUtils.updateRegistrationAccessToken(realm, uriInfo, client);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
rep.setRegistrationAccessToken(token);
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).representation(rep).success();
return rep;
}

View file

@ -143,6 +143,18 @@ public class RealmAdminResource {
return clientsResource;
}
/**
* Base path for managing client initial access tokens
*
* @return
*/
@Path("clients-initial-access")
public ClientInitialAccessResource getClientInitialAccess() {
ClientInitialAccessResource resource = new ClientInitialAccessResource(realm, auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
/**
* base path for managing realm-level roles of this realm
*

View file

@ -2,6 +2,7 @@ package org.keycloak.testsuite.client;
import org.junit.After;
import org.junit.Before;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistration;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.models.AdminRoles;
@ -13,7 +14,6 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@ -76,13 +76,11 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
testRealms.add(rep);
}
public ClientRepresentation createClient(ClientRepresentation client) {
Response response = adminClient.realm(REALM_NAME).clients().create(client);
String id = response.getLocation().toString();
id = id.substring(id.lastIndexOf('/') + 1);
client.setId(id);
response.close();
return client;
public ClientRepresentation createClient(ClientRepresentation client) throws ClientRegistrationException {
authManageClients();
ClientRepresentation response = reg.create(client);
reg.auth(null);
return response;
}
public ClientRepresentation getClient(String clientId) {
@ -93,4 +91,20 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
}
}
void authCreateClients() {
reg.auth(Auth.token(getToken("create-clients", "password")));
}
void authManageClients() {
reg.auth(Auth.token(getToken("manage-clients", "password")));
}
void authNoAccess() {
reg.auth(Auth.token(getToken("no-access", "password")));
}
private String getToken(String username, String password) {
return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
}
}

View file

@ -9,8 +9,6 @@ import org.keycloak.common.enums.SslRequired;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.ClientRepresentation;
import javax.ws.rs.core.Response;
import static org.junit.Assert.*;
/**
@ -37,6 +35,7 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes
client.setRegistrationAccessToken("RegistrationAccessTokenTestRegistrationAccessToken");
client.setRootUrl("http://root");
client = createClient(client);
client.setSecret("RegistrationAccessTokenTestClientSecret");
client2 = new ClientRepresentation();
client2.setEnabled(true);

View file

@ -196,20 +196,4 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
}
}
private void authCreateClients() {
reg.auth(Auth.token(getToken("create-clients", "password")));
}
private void authManageClients() {
reg.auth(Auth.token(getToken("manage-clients", "password")));
}
private void authNoAccess() {
reg.auth(Auth.token(getToken("no-access", "password")));
}
private String getToken(String username, String password) {
return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
}
}

View file

@ -0,0 +1,101 @@
package org.keycloak.testsuite.client;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientInitialAccessResource;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class InitialAccessTokenTest extends AbstractClientRegistrationTest {
private ClientInitialAccessResource resource;
@Before
public void before() throws Exception {
super.before();
resource = adminClient.realm(REALM_NAME).clientInitialAccess();
}
@Test
public void create() throws ClientRegistrationException {
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
reg.auth(Auth.token(response));
ClientRepresentation rep = new ClientRepresentation();
ClientRepresentation created = reg.create(rep);
Assert.assertNotNull(created);
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@Test
public void createMultiple() throws ClientRegistrationException {
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation(0, 2));
reg.auth(Auth.token(response));
ClientRepresentation rep = new ClientRepresentation();
ClientRepresentation created = reg.create(rep);
Assert.assertNotNull(created);
created = reg.create(rep);
Assert.assertNotNull(created);
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@Test
public void createExpired() throws ClientRegistrationException, InterruptedException {
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation(1, 1));
reg.auth(Auth.token(response));
ClientRepresentation rep = new ClientRepresentation();
Thread.sleep(2);
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@Test
public void createDeleted() throws ClientRegistrationException, InterruptedException {
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
reg.auth(Auth.token(response));
resource.delete(response.getId());
ClientRepresentation rep = new ClientRepresentation();
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
}

View file

@ -7,8 +7,6 @@ import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.representations.idm.ClientRepresentation;
import javax.ws.rs.core.Response;
import static org.junit.Assert.*;
/**
@ -22,13 +20,13 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
public void before() throws Exception {
super.before();
client = new ClientRepresentation();
client.setEnabled(true);
client.setClientId("RegistrationAccessTokenTest");
client.setSecret("RegistrationAccessTokenTestClientSecret");
client.setRegistrationAccessToken("RegistrationAccessTokenTestRegistrationAccessToken");
client.setRootUrl("http://root");
client = createClient(client);
ClientRepresentation c = new ClientRepresentation();
c.setEnabled(true);
c.setClientId("RegistrationAccessTokenTest");
c.setSecret("RegistrationAccessTokenTestClientSecret");
c.setRootUrl("http://root");
client = createClient(c);
reg.auth(Auth.token(client.getRegistrationAccessToken()));
}
@ -36,7 +34,7 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
private ClientRepresentation assertRead(String id, String registrationAccess, boolean expectSuccess) throws ClientRegistrationException {
if (expectSuccess) {
reg.auth(Auth.token(registrationAccess));
ClientRepresentation rep = reg.get(client.getClientId());
ClientRepresentation rep = reg.get(id);
assertNotNull(rep);
return rep;
} else {
@ -76,6 +74,7 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
@Test
public void updateClientWithRegistrationToken() throws ClientRegistrationException {
client.setRootUrl("http://newroot");
ClientRepresentation rep = reg.update(client);
assertEquals("http://newroot", getClient(client.getId()).getRootUrl());

View file

@ -230,7 +230,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-api</artifactId>
<artifactId>keycloak-client-registration-api</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>

View file

@ -0,0 +1,58 @@
package org.keycloak.testsuite.admin;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientInitialAccessResource;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessTest extends AbstractClientTest {
@Test
public void create() {
ClientInitialAccessResource resource = keycloak.realm(REALM_NAME).clientInitialAccess();
ClientInitialAccessPresentation access = resource.create(new ClientInitialAccessCreatePresentation(1000, 2));
Assert.assertEquals(new Integer(2), access.getCount());
Assert.assertEquals(new Integer(2), access.getRemainingCount());
Assert.assertEquals(new Integer(1000), access.getExpiration());
Assert.assertNotNull(access.getTimestamp());
Assert.assertNotNull(access.getToken());
ClientInitialAccessPresentation access2 = resource.create(new ClientInitialAccessCreatePresentation());
List<ClientInitialAccessPresentation> list = resource.list();
Assert.assertEquals(2, list.size());
for (ClientInitialAccessPresentation r : list) {
if (r.getId().equals(access.getId())) {
Assert.assertEquals(new Integer(2), r.getCount());
Assert.assertEquals(new Integer(2), r.getRemainingCount());
Assert.assertEquals(new Integer(1000), r.getExpiration());
Assert.assertNotNull(r.getTimestamp());
Assert.assertNull(r.getToken());
} else if(r.getId().equals(access2.getId())) {
Assert.assertEquals(new Integer(1), r.getCount());
Assert.assertEquals(new Integer(1), r.getRemainingCount());
Assert.assertEquals(new Integer(0), r.getExpiration());
Assert.assertNotNull(r.getTimestamp());
Assert.assertNull(r.getToken());
} else {
Assert.fail("Unexpected id");
}
}
resource.delete(access.getId());
resource.delete(access2.getId());
Assert.assertTrue(resource.list().isEmpty());
}
}