Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
6989589e72
66 changed files with 1622 additions and 450 deletions
|
@ -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>
|
|
@ -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());
|
||||
}
|
|
@ -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) {
|
|
@ -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
34
client-registration/cli/pom.xml
Executable 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>
|
|
@ -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
19
client-registration/pom.xml
Executable 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>
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 : {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
|
@ -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>
|
|
@ -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'}} {{(ia.timestamp * 1000)|date:'mediumTime'}}</td>
|
||||
<td><span data-ng-show="ia.expiration > 0">{{((ia.timestamp + ia.expiration) * 1000)|date:'shortDate'}} {{((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>
|
|
@ -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>
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -56,5 +56,8 @@ public interface RealmResource {
|
|||
@Path("client-session-stats")
|
||||
@GET
|
||||
List<Map<String, String>> getClientSessionStats();
|
||||
|
||||
|
||||
@Path("clients-initial-access")
|
||||
ClientInitialAccessResource clientInitialAccess();
|
||||
|
||||
}
|
||||
|
|
22
model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
Executable file
22
model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
Executable 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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
13
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ public class ClientRegistrationService {
|
|||
}
|
||||
|
||||
provider.setEvent(event);
|
||||
provider.setAuth(new ClientRegAuth(session, event));
|
||||
provider.setAuth(new ClientRegistrationAuth(session, event));
|
||||
return provider;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'");
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue