Merge pull request #1638 from mposolda/master

KEYCLOAK-904 Consents support. Added scopeParamRequired flag to RoleM…
This commit is contained in:
Marek Posolda 2015-09-23 15:30:36 +02:00
commit 22f7efd977
57 changed files with 514 additions and 177 deletions

View file

@ -2,8 +2,8 @@
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="mposolda@redhat.com" id="1.6.0">
<addColumn tableName="CLIENT">
<column name="OFFLINE_TOKENS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
<addColumn tableName="KEYCLOAK_ROLE">
<column name="SCOPE_PARAM_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>

View file

@ -24,7 +24,6 @@ public class ClientRepresentation {
protected Boolean bearerOnly;
protected Boolean consentRequired;
protected Boolean serviceAccountsEnabled;
protected Boolean offlineTokensEnabled;
protected Boolean directGrantsOnly;
protected Boolean publicClient;
protected Boolean frontchannelLogout;
@ -163,14 +162,6 @@ public class ClientRepresentation {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
public Boolean isOfflineTokensEnabled() {
return offlineTokensEnabled;
}
public void setOfflineTokensEnabled(Boolean offlineTokensEnabled) {
this.offlineTokensEnabled = offlineTokensEnabled;
}
public Boolean isDirectGrantsOnly() {
return directGrantsOnly;
}

View file

@ -12,6 +12,7 @@ public class RoleRepresentation {
protected String id;
protected String name;
protected String description;
protected Boolean scopeParamRequired;
protected boolean composite;
protected Composites composites;
@ -46,9 +47,10 @@ public class RoleRepresentation {
public RoleRepresentation() {
}
public RoleRepresentation(String name, String description) {
public RoleRepresentation(String name, String description, boolean scopeParamRequired) {
this.name = name;
this.description = description;
this.scopeParamRequired = scopeParamRequired;
}
public String getId() {
@ -75,6 +77,14 @@ public class RoleRepresentation {
this.description = description;
}
public Boolean isScopeParamRequired() {
return scopeParamRequired;
}
public void setScopeParamRequired(Boolean scopeParamRequired) {
this.scopeParamRequired = scopeParamRequired;
}
public Composites getComposites() {
return composites;
}

View file

@ -5,7 +5,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.ClientModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.ProtocolMapperModel;
@ -13,7 +12,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.offline.OfflineUserSessionManager;
import org.keycloak.services.offline.OfflineTokenUtils;
import org.keycloak.util.MultivaluedHashMap;
/**
@ -25,7 +24,7 @@ public class ApplicationsBean {
public ApplicationsBean(RealmModel realm, UserModel user) {
Set<ClientModel> offlineClients = new OfflineUserSessionManager().findClientsWithOfflineToken(realm, user);
Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(realm, user);
List<ClientModel> realmClients = realm.getClients();
for (ClientModel client : realmClients) {
@ -34,7 +33,7 @@ public class ApplicationsBean {
continue;
}
Set<RoleModel> availableRoles = TokenManager.getAccess(null, client, user);
Set<RoleModel> availableRoles = TokenManager.getAccess(null, false, client, user);
// Don't show applications, which user doesn't have access into (any available roles)
if (availableRoles.isEmpty()) {
continue;
@ -60,7 +59,7 @@ public class ApplicationsBean {
List<String> additionalGrants = new ArrayList<>();
if (offlineClients.contains(client)) {
additionalGrants.add("${offlineAccess}");
additionalGrants.add("${offlineToken}");
}
ApplicationEntry appEntry = new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, realmRolesGranted, resourceRolesGranted, client,

View file

@ -52,6 +52,7 @@ role_manage-events=Manage events
role_view-profile=View profile
role_manage-account=Manage account
role_read-token=Read token
role_offline-access=Offline access
client_account=Account
client_security-admin-console=Security Admin Console
client_realm-management=Realm Management
@ -89,7 +90,7 @@ additionalGrants=Additional Grants
action=Action
inResource=in
fullAccess=Full Access
offlineAccess=Offline Access
offlineToken=Offline Token
revoke=Revoke Grant
configureAuthenticators=Configured Authenticators

View file

@ -79,13 +79,6 @@
<input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch />
</div>
</div>
<div class="form-group" data-ng-show="protocol == 'openid-connect' && !client.bearerOnly">
<label class="col-md-2 control-label" for="offlineTokensEnabled">Offline Tokens Enabled</label>
<kc-tooltip>Allows you to retrieve offline tokens for users. Offline token can be stored by client application and is valid even if user is not logged anymore.</kc-tooltip>
<div class="col-md-6">
<input ng-model="client.offlineTokensEnabled" name="offlineTokensEnabled" id="offlineTokensEnabled" onoffswitch />
</div>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlServerSignature">Include AuthnStatement</label>
<div class="col-sm-6">

View file

@ -32,6 +32,13 @@
required> -->
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="scopeParamRequired">Scope Param Required </label>
<kc-tooltip>This role will be granted just if scope parameter with role name is used during authorization/token request.</kc-tooltip>
<div class="col-md-6">
<input ng-model="role.scopeParamRequired" name="scopeParamRequired" id="scopeParamRequired" onoffswitch />
</div>
</div>
<div class="form-group clearfix block" data-ng-hide="create">
<label class="col-md-2 control-label" for="compositeSwitch" class="control-label">Composite Roles</label>
<div class="col-md-6">

View file

@ -28,6 +28,13 @@
<textarea class="form-control" rows="5" cols="50" id="description" name="description" data-ng-model="role.description"></textarea>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="scopeParamRequired">Scope Param Required </label>
<kc-tooltip>This role will be granted just if scope parameter with role name is used during authorization/token request.</kc-tooltip>
<div class="col-md-6">
<input ng-model="role.scopeParamRequired" name="scopeParamRequired" id="scopeParamRequired" onoffswitch />
</div>
</div>
<div class="form-group" data-ng-hide="create">
<label class="col-md-2 control-label" for="compositeSwitch" class="control-label">Composite Roles</label>
<div class="col-md-6">

View file

@ -104,6 +104,7 @@ role_manage-events=Manage events
role_view-profile=View profile
role_manage-account=Manage account
role_read-token=Read token
role_offline-access=Offline access
client_account=Account
client_security-admin-console=Security Admin Console
client_realm-management=Realm Management

View file

@ -11,7 +11,7 @@ public interface MigrationModel {
/**
* Must have the form of major.minor.micro as the version is parsed and numbers are compared
*/
public static final String LATEST_VERSION = "1.5.0";
public static final String LATEST_VERSION = "1.6.0";
String getStoredVersion();
void setStoredVersion(String version);

View file

@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.keycloak.migration.migrators.MigrateTo1_3_0;
import org.keycloak.migration.migrators.MigrateTo1_4_0;
import org.keycloak.migration.migrators.MigrateTo1_5_0;
import org.keycloak.migration.migrators.MigrateTo1_6_0;
import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1;
import org.keycloak.models.KeycloakSession;
@ -47,6 +48,12 @@ public class MigrationModelManager {
}
new MigrateTo1_5_0().migrate(session);
}
if (stored == null || stored.lessThan(MigrateTo1_6_0.VERSION)) {
if (stored != null) {
logger.debug("Migrating older model to 1.6.0 updates");
}
new MigrateTo1_6_0().migrate(session);
}
model.setStoredVersion(MigrationModel.LATEST_VERSION);
}

View file

@ -0,0 +1,25 @@
package org.keycloak.migration.migrators;
import java.util.List;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class MigrateTo1_6_0 {
public static final ModelVersion VERSION = new ModelVersion("1.6.0");
public void migrate(KeycloakSession session) {
List<RealmModel> realms = session.realms().getRealms();
for (RealmModel realm : realms) {
KeycloakModelUtils.setupOfflineTokens(realm);
}
}
}

View file

@ -5,6 +5,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.List;
@ -26,7 +27,9 @@ public class MigrationTo1_2_0_CR1 {
client.setFullScopeAllowed(false);
for (String role : Constants.BROKER_SERVICE_ROLES) {
client.addRole(role).setDescription("${role_"+ role.toLowerCase().replaceAll("_", "-") +"}");
RoleModel roleModel = client.addRole(role);
roleModel.setDescription("${role_" + role.toLowerCase().replaceAll("_", "-") + "}");
roleModel.setScopeParamRequired(false);
}
}
}

View file

@ -109,9 +109,6 @@ public interface ClientModel extends RoleContainerModel {
boolean isServiceAccountsEnabled();
void setServiceAccountsEnabled(boolean serviceAccountsEnabled);
boolean isOfflineTokensEnabled();
void setOfflineTokensEnabled(boolean offlineTokensEnabled);
Set<RoleModel> getScopeMappings();
void addScopeMapping(RoleModel role);
void deleteScopeMapping(RoleModel role);

View file

@ -1,5 +1,7 @@
package org.keycloak.models;
import org.keycloak.OAuth2Constants;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -16,4 +18,5 @@ public interface Constants {
String INSTALLED_APP_URL = "http://localhost";
String READ_TOKEN_ROLE = "read-token";
String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS;
}

View file

@ -26,6 +26,7 @@ public class ImpersonationConstants {
if (realmAdminApp.getRole(IMPERSONATION_ROLE) != null) return;
RoleModel impersonationRole = realmAdminApp.addRole(IMPERSONATION_ROLE);
impersonationRole.setDescription("${role_" + IMPERSONATION_ROLE + "}");
impersonationRole.setScopeParamRequired(false);
adminRole.addCompositeRole(impersonationRole);
}
@ -36,6 +37,7 @@ public class ImpersonationConstants {
if (realmAdminApp.getRole(IMPERSONATION_ROLE) != null) return;
RoleModel impersonationRole = realmAdminApp.addRole(IMPERSONATION_ROLE);
impersonationRole.setDescription("${role_" + IMPERSONATION_ROLE + "}");
impersonationRole.setScopeParamRequired(false);
RoleModel adminRole = realmAdminApp.getRole(AdminRoles.REALM_ADMIN);
adminRole.addCompositeRole(impersonationRole);
}

View file

@ -17,6 +17,10 @@ public interface RoleModel {
void setName(String name);
boolean isScopeParamRequired();
void setScopeParamRequired(boolean scopeParamRequired);
boolean isComposite();
void addCompositeRole(RoleModel role);

View file

@ -28,7 +28,6 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private boolean bearerOnly;
private boolean consentRequired;
private boolean serviceAccountsEnabled;
private boolean offlineTokensEnabled;
private boolean directGrantsOnly;
private int nodeReRegistrationTimeout;
@ -229,14 +228,6 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
public boolean isOfflineTokensEnabled() {
return offlineTokensEnabled;
}
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
this.offlineTokensEnabled = offlineTokensEnabled;
}
public boolean isDirectGrantsOnly() {
return directGrantsOnly;
}

View file

@ -9,6 +9,7 @@ public class RoleEntity extends AbstractIdentifiableEntity {
private String name;
private String description;
private boolean scopeParamRequired;
private List<String> compositeRoleIds;
@ -31,6 +32,14 @@ public class RoleEntity extends AbstractIdentifiableEntity {
this.description = description;
}
public boolean isScopeParamRequired() {
return scopeParamRequired;
}
public void setScopeParamRequired(boolean scopeParamRequired) {
this.scopeParamRequired = scopeParamRequired;
}
public List<String> getCompositeRoleIds() {
return compositeRoleIds;
}

View file

@ -4,6 +4,7 @@ import org.bouncycastle.openssl.PEMWriter;
import org.keycloak.constants.KerberosConstants;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
@ -360,4 +361,13 @@ public final class KeycloakModelUtils {
public static String toLowerCaseSafe(String str) {
return str==null ? null : str.toLowerCase();
}
public static void setupOfflineTokens(RealmModel realm) {
if (realm.getRole(Constants.OFFLINE_ACCESS_ROLE) == null) {
RoleModel role = realm.addRole(Constants.OFFLINE_ACCESS_ROLE);
role.setDescription("${role_offline-access}");
role.setScopeParamRequired(true);
realm.addDefaultRole(Constants.OFFLINE_ACCESS_ROLE);
}
}
}

View file

@ -89,6 +89,7 @@ public class ModelToRepresentation {
rep.setId(role.getId());
rep.setName(role.getName());
rep.setDescription(role.getDescription());
rep.setScopeParamRequired(role.isScopeParamRequired());
rep.setComposite(role.isComposite());
return rep;
}
@ -303,7 +304,6 @@ public class ModelToRepresentation {
rep.setBearerOnly(clientModel.isBearerOnly());
rep.setConsentRequired(clientModel.isConsentRequired());
rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled());
rep.setOfflineTokensEnabled(clientModel.isOfflineTokensEnabled());
rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly());
rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired());
rep.setBaseUrl(clientModel.getBaseUrl());

View file

@ -181,6 +181,8 @@ public class RepresentationToModel {
// Application role may already exists (for example if it is defaultRole)
RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName());
role.setDescription(roleRep.getDescription());
boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired();
role.setScopeParamRequired(scopeParamRequired);
}
}
}
@ -633,6 +635,8 @@ public class RepresentationToModel {
public static void createRole(RealmModel newRealm, RoleRepresentation roleRep) {
RoleModel role = roleRep.getId()!=null ? newRealm.addRole(roleRep.getId(), roleRep.getName()) : newRealm.addRole(roleRep.getName());
if (roleRep.getDescription() != null) role.setDescription(roleRep.getDescription());
boolean scopeParamRequired = roleRep.isScopeParamRequired() == null ? false : roleRep.isScopeParamRequired();
role.setScopeParamRequired(scopeParamRequired);
}
private static void addComposites(RoleModel role, RoleRepresentation roleRep, RealmModel realm) {
@ -692,7 +696,6 @@ public class RepresentationToModel {
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
if (resourceRep.isOfflineTokensEnabled() != null) client.setOfflineTokensEnabled(resourceRep.isOfflineTokensEnabled());
if (resourceRep.isDirectGrantsOnly() != null) client.setDirectGrantsOnly(resourceRep.isDirectGrantsOnly());
if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
@ -789,7 +792,6 @@ public class RepresentationToModel {
if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired());
if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled());
if (rep.isOfflineTokensEnabled() != null) resource.setOfflineTokensEnabled(rep.isOfflineTokensEnabled());
if (rep.isDirectGrantsOnly() != null) resource.setDirectGrantsOnly(rep.isDirectGrantsOnly());
if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());

View file

@ -461,16 +461,6 @@ public class ClientAdapter implements ClientModel {
entity.setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
public boolean isOfflineTokensEnabled() {
return entity.isOfflineTokensEnabled();
}
@Override
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
entity.setOfflineTokensEnabled(offlineTokensEnabled);
}
@Override
public boolean isDirectGrantsOnly() {
return entity.isDirectGrantsOnly();

View file

@ -88,6 +88,16 @@ public class RoleAdapter implements RoleModel {
role.setDescription(description);
}
@Override
public boolean isScopeParamRequired() {
return role.isScopeParamRequired();
}
@Override
public void setScopeParamRequired(boolean scopeParamRequired) {
role.setScopeParamRequired(scopeParamRequired);
}
@Override
public boolean isComposite() {
return role.getCompositeRoleIds() != null && role.getCompositeRoleIds().size() > 0;

View file

@ -430,18 +430,6 @@ public class ClientAdapter implements ClientModel {
updated.setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
public boolean isOfflineTokensEnabled() {
if (updated != null) return updated.isOfflineTokensEnabled();
return cached.isOfflineTokensEnabled();
}
@Override
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
getDelegateForUpdate();
updated.setOfflineTokensEnabled(offlineTokensEnabled);
}
@Override
public RoleModel getRole(String name) {
if (updated != null) return updated.getRole(name);

View file

@ -59,6 +59,18 @@ public class RoleAdapter implements RoleModel {
updated.setDescription(description);
}
@Override
public boolean isScopeParamRequired() {
if (updated != null) return updated.isScopeParamRequired();
return cached.isScopeParamRequired();
}
@Override
public void setScopeParamRequired(boolean scopeParamRequired) {
getDelegateForUpdate();
updated.setScopeParamRequired(scopeParamRequired);
}
@Override
public String getId() {
if (updated != null) return updated.getId();

View file

@ -47,7 +47,6 @@ public class CachedClient implements Serializable {
private boolean bearerOnly;
private boolean consentRequired;
private boolean serviceAccountsEnabled;
private boolean offlineTokensEnabled;
private Map<String, String> roles = new HashMap<String, String>();
private int nodeReRegistrationTimeout;
private Map<String, Integer> registeredNodes;
@ -82,7 +81,6 @@ public class CachedClient implements Serializable {
bearerOnly = model.isBearerOnly();
consentRequired = model.isConsentRequired();
serviceAccountsEnabled = model.isServiceAccountsEnabled();
offlineTokensEnabled = model.isOfflineTokensEnabled();
for (RoleModel role : model.getRoles()) {
roles.put(role.getName(), role.getId());
cache.addCachedRole(new CachedClientRole(id, role, realm));
@ -191,10 +189,6 @@ public class CachedClient implements Serializable {
return serviceAccountsEnabled;
}
public boolean isOfflineTokensEnabled() {
return offlineTokensEnabled;
}
public Map<String, String> getRoles() {
return roles;
}

View file

@ -17,6 +17,7 @@ public class CachedRole implements Serializable {
final protected String name;
final protected String realm;
final protected String description;
final protected Boolean scopeParamRequired;
final protected boolean composite;
final protected Set<String> composites = new HashSet<String>();
@ -25,6 +26,7 @@ public class CachedRole implements Serializable {
description = model.getDescription();
id = model.getId();
name = model.getName();
scopeParamRequired = model.isScopeParamRequired();
this.realm = realm.getId();
if (composite) {
for (RoleModel child : model.getComposites()) {
@ -50,6 +52,10 @@ public class CachedRole implements Serializable {
return description;
}
public Boolean isScopeParamRequired() {
return scopeParamRequired;
}
public boolean isComposite() {
return composite;
}

View file

@ -481,16 +481,6 @@ public class ClientAdapter implements ClientModel {
entity.setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
public boolean isOfflineTokensEnabled() {
return entity.isOfflineTokensEnabled();
}
@Override
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
entity.setOfflineTokensEnabled(offlineTokensEnabled);
}
@Override
public boolean isDirectGrantsOnly() {
return entity.isDirectGrantsOnly();

View file

@ -49,6 +49,16 @@ public class RoleAdapter implements RoleModel {
role.setDescription(description);
}
@Override
public boolean isScopeParamRequired() {
return role.isScopeParamRequired();
}
@Override
public void setScopeParamRequired(boolean scopeParamRequired) {
role.setScopeParamRequired(scopeParamRequired);
}
@Override
public String getId() {
return role.getId();

View file

@ -100,9 +100,6 @@ public class ClientEntity {
@Column(name="SERVICE_ACCOUNTS_ENABLED")
private boolean serviceAccountsEnabled;
@Column(name="OFFLINE_TOKENS_ENABLED")
private boolean offlineTokensEnabled;
@Column(name="NODE_REREG_TIMEOUT")
private int nodeReRegistrationTimeout;
@ -319,14 +316,6 @@ public class ClientEntity {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
public boolean isOfflineTokensEnabled() {
return offlineTokensEnabled;
}
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
this.offlineTokensEnabled = offlineTokensEnabled;
}
public boolean isDirectGrantsOnly() {
return directGrantsOnly;
}

View file

@ -37,6 +37,8 @@ public class RoleEntity {
private String name;
@Column(name = "DESCRIPTION")
private String description;
@Column(name = "SCOPE_PARAM_REQUIRED")
private boolean scopeParamRequired;
// hax! couldn't get constraint to work properly
@Column(name = "REALM_ID")
@ -93,6 +95,14 @@ public class RoleEntity {
this.description = description;
}
public boolean isScopeParamRequired() {
return scopeParamRequired;
}
public void setScopeParamRequired(boolean scopeParamRequired) {
this.scopeParamRequired = scopeParamRequired;
}
public Collection<RoleEntity> getCompositeRoles() {
return compositeRoles;
}

View file

@ -483,17 +483,6 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
updateMongoEntity();
}
@Override
public boolean isOfflineTokensEnabled() {
return getMongoEntity().isOfflineTokensEnabled();
}
@Override
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
getMongoEntity().setOfflineTokensEnabled(offlineTokensEnabled);
updateMongoEntity();
}
@Override
public boolean isDirectGrantsOnly() {
return getMongoEntity().isDirectGrantsOnly();

View file

@ -68,6 +68,17 @@ public class RoleAdapter extends AbstractMongoAdapter<MongoRoleEntity> implement
updateRole();
}
@Override
public boolean isScopeParamRequired() {
return role.isScopeParamRequired();
}
@Override
public void setScopeParamRequired(boolean scopeParamRequired) {
role.setScopeParamRequired(scopeParamRequired);
updateRole();
}
@Override
public boolean isComposite() {
return role.getCompositeRoleIds() != null && role.getCompositeRoleIds().size() > 0;

View file

@ -2,6 +2,7 @@ package org.keycloak.protocol.oidc;
import org.jboss.logging.Logger;
import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@ -30,7 +31,7 @@ import org.keycloak.representations.RefreshToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.offline.OfflineUserSessionManager;
import org.keycloak.services.offline.OfflineTokenUtils;
import org.keycloak.util.RefreshTokenUtil;
import org.keycloak.util.Time;
@ -38,6 +39,9 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@ -92,13 +96,9 @@ public class TokenManager {
UserSessionModel userSession = null;
ClientSessionModel clientSession = null;
if (RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
// Check if offline tokens still allowed for the client
clientSession = new OfflineUserSessionManager().findOfflineClientSession(realm, user, oldToken.getClientSession(), oldToken.getSessionState());
if (clientSession != null) {
if (!clientSession.getClient().isOfflineTokensEnabled()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline tokens not allowed for client", "Offline tokens not allowed for client");
}
clientSession = OfflineTokenUtils.findOfflineClientSession(realm, user, oldToken.getClientSession(), oldToken.getSessionState());
if (clientSession != null) {
userSession = clientSession.getUserSession();
}
} else {
@ -136,7 +136,8 @@ public class TokenManager {
// recreate token.
Set<RoleModel> requestedRoles = TokenManager.getAccess(null, clientSession.getClient(), user);
String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
Set<RoleModel> requestedRoles = TokenManager.getAccess(scopeParam, true, clientSession.getClient(), user);
AccessToken newToken = createClientAccessToken(session, requestedRoles, realm, client, user, userSession, clientSession);
verifyAccess(oldToken, newToken);
@ -233,7 +234,8 @@ public class TokenManager {
clientSession.setUserSession(session);
Set<String> requestedRoles = new HashSet<String>();
// todo scope param protocol independent
for (RoleModel r : TokenManager.getAccess(null, clientSession.getClient(), user)) {
String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
for (RoleModel r : TokenManager.getAccess(scopeParam, true, clientSession.getClient(), user)) {
requestedRoles.add(r.getId());
}
clientSession.setRoles(requestedRoles);
@ -269,12 +271,14 @@ public class TokenManager {
}
}
public static Set<RoleModel> getAccess(String scopeParam, ClientModel client, UserModel user) {
// todo scopeParam is ignored until we figure out a scheme that fits with openid connect
public static Set<RoleModel> getAccess(String scopeParam, boolean applyScopeParam, ClientModel client, UserModel user) {
Set<RoleModel> requestedRoles = new HashSet<RoleModel>();
Set<RoleModel> roleMappings = user.getRoleMappings();
if (client.isFullScopeAllowed()) return roleMappings;
if (client.isFullScopeAllowed()) {
requestedRoles = roleMappings;
} else {
Set<RoleModel> scopeMappings = client.getScopeMappings();
scopeMappings.addAll(client.getRoles());
@ -285,10 +289,44 @@ public class TokenManager {
applyScope(role, desiredRole, visited, requestedRoles);
}
}
}
if (applyScopeParam) {
Collection<String> scopeParamRoles;
if (scopeParam != null) {
String[] scopes = scopeParam.split(" ");
scopeParamRoles = Arrays.asList(scopes);
} else {
scopeParamRoles = Collections.emptyList();
}
Set<RoleModel> roles = new HashSet<>();
for (RoleModel role : requestedRoles) {
String roleName = getRoleNameForScopeParam(role);
if (!role.isScopeParamRequired() || scopeParamRoles.contains(roleName)) {
roles.add(role);
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Role '%s' excluded by scope param. Client is '%s', User is '%s', Scope param is '%s' ", role.getName(), client.getClientId(), user.getUsername(), scopeParam);
}
}
}
requestedRoles = roles;
}
return requestedRoles;
}
// For now, just use "roleName" for realm roles and "clientId/roleName" for client roles
private static String getRoleNameForScopeParam(RoleModel role) {
if (role.getContainer() instanceof RealmModel) {
return role.getName();
} else {
ClientModel client = (ClientModel) role.getContainer();
return client.getClientId() + "/" + role.getName();
}
}
public void verifyAccess(AccessToken token, AccessToken newToken) throws OAuthErrorException {
if (token.getRealmAccess() != null) {
if (newToken.getRealmAccess() == null) throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm roles");
@ -437,7 +475,7 @@ public class TokenManager {
public AccessTokenResponseBuilder generateAccessToken() {
UserModel user = userSession.getUser();
String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
Set<RoleModel> requestedRoles = getAccess(scopeParam, client, user);
Set<RoleModel> requestedRoles = getAccess(scopeParam, true, client, user);
accessToken = createClientAccessToken(session, requestedRoles, realm, client, user, userSession, clientSession);
return this;
}
@ -450,14 +488,14 @@ public class TokenManager {
String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
boolean offlineTokenRequested = RefreshTokenUtil.isOfflineTokenRequested(scopeParam);
if (offlineTokenRequested) {
if (!clientSession.getClient().isOfflineTokensEnabled()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("invalid_client", "Offline tokens not allowed for the client", Response.Status.BAD_REQUEST);
if (!OfflineTokenUtils.isOfflineTokenAllowed(realm, clientSession)) {
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException("not_allowed", "Offline tokens not allowed for the user or client", Response.Status.BAD_REQUEST);
}
refreshToken = new RefreshToken(accessToken);
refreshToken.type(RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
new OfflineUserSessionManager().persistOfflineSession(clientSession, userSession);
OfflineTokenUtils.persistOfflineSession(clientSession, userSession);
} else {
refreshToken = new RefreshToken(accessToken);
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());

View file

@ -27,6 +27,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.OAuthClientRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.timer.TimerProvider;
import java.util.Collections;
@ -95,6 +96,7 @@ public class RealmManager implements RealmImporter {
setupImpersonationService(realm);
setupAuthenticationFlows(realm);
setupRequiredActions(realm);
setupOfflineTokens(realm);
return realm;
}
@ -107,6 +109,10 @@ public class RealmManager implements RealmImporter {
if (realm.getRequiredActionProviders().size() == 0) DefaultRequiredActions.addActions(realm);
}
protected void setupOfflineTokens(RealmModel realm) {
KeycloakModelUtils.setupOfflineTokens(realm);
}
protected void setupAdminConsole(RealmModel realm) {
ClientModel adminConsole = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
if (adminConsole == null) adminConsole = new ClientManager(this).createClient(realm, Constants.ADMIN_CONSOLE_CLIENT_ID);
@ -217,11 +223,13 @@ public class RealmManager implements RealmImporter {
RoleModel createRealmRole = realm.addRole(AdminRoles.CREATE_REALM);
adminRole.addCompositeRole(createRealmRole);
createRealmRole.setDescription("${role_" + AdminRoles.CREATE_REALM + "}");
createRealmRole.setScopeParamRequired(false);
} else {
adminRealm = model.getRealmByName(Config.getAdminRealm());
adminRole = adminRealm.getRole(AdminRoles.ADMIN);
}
adminRole.setDescription("${role_"+AdminRoles.ADMIN+"}");
adminRole.setScopeParamRequired(false);
ClientModel realmAdminApp = KeycloakModelUtils.createClient(adminRealm, KeycloakModelUtils.getMasterRealmAdminApplicationClientId(realm.getName()));
// No localized name for now
@ -232,6 +240,7 @@ public class RealmManager implements RealmImporter {
for (String r : AdminRoles.ALL_REALM_ROLES) {
RoleModel role = realmAdminApp.addRole(r);
role.setDescription("${role_"+r+"}");
role.setScopeParamRequired(false);
adminRole.addCompositeRole(role);
}
}
@ -249,12 +258,14 @@ public class RealmManager implements RealmImporter {
}
RoleModel adminRole = realmAdminClient.addRole(AdminRoles.REALM_ADMIN);
adminRole.setDescription("${role_" + AdminRoles.REALM_ADMIN + "}");
adminRole.setScopeParamRequired(false);
realmAdminClient.setBearerOnly(true);
realmAdminClient.setFullScopeAllowed(false);
for (String r : AdminRoles.ALL_REALM_ROLES) {
RoleModel role = realmAdminClient.addRole(r);
role.setDescription("${role_"+r+"}");
role.setScopeParamRequired(false);
adminRole.addCompositeRole(role);
}
}
@ -274,7 +285,9 @@ public class RealmManager implements RealmImporter {
for (String role : AccountRoles.ALL) {
client.addDefaultRole(role);
client.getRole(role).setDescription("${role_"+role+"}");
RoleModel roleModel = client.getRole(role);
roleModel.setDescription("${role_" + role + "}");
roleModel.setScopeParamRequired(false);
}
}
}
@ -292,7 +305,9 @@ public class RealmManager implements RealmImporter {
client.setFullScopeAllowed(false);
for (String role : Constants.BROKER_SERVICE_ROLES) {
client.addRole(role).setDescription("${role_"+ role.toLowerCase().replaceAll("_", "-") +"}");
RoleModel roleModel = client.addRole(role);
roleModel.setDescription("${role_"+ role.toLowerCase().replaceAll("_", "-") +"}");
roleModel.setScopeParamRequired(false);
}
}
}
@ -329,6 +344,7 @@ public class RealmManager implements RealmImporter {
if (!hasBrokerClient(rep)) setupBrokerService(realm);
if (!hasAdminConsoleClient(rep)) setupAdminConsole(realm);
if (!hasRealmRole(rep, Constants.OFFLINE_ACCESS_ROLE)) setupOfflineTokens(realm);
RepresentationToModel.importRealm(session, rep, realm);
@ -409,6 +425,20 @@ public class RealmManager implements RealmImporter {
return false;
}
private boolean hasRealmRole(RealmRepresentation rep, String roleName) {
if (rep.getRoles() == null || rep.getRoles().getRealm() == null) {
return false;
}
for (RoleRepresentation role : rep.getRoles().getRealm()) {
if (roleName.equals(role.getName())) {
return true;
}
}
return false;
}
/**
* Query users based on a search string:
* <p/>

View file

@ -3,18 +3,17 @@ package org.keycloak.services.offline;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.Constants;
import org.keycloak.models.ModelException;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.util.JsonSerialization;
@ -24,11 +23,11 @@ import org.keycloak.util.JsonSerialization;
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineUserSessionManager {
public class OfflineTokenUtils {
protected static Logger logger = Logger.getLogger(OfflineUserSessionManager.class);
protected static Logger logger = Logger.getLogger(OfflineTokenUtils.class);
public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
public static void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
UserModel user = userSession.getUser();
ClientModel client = clientSession.getClient();
@ -61,7 +60,7 @@ public class OfflineUserSessionManager {
}
// userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
public ClientSessionModel findOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
public static ClientSessionModel findOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
OfflineClientSessionModel clientSession = user.getOfflineClientSession(clientSessionId);
if (clientSession == null) {
return null;
@ -85,7 +84,7 @@ public class OfflineUserSessionManager {
return clientSessionAdapter;
}
public Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
public static Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
Set<ClientModel> clients = new HashSet<>();
for (OfflineClientSessionModel clientSession : clientSessions) {
@ -95,7 +94,7 @@ public class OfflineUserSessionManager {
return clients;
}
public boolean revokeOfflineToken(UserModel user, ClientModel client) {
public static boolean revokeOfflineToken(UserModel user, ClientModel client) {
Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
boolean anyRemoved = false;
for (OfflineClientSessionModel clientSession : clientSessions) {
@ -114,7 +113,17 @@ public class OfflineUserSessionManager {
return anyRemoved;
}
private void createOfflineUserSession(UserModel user, UserSessionModel userSession) {
public static boolean isOfflineTokenAllowed(RealmModel realm, ClientSessionModel clientSession) {
RoleModel offlineAccessRole = realm.getRole(Constants.OFFLINE_ACCESS_ROLE);
if (offlineAccessRole == null) {
logger.warnf("Role '%s' not available in realm", Constants.OFFLINE_ACCESS_ROLE);
return false;
}
return clientSession.getRoles().contains(offlineAccessRole.getId());
}
private static void createOfflineUserSession(UserModel user, UserSessionModel userSession) {
if (logger.isTraceEnabled()) {
logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername());
}
@ -138,7 +147,7 @@ public class OfflineUserSessionManager {
}
}
private void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
private static void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
if (logger.isTraceEnabled()) {
logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" ,
clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
@ -165,7 +174,7 @@ public class OfflineUserSessionManager {
}
// Check if userSession has any offline clientSessions attached to it. Remove userSession if not
private void checkUserSessionHasClientSessions(UserModel user, String userSessionId) {
private static void checkUserSessionHasClientSessions(UserModel user, String userSessionId) {
Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
for (OfflineClientSessionModel clientSession : clientSessions) {

View file

@ -46,7 +46,6 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.idm.CredentialRepresentation;
@ -58,7 +57,7 @@ import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.offline.OfflineUserSessionManager;
import org.keycloak.services.offline.OfflineTokenUtils;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
import org.keycloak.util.UriUtils;
@ -487,7 +486,7 @@ public class AccountService extends AbstractSecuredLocalService {
// Revoke grant in UserModel
UserModel user = auth.getUser();
user.revokeConsentForClient(client.getId());
new OfflineUserSessionManager().revokeOfflineToken(user, client);
OfflineTokenUtils.revokeOfflineToken(user, client);
// Logout clientSessions for this user and client
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);

View file

@ -82,6 +82,8 @@ public class RoleContainerResource extends RoleResource {
try {
RoleModel role = roleContainer.addRole(rep.getName());
role.setDescription(rep.getDescription());
boolean scopeParamRequired = rep.isScopeParamRequired()==null ? false : rep.isScopeParamRequired();
role.setScopeParamRequired(scopeParamRequired);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, role.getId()).representation(rep).success();

View file

@ -38,6 +38,7 @@ public abstract class RoleResource {
protected void updateRole(RoleRepresentation rep, RoleModel role) {
role.setName(rep.getName());
role.setDescription(rep.getDescription());
if (rep.isScopeParamRequired() != null) role.setScopeParamRequired(rep.isScopeParamRequired());
}
protected void addComposites(AdminEventBuilder adminEvent, UriInfo uriInfo, List<RoleRepresentation> roles, RoleModel role) {

View file

@ -18,6 +18,9 @@ public class RoleForm extends Form {
@FindBy(id = "description")
private WebElement descriptionInput;
@FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='scopeParamRequired']]")
private OnOffSwitch scopeParamRequired;
@FindBy(xpath = ".//div[contains(@class,'onoffswitch') and ./input[@id='compositeSwitch']]")
private OnOffSwitch compositeSwitch;
@ -28,7 +31,7 @@ public class RoleForm extends Form {
private WebElement removeIcon;
public RoleRepresentation getRole() {
RoleRepresentation role = new RoleRepresentation(getName(), getDescription());
RoleRepresentation role = new RoleRepresentation(getName(), getDescription(), isScopeParamRequired());
role.setComposite(isComposite());
if (role.isComposite()) {
role.setComposites(compositeRoles.getComposites());
@ -44,6 +47,7 @@ public class RoleForm extends Form {
RoleRepresentation role = new RoleRepresentation();
role.setName(getName());
role.setDescription(getDescription());
role.setScopeParamRequired(isScopeParamRequired());
role.setComposite(isComposite());
log.info(role.getName() + ": " + role.getDescription() + ", comp: " + role.isComposite());
return role;
@ -52,6 +56,7 @@ public class RoleForm extends Form {
public void setBasicAttributes(RoleRepresentation role) {
setName(role.getName());
setDescription(role.getDescription());
setScopeParamRequired(role.isScopeParamRequired());
if (role.isComposite()) {
setCompositeRoles(role);
}
@ -82,6 +87,14 @@ public class RoleForm extends Form {
return getInputValue(descriptionInput);
}
public void setScopeParamRequired(boolean scopeParamRequired) {
this.scopeParamRequired.setOn(scopeParamRequired);
}
public boolean isScopeParamRequired() {
return scopeParamRequired.isOn();
}
public void setComposite(boolean composite) {
compositeSwitch.setOn(composite);
}

View file

@ -44,7 +44,7 @@ public class ClientRolesTest extends AbstractClientTest {
@Test
public void testAddClientRole() {
ClientRepresentation newClient = createClientRepresentation("test-client1", "http://example.com/*");
RoleRepresentation newRole = new RoleRepresentation("client-role", "");
RoleRepresentation newRole = new RoleRepresentation("client-role", "", false);
createClient(newClient);
assertFlashMessageSuccess();

View file

@ -30,7 +30,7 @@ public class DefaultRolesTest extends AbstractRolesTest {
@Before
public void beforeDefaultRolesTest() {
// create a role via admin client
defaultRoleRep = new RoleRepresentation("default-role", "");
defaultRoleRep = new RoleRepresentation("default-role", "", false);
rolesPage.rolesResource().create(defaultRoleRep);
defaultRolesPage.navigateTo();

View file

@ -32,7 +32,7 @@ public class RealmRolesTest extends AbstractRolesTest {
@Before
public void beforeTestAddNewRole() {
testRole = new RoleRepresentation("test_role", "role description");
testRole = new RoleRepresentation("test_role", "role description", false);
realmRolesPage.navigateTo();
}
@ -104,7 +104,7 @@ public class RealmRolesTest extends AbstractRolesTest {
@Ignore
public void testAddRoleWithLongName() {
String name = "hjewr89y1894yh98(*&*&$jhjkashd)*(&y8934h*&@#hjkahsdj";
addRole(new RoleRepresentation(name, ""));
addRole(new RoleRepresentation(name, "", false));
assertNotNull(realmRolesPage.table().findRole(name));
}
@ -124,7 +124,7 @@ public class RealmRolesTest extends AbstractRolesTest {
Timer.time();
for (int i = 0; i < count; i++) {
String roleName = String.format("%s%02d", namePrefix, i);
RoleRepresentation rr = new RoleRepresentation(roleName, "");
RoleRepresentation rr = new RoleRepresentation(roleName, "", false);
testRealmResource().roles().create(rr);
}
Timer.time("create " + count + " roles");

View file

@ -668,7 +668,8 @@ public class AccountTest {
Assert.assertTrue(accountEntry.getProtocolMappersGranted().contains("Full Access"));
AccountApplicationsPage.AppEntry testAppEntry = apps.get("test-app");
Assert.assertEquals(4, testAppEntry.getRolesAvailable().size());
Assert.assertEquals(5, testAppEntry.getRolesAvailable().size());
Assert.assertTrue(testAppEntry.getRolesAvailable().contains("Offline access"));
Assert.assertTrue(testAppEntry.getRolesGranted().contains("Full Access"));
Assert.assertTrue(testAppEntry.getProtocolMappersGranted().contains("Full Access"));

View file

@ -85,7 +85,7 @@ public class AdminAPITest {
ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/master");
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
return tm.encodeToken(adminRealm, token);
} finally {
keycloakRule.stopSession(session, true);

View file

@ -108,7 +108,7 @@ public class ClientTest extends AbstractClientTest {
response.close();
String id = ApiUtil.getCreatedId(response);
RoleRepresentation role = new RoleRepresentation("test", "test");
RoleRepresentation role = new RoleRepresentation("test", "test", false);
realm.clients().get(id).roles().create(role);
rep = realm.clients().get(id).toRepresentation();

View file

@ -123,7 +123,7 @@ public class ImpersonationTest {
ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/" + realm);
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
return tm.encodeToken(adminRealm, token);
} finally {
keycloakRule.stopSession(session, true);

View file

@ -112,7 +112,7 @@ public class RealmTest extends AbstractClientTest {
@Test
// KEYCLOAK-1110
public void deleteDefaultRole() {
RoleRepresentation role = new RoleRepresentation("test", "test");
RoleRepresentation role = new RoleRepresentation("test", "test", false);
realm.roles().create(role);
assertNotNull(realm.roles().get("test").toRepresentation());

View file

@ -117,6 +117,7 @@ public class AbstractModelTest {
Assert.assertEquals(expected.getId(), actual.getId());
Assert.assertEquals(expected.getName(), actual.getName());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.isScopeParamRequired(), actual.isScopeParamRequired());
Assert.assertEquals(expected.getContainer(), actual.getContainer());
Assert.assertEquals(expected.getComposites().size(), actual.getComposites().size());
}

View file

@ -68,8 +68,8 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertEquals(realmModel.getName(), "JUGGLER");
Assert.assertArrayEquals(realmModel.getPrivateKey().getEncoded(), keyPair.getPrivate().getEncoded());
Assert.assertArrayEquals(realmModel.getPublicKey().getEncoded(), keyPair.getPublic().getEncoded());
Assert.assertEquals(1, realmModel.getDefaultRoles().size());
Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0));
Assert.assertEquals(2, realmModel.getDefaultRoles().size());
Assert.assertTrue(realmModel.getDefaultRoles().contains("foo"));
}
@Test
@ -94,8 +94,8 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertEquals(realmModel.getName(), "JUGGLER");
Assert.assertArrayEquals(realmModel.getPrivateKey().getEncoded(), keyPair.getPrivate().getEncoded());
Assert.assertArrayEquals(realmModel.getPublicKey().getEncoded(), keyPair.getPublic().getEncoded());
Assert.assertEquals(1, realmModel.getDefaultRoles().size());
Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0));
Assert.assertEquals(2, realmModel.getDefaultRoles().size());
Assert.assertTrue(realmModel.getDefaultRoles().contains("foo"));
realmModel.getId();
@ -444,7 +444,7 @@ public class AdapterTest extends AbstractModelTest {
realmModel.addRole("admin");
realmModel.addRole("user");
Set<RoleModel> roles = realmModel.getRoles();
Assert.assertEquals(3, roles.size());
Assert.assertEquals(4, roles.size());
UserModel user = realmManager.getSession().users().addUser(realmModel, "bburke");
RoleModel realmUserRole = realmModel.getRole("user");
user.grantRole(realmUserRole);
@ -470,7 +470,7 @@ public class AdapterTest extends AbstractModelTest {
user.grantRole(application.getRole("user"));
roles = user.getRealmRoleMappings();
Assert.assertEquals(roles.size(), 2);
Assert.assertEquals(roles.size(), 3);
assertRolesContains(realmUserRole, roles);
Assert.assertTrue(user.hasRole(realmUserRole));
// Role "foo" is default realm role
@ -485,13 +485,13 @@ public class AdapterTest extends AbstractModelTest {
// Test that application role 'user' don't clash with realm role 'user'
Assert.assertNotEquals(realmModel.getRole("user").getId(), application.getRole("user").getId());
Assert.assertEquals(6, user.getRoleMappings().size());
Assert.assertEquals(7, user.getRoleMappings().size());
// Revoke some roles
user.deleteRoleMapping(realmModel.getRole("foo"));
user.deleteRoleMapping(appBarRole);
roles = user.getRoleMappings();
Assert.assertEquals(4, roles.size());
Assert.assertEquals(5, roles.size());
assertRolesContains(realmUserRole, roles);
assertRolesContains(application.getRole("user"), roles);
Assert.assertFalse(user.hasRole(appBarRole));

View file

@ -79,7 +79,7 @@ public class ImportTest extends AbstractModelTest {
Assert.assertEquals(1, creds.size());
RequiredCredentialModel cred = creds.get(0);
Assert.assertEquals("password", cred.getFormLabel());
Assert.assertEquals(2, realm.getDefaultRoles().size());
Assert.assertEquals(3, realm.getDefaultRoles().size());
Assert.assertNotNull(realm.getRole("foo"));
Assert.assertNotNull(realm.getRole("bar"));
@ -132,6 +132,10 @@ public class ImportTest extends AbstractModelTest {
Assert.assertTrue(allRoles.contains(application.getRole("app-admin")));
Assert.assertTrue(allRoles.contains(otherApp.getRole("otherapp-admin")));
Assert.assertTrue(application.getRole("app-admin").isScopeParamRequired());
Assert.assertFalse(otherApp.getRole("otherapp-admin").isScopeParamRequired());
Assert.assertFalse(otherApp.getRole("otherapp-user").isScopeParamRequired());
UserModel wburke = session.users().getUserByUsername("wburke", realm);
// user with creation timestamp in import
Assert.assertEquals(new Long(123654), wburke.getCreatedTimestamp());
@ -326,8 +330,6 @@ public class ImportTest extends AbstractModelTest {
// Test service accounts
Assert.assertFalse(application.isServiceAccountsEnabled());
Assert.assertTrue(otherApp.isServiceAccountsEnabled());
Assert.assertFalse(application.isOfflineTokensEnabled());
Assert.assertTrue(otherApp.isOfflineTokensEnabled());
Assert.assertNull(session.users().getUserByServiceAccountClient(application));
UserModel linked = session.users().getUserByServiceAccountClient(otherApp);
Assert.assertNotNull(linked);

View file

@ -34,6 +34,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
@ -290,4 +291,71 @@ public class OAuthGrantTest {
});
}
@Test
public void oauthGrantScopeParamRequired() throws Exception {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
ClientModel thirdParty = appRealm.getClientByClientId("third-party");
RoleModel barAppRole = thirdParty.addRole("bar-role");
barAppRole.setScopeParamRequired(true);
RoleModel fooRole = appRealm.addRole("foo-role");
fooRole.setScopeParamRequired(true);
thirdParty.addScopeMapping(fooRole);
UserModel testUser = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
testUser.grantRole(fooRole);
testUser.grantRole(barAppRole);
}
});
// Assert roles not on grant screen when not requested
oauth.clientId("third-party");
oauth.doLoginGrant("test-user@localhost", "password");
grantPage.assertCurrent();
Assert.assertFalse(driver.getPageSource().contains("foo-role"));
Assert.assertFalse(driver.getPageSource().contains("bar-role"));
grantPage.cancel();
events.expectLogin()
.client("third-party")
.error("rejected_by_user")
.removeDetail(Details.CONSENT)
.assertEvent();
oauth.scope("foo-role third-party/bar-role");
oauth.doLoginGrant("test-user@localhost", "password");
grantPage.assertCurrent();
Assert.assertTrue(driver.getPageSource().contains("foo-role"));
Assert.assertTrue(driver.getPageSource().contains("bar-role"));
grantPage.accept();
events.expectLogin()
.client("third-party")
.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
.assertEvent();
// Revoke
accountAppsPage.open();
accountAppsPage.revokeGrant("third-party");
events.expect(EventType.REVOKE_GRANT)
.client("account").detail(Details.REVOKED_CLIENT, "third-party").assertEvent();
// cleanup
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.removeRole(appRealm.getRole("foo-role"));
ClientModel thirdparty = appRealm.getClientByClientId("third-party");
thirdparty.removeRole(thirdparty.getRole("bar-role"));
}
});
}
}

View file

@ -26,6 +26,7 @@ import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
@ -37,6 +38,7 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AccountApplicationsPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
@ -74,7 +76,6 @@ public class OfflineTokenTest {
RoleModel customerUserRole = appRealm.getClientByClientId("test-app").getRole("customer-user");
serviceAccountUser.grantRole(customerUserRole);
app.setOfflineTokensEnabled(true);
userId = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm).getId();
URL url = getClass().getResource("/oidc/offline-client-keycloak.json");
@ -101,6 +102,9 @@ public class OfflineTokenTest {
@WebResource
protected LoginPage loginPage;
@WebResource
protected OAuthGrantPage oauthGrantPage;
@WebResource
protected AccountApplicationsPage accountAppPage;
@ -115,23 +119,80 @@ public class OfflineTokenTest {
@Test
public void offlineTokenDisabledForClient() throws Exception {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.getClientByClientId("offline-client").setFullScopeAllowed(false);
}
});
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
oauth.redirectUri(offlineClientAppUri);
oauth.doLogin("test-user@localhost", "password");
Event loginEvent = events.expectLogin().assertEvent();
Event loginEvent = events.expectLogin()
.client("offline-client")
.detail(Details.REDIRECT_URI, offlineClientAppUri)
.assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
assertEquals(400, tokenResponse.getStatusCode());
assertEquals("invalid_client", tokenResponse.getError());
assertEquals("not_allowed", tokenResponse.getError());
events.expectCodeToToken(codeId, sessionId)
.error("invalid_client")
.client("offline-client")
.error("not_allowed")
.clearDetails()
.assertEvent();
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.getClientByClientId("offline-client").setFullScopeAllowed(true);
}
});
}
@Test
public void offlineTokenUserNotAllowed() throws Exception {
String userId = keycloakRule.getUser("test", "keycloak-user@localhost").getId();
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
oauth.redirectUri(offlineClientAppUri);
oauth.doLogin("keycloak-user@localhost", "password");
Event loginEvent = events.expectLogin()
.client("offline-client")
.user(userId)
.detail(Details.REDIRECT_URI, offlineClientAppUri)
.assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
assertEquals(400, tokenResponse.getStatusCode());
assertEquals("not_allowed", tokenResponse.getError());
events.expectCodeToToken(codeId, sessionId)
.client("offline-client")
.user(userId)
.error("not_allowed")
.clearDetails()
.assertEvent();
}
@ -206,8 +267,9 @@ public class OfflineTokenTest {
Assert.assertEquals(userId, refreshedToken.getSubject());
Assert.assertEquals(1, refreshedToken.getRealmAccess().getRoles().size());
Assert.assertEquals(2, refreshedToken.getRealmAccess().getRoles().size());
Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole(Constants.OFFLINE_ACCESS_ROLE));
Assert.assertEquals(1, refreshedToken.getResourceAccess("test-app").getRoles().size());
Assert.assertTrue(refreshedToken.getResourceAccess("test-app").isUserInRole("customer-user"));
@ -374,7 +436,7 @@ public class OfflineTokenTest {
accountAppPage.open();
List<String> additionalGrants = accountAppPage.getApplications().get("offline-client").getAdditionalGrants();
Assert.assertEquals(additionalGrants.size(), 1);
Assert.assertEquals(additionalGrants.get(0), "Offline Access");
Assert.assertEquals(additionalGrants.get(0), "Offline Token");
accountAppPage.revokeGrant("offline-client");
Assert.assertEquals(accountAppPage.getApplications().get("offline-client").getAdditionalGrants().size(), 0);
@ -389,6 +451,55 @@ public class OfflineTokenTest {
Time.setOffset(0);
}
@Test
public void testServletWithConsent() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.getClientByClientId("offline-client").setConsentRequired(true);
}
});
// Assert grant page doesn't have 'Offline Access' role when offline token is not requested
driver.navigate().to(offlineClientAppUri);
loginPage.login("test-user@localhost", "password");
oauthGrantPage.assertCurrent();
Assert.assertFalse(driver.getPageSource().contains("Offline access"));
oauthGrantPage.cancel();
// Assert grant page has 'Offline Access' role now
String servletUri = UriBuilder.fromUri(offlineClientAppUri)
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
.build().toString();
driver.navigate().to(servletUri);
loginPage.login("test-user@localhost", "password");
oauthGrantPage.assertCurrent();
Assert.assertTrue(driver.getPageSource().contains("Offline access"));
oauthGrantPage.accept();
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
accountAppPage.open();
AccountApplicationsPage.AppEntry offlineClient = accountAppPage.getApplications().get("offline-client");
Assert.assertTrue(offlineClient.getRolesGranted().contains("Offline access"));
Assert.assertTrue(offlineClient.getAdditionalGrants().contains("Offline Token"));
events.clear();
// Revert change
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.getClientByClientId("offline-client").setConsentRequired(false);
}
});
}
public static class OfflineTokenServlet extends HttpServlet {
private static TokenInfo tokenInfo;

View file

@ -457,7 +457,7 @@ public class SamlBindingTest {
ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/master");
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
return tm.encodeToken(adminRealm, token);
} finally {
keycloakRule.stopSession(session, true);

View file

@ -164,7 +164,6 @@
"name": "Other Application",
"enabled": true,
"serviceAccountsEnabled": true,
"offlineTokensEnabled": true,
"clientAuthenticatorType": "client-jwt",
"protocolMappers" : [
{
@ -199,7 +198,8 @@
"application" : {
"Application" : [
{
"name": "app-admin"
"name": "app-admin",
"scopeParamRequired": true
},
{
"name": "app-user"
@ -207,7 +207,8 @@
],
"OtherApp" : [
{
"name": "otherapp-admin"
"name": "otherapp-admin",
"scopeParamRequired": false
},
{
"name": "otherapp-user"

View file

@ -26,7 +26,7 @@
{ "type" : "password",
"value" : "password" }
],
"realmRoles": ["user"],
"realmRoles": ["user", "offline_access"],
"clientRoles": {
"test-app": [ "customer-user" ],
"account": [ "view-profile", "manage-account" ]