oidc broker role mapper

This commit is contained in:
Bill Burke 2015-04-17 16:41:04 -04:00
parent edb9f0cecf
commit a7c563b0eb
17 changed files with 701 additions and 50 deletions

View file

@ -0,0 +1,31 @@
package org.keycloak.broker.provider;
import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractIdentityProviderMapper implements IdentityProviderMapper {
@Override
public void close() {
}
@Override
public IdentityProviderMapper create(KeycloakSession session) {
return null;
}
@Override
public void init(org.keycloak.Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
}

View file

@ -1,11 +1,14 @@
package org.keycloak.broker.oidc;
import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.services.managers.AuthenticationManager;
@ -23,6 +26,8 @@ import java.security.PublicKey;
*/
public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) {
super(config);
}
@ -32,6 +37,12 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
return new KeycloakEndpoint(callback, realm, event);
}
@Override
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
JsonWebToken access = validateToken(idpKey, response.getToken());
context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
}
protected class KeycloakEndpoint extends OIDCEndpoint {
public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
super(callback, realm, event);

View file

@ -95,13 +95,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
protected boolean verify(JWSInput jws, PublicKey key) {
if (key == null) return true;
if (!getConfig().isValidateSignature()) return true;
return RSAProvider.verify(jws, key);
}
protected class OIDCEndpoint extends Endpoint {
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
super(callback, realm, event);
@ -160,6 +153,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return authorizationUrl;
}
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
}
@Override
protected BrokeredIdentityContext getFederatedIdentity(String response) {
AccessTokenResponse tokenResponse = null;
@ -175,7 +172,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
JsonWebToken idToken = validateIdToken(key, encodedIdToken);
JsonWebToken idToken = validateToken(key, encodedIdToken);
try {
String id = idToken.getSubject();
@ -197,6 +194,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
processAccessTokenResponse(identity, key, tokenResponse);
identity.setId(id);
identity.setName(name);
@ -236,23 +234,34 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return accessToken;
}
private JsonWebToken validateIdToken(PublicKey key, String encodedToken) {
protected boolean verify(JWSInput jws, PublicKey key) {
if (key == null) return true;
if (!getConfig().isValidateSignature()) return true;
return RSAProvider.verify(jws, key);
}
protected JsonWebToken validateToken(PublicKey key, String encodedToken) {
if (encodedToken == null) {
throw new IdentityBrokerException("No id_token from server.");
throw new IdentityBrokerException("No token from server.");
}
try {
JWSInput jws = new JWSInput(encodedToken);
if (!verify(jws, key)) {
throw new IdentityBrokerException("IDToken signature validation failed");
throw new IdentityBrokerException("token signature validation failed");
}
JsonWebToken idToken = jws.readJsonContent(JsonWebToken.class);
JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
String aud = idToken.getAudience();
String iss = idToken.getIssuer();
String aud = token.getAudience();
String iss = token.getIssuer();
if (aud != null && !aud.equals(getConfig().getClientId())) {
throw new IdentityBrokerException("Wrong audience from id_token..");
throw new IdentityBrokerException("Wrong audience from token.");
}
if (!token.isActive()) {
throw new IdentityBrokerException("Token is no longer valid");
}
String trustedIssuers = getConfig().getIssuer();
@ -262,15 +271,15 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
for (String trustedIssuer : issuers) {
if (iss != null && iss.equals(trustedIssuer.trim())) {
return idToken;
return token;
}
}
throw new IdentityBrokerException("Wrong issuer from id_token. Got: " + iss + " expected: " + getConfig().getIssuer());
throw new IdentityBrokerException("Wrong issuer from token. Got: " + iss + " expected: " + getConfig().getIssuer());
}
return idToken;
return token;
} catch (IOException e) {
throw new IdentityBrokerException("Could not decode id token.", e);
throw new IdentityBrokerException("Could not decode token.", e);
}
}

View file

@ -0,0 +1,230 @@
package org.keycloak.broker.oidc.mappers;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.JsonWebToken;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RoleMapper extends AbstractIdentityProviderMapper {
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
public static final String ROLE = "role";
public static final String CLAIM = "claim";
public static final String ID_TOKEN_CLAIM = "id.token.claim";
public static final String ACCESS_TOKEN_CLAIM = "access.token.claim";
public static final String CLAIM_VALUE = "claim.value";
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(CLAIM);
property.setLabel("Claim");
property.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(CLAIM_VALUE);
property.setLabel("Claim Value");
property.setHelpText("Value the claim must have. If the claim is an array, then the value must be contained in the array.");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(ID_TOKEN_CLAIM);
property.setLabel("ID Token Claim");
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText("If this claim is in ID Token, apply role.");
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(ACCESS_TOKEN_CLAIM);
property.setLabel("Access Token Claim");
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText("If this claim is in Access Token, apply role.");
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(ROLE);
property.setLabel("Role");
property.setHelpText("Role to grant to user. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
}
public static final String PROVIDER_ID = "oidc-role-idp-mapper";
public static String[] parseRole(String role) {
int scopeIndex = role.indexOf('.');
if (scopeIndex > -1) {
String appName = role.substring(0, scopeIndex);
role = role.substring(scopeIndex + 1);
String[] rtn = {appName, role};
return rtn;
} else {
String[] rtn = {null, role};
return rtn;
}
}
public static Object getClaimValue(JsonWebToken token, String claim) {
String[] split = claim.split("\\.");
Map<String, Object> jsonObject = token.getOtherClaims();
for (int i = 0; i < split.length; i++) {
if (i == split.length - 1) {
return jsonObject.get(split[i]);
} else {
Object val = jsonObject.get(split[i]);
if (!(val instanceof Map)) return null;
jsonObject = (Map<String, Object>)val;
}
}
return null;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String[] getCompatibleProviders() {
return COMPATIBLE_PROVIDERS;
}
@Override
public String getDisplayCategory() {
return "Role Mapper";
}
@Override
public String getDisplayType() {
return "Role Mapper";
}
@Override
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String roleName = mapperModel.getConfig().get(ROLE);
if (isClaimPresent(mapperModel, context)) {
RoleModel role = getRoleFromString(realm, roleName);
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
user.grantRole(role);
}
}
protected RoleModel getRoleFromString(RealmModel realm, String roleName) {
String[] parsedRole = parseRole(roleName);
RoleModel role = null;
if (parsedRole[0] == null) {
role = realm.getRole(parsedRole[1]);
} else {
ClientModel client = realm.getClientByClientId(parsedRole[0]);
role = client.getRole(parsedRole[1]);
}
return role;
}
protected boolean isClaimPresent(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
boolean searchAccess = Boolean.valueOf(mapperModel.getConfig().get(ACCESS_TOKEN_CLAIM));
boolean searchId = Boolean.valueOf(mapperModel.getConfig().get(ID_TOKEN_CLAIM));
String claim = mapperModel.getConfig().get(CLAIM);
String desiredValue = mapperModel.getConfig().get(CLAIM_VALUE);
if (searchAccess) {
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
if (token != null) {
Object value = getClaimValue(token, claim);
if (valueEquals(desiredValue, value)) return true;
}
}
if (searchId) {
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN);
if (token != null) {
Object value = getClaimValue(token, claim);
if (valueEquals(desiredValue, value)) return true;
}
}
return false;
}
public boolean valueEquals(String desiredValue, Object value) {
if (value instanceof String) {
if (desiredValue.equals(value)) return true;
} else if (value instanceof Double) {
try {
if (Double.valueOf(desiredValue).equals(value)) return true;
} catch (Exception e) {
}
} else if (value instanceof Integer) {
try {
if (Integer.valueOf(desiredValue).equals(value)) return true;
} catch (Exception e) {
}
} else if (value instanceof Boolean) {
try {
if (Boolean.valueOf(desiredValue).equals(value)) return true;
} catch (Exception e) {
}
} else if (value instanceof List) {
List list = (List)value;
for (Object val : list) {
return valueEquals(desiredValue, val);
}
}
return false;
}
@Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String roleName = mapperModel.getConfig().get(ROLE);
if (!isClaimPresent(mapperModel, context)) {
RoleModel role = getRoleFromString(realm, roleName);
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
user.deleteRoleMapping(role);
}
}
@Override
public String getHelpText() {
return "If a claim exists, grant the user the specified realm or application role.";
}
}

View file

@ -0,0 +1 @@
org.keycloak.broker.oidc.mappers.RoleMapper

View file

@ -1,5 +1,6 @@
package org.keycloak.representations.idm;
import java.util.LinkedList;
import java.util.List;
/**
@ -12,7 +13,7 @@ public class IdentityProviderMapperTypeRepresentation {
protected String category;
protected String helpText;
protected List<ConfigPropertyRepresentation> properties;
protected List<ConfigPropertyRepresentation> properties = new LinkedList<>();
public String getId() {
return id;

View file

@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.codehaus.jackson.annotate.JsonAnyGetter;
import org.codehaus.jackson.annotate.JsonAnySetter;
@ -12,6 +13,7 @@ import org.codehaus.jackson.annotate.JsonUnwrapped;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.JsonSerialization;
@ -20,6 +22,32 @@ import org.keycloak.util.JsonSerialization;
*/
public class JsonParserTest {
@Test
public void regex() throws Exception {
Pattern p = Pattern.compile(".*(?!\\.pdf)");
if (p.matcher("foo.pdf").matches()) {
System.out.println(".pdf no match");
}
if (p.matcher("foo.txt").matches()) {
System.out.println("foo.txt matches");
}
}
@Test
public void testOtherClaims() throws Exception {
String json = "{ \"floatData\" : 555.5," +
"\"boolData\": true, " +
"\"intData\": 1234," +
"\"array\": [ \"val\", \"val2\"] }";
JsonWebToken token = JsonSerialization.readValue(json, JsonWebToken.class);
System.out.println(token.getOtherClaims().get("floatData").getClass().getName());
System.out.println(token.getOtherClaims().get("boolData").getClass().getName());
System.out.println(token.getOtherClaims().get("intData").getClass().getName());
System.out.println(token.getOtherClaims().get("array").getClass().getName());
}
@Test
public void testUnwrap() throws Exception {
// just experimenting with unwrapped and any properties

View file

@ -206,6 +206,58 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmIdentityProviderExportCtrl'
})
.when('/realms/:realm/identity-provider-mappers/:alias/mappers', {
templateUrl : function(params){ return resourceUrl + '/partials/identity-provider-mappers.html'; },
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
identityProvider : function(IdentityProviderLoader) {
return IdentityProviderLoader();
},
mapperTypes : function(IdentityProviderMapperTypesLoader) {
return IdentityProviderMapperTypesLoader();
},
mappers : function(IdentityProviderMappersLoader) {
return IdentityProviderMappersLoader();
}
},
controller : 'IdentityProviderMapperListCtrl'
})
.when('/realms/:realm/identity-provider-mappers/:alias/mappers/:mapperId', {
templateUrl : function(params){ return resourceUrl + '/partials/identity-provider-mapper-detail.html'; },
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
identityProvider : function(IdentityProviderLoader) {
return IdentityProviderLoader();
},
mapperTypes : function(IdentityProviderMapperTypesLoader) {
return IdentityProviderMapperTypesLoader();
},
mapper : function(IdentityProviderMapperLoader) {
return IdentityProviderMapperLoader();
}
},
controller : 'IdentityProviderMapperCtrl'
})
.when('/create/identity-provider-mappers/:realm/:alias', {
templateUrl : function(params){ return resourceUrl + '/partials/identity-provider-mapper-detail.html'; },
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
identityProvider : function(IdentityProviderLoader) {
return IdentityProviderLoader();
},
mapperTypes : function(IdentityProviderMapperTypesLoader) {
return IdentityProviderMapperTypesLoader();
}
},
controller : 'IdentityProviderMapperCreateCtrl'
})
.when('/realms/:realm/default-roles', {
templateUrl : resourceUrl + '/partials/realm-default-roles.html',
resolve : {

View file

@ -1066,30 +1066,6 @@ module.controller('ClientClusteringNodeCtrl', function($scope, client, Client, C
}
});
module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client, serverInfo,
ClientProtocolMappersByProtocol,
$http, $location, Dialog, Notifications) {
$scope.realm = realm;
$scope.client = client;
if (client.protocol == null) {
client.protocol = 'openid-connect';
}
var protocolMappers = serverInfo.protocolMapperTypes[client.protocol];
var mapperTypes = {};
for (var i = 0; i < protocolMappers.length; i++) {
mapperTypes[protocolMappers[i].id] = protocolMappers[i];
}
$scope.mapperTypes = mapperTypes;
var updateMappers = function() {
$scope.mappers = ClientProtocolMappersByProtocol.query({realm : realm.realm, client : client.id, protocol : client.protocol});
};
updateMappers();
});
module.controller('AddBuiltinProtocolMapperCtrl', function($scope, realm, client, serverInfo,
ClientProtocolMappersByProtocol,
$http, $location, Dialog, Notifications) {
@ -1152,6 +1128,30 @@ module.controller('AddBuiltinProtocolMapperCtrl', function($scope, realm, client
});
module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client, serverInfo,
ClientProtocolMappersByProtocol,
$http, $location, Dialog, Notifications) {
$scope.realm = realm;
$scope.client = client;
if (client.protocol == null) {
client.protocol = 'openid-connect';
}
var protocolMappers = serverInfo.protocolMapperTypes[client.protocol];
var mapperTypes = {};
for (var i = 0; i < protocolMappers.length; i++) {
mapperTypes[protocolMappers[i].id] = protocolMappers[i];
}
$scope.mapperTypes = mapperTypes;
var updateMappers = function() {
$scope.mappers = ClientProtocolMappersByProtocol.query({realm : realm.realm, client : client.id, protocol : client.protocol});
};
updateMappers();
});
module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo, client, mapper, ClientProtocolMapper, Notifications, Dialog, $location) {
$scope.realm = realm;
$scope.client = client;

View file

@ -807,7 +807,6 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
});
};
$scope.$watch('fromUrl.data', function(newVal, oldVal){
console.log('watch fromUrl: ' + newVal + " " + oldVal);
if ($scope.fromUrl.data && $scope.fromUrl.data.length > 0) {
$scope.importUrl = true;
} else{
@ -1412,3 +1411,99 @@ module.controller('RealmBruteForceCtrl', function($scope, Realm, realm, $http, $
});
module.controller('IdentityProviderMapperListCtrl', function($scope, realm, identityProvider, mapperTypes, mappers) {
$scope.realm = realm;
$scope.identityProvider = identityProvider;
$scope.mapperTypes = mapperTypes;
$scope.mappers = mappers;
});
module.controller('IdentityProviderMapperCtrl', function($scope, realm, identityProvider, mapperTypes, mapper, IdentityProviderMapper, Notifications, Dialog, $location) {
$scope.realm = realm;
$scope.identityProvider = identityProvider;
$scope.create = false;
$scope.mapper = angular.copy(mapper);
$scope.changed = false;
$scope.mapperType = mapperTypes[mapper.identityProviderMapper];
$scope.$watch(function() {
return $location.path();
}, function() {
$scope.path = $location.path().substring(1).split("/");
});
$scope.$watch('mapper', function() {
if (!angular.equals($scope.mapper, mapper)) {
$scope.changed = true;
}
}, true);
$scope.save = function() {
IdentityProviderMapper.update({
realm : realm.realm,
client: client.id,
mapperId : mapper.id
}, $scope.mapper, function() {
$scope.changed = false;
mapper = angular.copy($scope.mapper);
$location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers/" + mapper.id);
Notifications.success("Your changes have been saved.");
});
};
$scope.reset = function() {
$scope.mapper = angular.copy(mapper);
$scope.changed = false;
};
$scope.cancel = function() {
//$location.url("/realms");
window.history.back();
};
$scope.remove = function() {
Dialog.confirmDelete($scope.mapper.name, 'mapper', function() {
IdentityProviderMapper.remove({ realm: realm.realm, alias: mapper.identityProviderAlias, mapperId : $scope.mapper.id }, function() {
Notifications.success("The mapper has been deleted.");
$location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers");
});
});
};
});
module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, identityProvider, mapperTypes, IdentityProviderMapper, Notifications, Dialog, $location) {
$scope.realm = realm;
$scope.identityProvider = identityProvider;
$scope.create = true;
$scope.mapper = { identityProviderAlias: identityProvider.alias, config: {}};
$scope.mapperTypes = mapperTypes;
$scope.$watch(function() {
return $location.path();
}, function() {
$scope.path = $location.path().substring(1).split("/");
});
$scope.save = function() {
$scope.mapper.identityProviderMapper = $scope.mapperType.id;
IdentityProviderMapper.save({
realm : realm.realm, alias: identityProvider.alias
}, $scope.mapper, function(data, headers) {
var l = headers().location;
var id = l.substring(l.lastIndexOf("/") + 1);
$location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers/" + id);
Notifications.success("Mapper has been created.");
});
};
$scope.cancel = function() {
//$location.url("/realms");
window.history.back();
};
});

View file

@ -266,4 +266,33 @@ module.factory('IdentityProviderFactoryLoader', function(Loader, IdentityProvide
provider_id: $route.current.params.provider_id
}
});
});
});
module.factory('IdentityProviderMapperTypesLoader', function(Loader, IdentityProviderMapperTypes, $route, $q) {
return Loader.get(IdentityProviderMapperTypes, function () {
return {
realm: $route.current.params.realm,
alias: $route.current.params.alias
}
});
});
module.factory('IdentityProviderMappersLoader', function(Loader, IdentityProviderMappers, $route, $q) {
return Loader.query(IdentityProviderMappers, function () {
return {
realm: $route.current.params.realm,
alias: $route.current.params.alias
}
});
});
module.factory('IdentityProviderMapperLoader', function(Loader, IdentityProviderMapper, $route, $q) {
return Loader.get(IdentityProviderMapper, function () {
return {
realm: $route.current.params.realm,
alias: $route.current.params.alias,
mapperId: $route.current.params.mapperId
}
});
});

View file

@ -1002,4 +1002,27 @@ module.factory('IdentityProviderFactory', function($resource) {
realm : '@realm',
provider_id : '@provider_id'
});
});
});
module.factory('IdentityProviderMapperTypes', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:alias/mapper-types', {
realm : '@realm',
alias : '@alias'
});
});
module.factory('IdentityProviderMappers', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:alias/mappers', {
realm : '@realm',
alias : '@alias'
});
});
module.factory('IdentityProviderMapper', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:alias/mappers/:mapperId', {
realm : '@realm',
alias : '@alias',
mapperId: '@mapperId'
});
});

View file

@ -0,0 +1,86 @@
<div class="bs-sidebar col-sm-3 " data-ng-include data-src="resourceUrl + '/partials/realm-menu.html'"></div>
<div id="content-area" class="col-sm-9" role="main">
<kc-navigation-client></kc-navigation-client>
<div id="content">
<ol class="breadcrumb" data-ng-show="create">
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">Identity Providers</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}} Provider</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Identity Provider Mappers</a></li>
<li class="active">Create IdentityProvider Mapper</li>
</ol>
<ol class="breadcrumb" data-ng-hide="create">
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">Identity Providers</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}} Provider</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Identity Provider Mappers</a></li>
<li class="active">{{mapper.name}}</li>
</ol>
<h2 class="pull-left" data-ng-hide="create">{{mapper.name}} Identity Provider Mapper</h2>
<h2 class="pull-left" data-ng-show="create">Create Identity Provider Mapper</h2>
<p class="subtitle"><span class="required">*</span> Required fields</p>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset>
<div class="form-group clearfix" data-ng-show="!create">
<label class="col-sm-2 control-label" for="mapperId">ID </label>
<div class="col-sm-4">
<input class="form-control" id="mapperId" type="text" ng-model="mapper.id" readonly>
</div>
</div>
<div class="form-group clearfix">
<label class="col-sm-2 control-label" for="name">Name <span class="required">*</span></label>
<div class="col-sm-4">
<input class="form-control" id="name" type="text" ng-model="mapper.name" data-ng-readonly="!create" required>
</div>
<span tooltip-placement="right" tooltip="Name of the mapper." class="fa fa-info-circle"></span>
</div>
<div class="form-group" data-ng-show="create">
<label class="col-sm-2 control-label" for="mapperTypeCreate">Mapper Type</label>
<div class="col-sm-6">
<div class="select-kc">
<select id="mapperTypeCreate"
ng-model="mapperType"
ng-options="mapperType.name for (mapperKey, mapperType) in mapperTypes">
</select>
</div>
</div>
<span tooltip-placement="right" tooltip="{{mapperType.helpText}}" class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix" data-ng-hide="create">
<label class="col-sm-2 control-label" for="mapperType">Mapper Type</label>
<div class="col-sm-4">
<input class="form-control" id="mapperType" type="text" ng-model="mapperType.name" data-ng-readonly="true">
</div>
<span tooltip-placement="right" tooltip="{{mapperType.helpText}}" class="fa fa-info-circle"></span>
</div>
<div data-ng-repeat="option in mapperType.properties" class="form-group">
<label class="col-sm-2 control-label">{{option.label}}</label>
<div class="col-sm-4" data-ng-hide="option.type == 'boolean' || option.type == 'List'">
<input class="form-control" type="text" data-ng-model="mapper.config[ option.name ]" >
</div>
<div class="col-sm-4" data-ng-show="option.type == 'boolean'">
<input ng-model="mapper.config[ option.name ]" value="'true'" name="option.name" id="option.name" onoffswitchmodel />
</div>
<div class="col-sm-4" data-ng-show="option.type == 'List'">
<select ng-model="mapper.config[ option.name ]" ng-options="data for data in option.defaultValue">
<option value="" selected> Select one... </option>
</select>
</div>
<span tooltip-placement="right" tooltip="{{option.helpText}}" class="fa fa-info-circle"></span>
</div>
</fieldset>
<div class="pull-right form-actions" data-ng-show="create && access.manageRealm">
<button kc-cancel data-ng-click="cancel()">Cancel</button>
<button kc-save>Save</button>
</div>
<div class="pull-right form-actions" data-ng-show="!create && access.manageRealm">
<button kc-reset data-ng-show="changed">Clear changes</button>
<button kc-save data-ng-show="changed">Save</button>
<button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete</button>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,46 @@
<div class="bs-sidebar col-md-3 clearfix" data-ng-include data-src="resourceUrl + '/partials/realm-menu.html'"></div>
<div id="content-area" class="col-md-9" role="main">
<kc-navigation-client></kc-navigation-client>
<div id="content">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">Identity Providers</a></li>
<li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}} Provider</a></li>
<li class="active">{{identityProvider.alias}} Mappers</li>
</ol>
<h2><span>{{realm.realm}} </span> {{identityProvider.alias}} Identity Provider Mappers <span tooltip-placement="right" tooltip="Identity Provider Mappers perform transformation on tokens and documents. They an do things like map external tokens and claims into role grants and user attributes." class="fa fa-info-circle"></span></h2>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="4">
<div class="search-comp clearfix">
<input type="text" placeholder="Search..." class="form-control search" data-ng-model="search.name"
onkeyup="if(event.keyCode == 13){$(this).next('button').click();}">
<button type="submit" class="kc-icon-search" tooltip-placement="right"
tooltip="Search by mapper name.">
Icon: search
</button>
</div>
<div class="pull-right">
<a class="btn btn-primary" href="#/create/identity-provider-mappers/{{realm.realm}}/{{identityProvider.alias}}">Create</a>
</div>
</th>
</tr>
<tr data-ng-hide="mappers.length == 0">
<th>Name</th>
<th>Category</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="mapper in mappers | filter:search">
<td><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers/{{mapper.id}}">{{mapper.name}}</a></td>
<td>{{mapperTypes[mapper.identityProviderMapper].category}}</td>
<td>{{mapperTypes[mapper.identityProviderMapper].name}}</td>
</tr>
<tr data-ng-show="mappers.length == 0">
<td>No mappers available</td>
</tr>
</tbody>
</table>
</div>
</div>

View file

@ -185,6 +185,7 @@
</fieldset>
<div class="pull-right form-actions">
<a data-ng-show="!newIdentityProvider" class="btn btn-lg btn-primary" href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a>
<button kc-save data-ng-show="changed">Save</button>
<button type="submit" data-ng-click="cancel()" data-ng-show="changed" class="btn btn-lg btn-default">Cancel</button>
<button kc-delete data-ng-click="remove()" data-ng-show="!newIdentityProvider">Delete</button>

View file

@ -43,8 +43,10 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author Pedro Igor
@ -194,10 +196,10 @@ public class IdentityProviderResource {
@GET
@Path("mapper-types")
@NoCache
public List<IdentityProviderMapperTypeRepresentation> getMapperTypes() {
public Map<String, IdentityProviderMapperTypeRepresentation> getMapperTypes() {
this.auth.requireView();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
List<IdentityProviderMapperTypeRepresentation> types = new LinkedList<>();
Map<String, IdentityProviderMapperTypeRepresentation> types = new HashMap<>();
List<ProviderFactory> factories = sessionFactory.getProviderFactories(IdentityProviderMapper.class);
for (ProviderFactory factory : factories) {
IdentityProviderMapper mapper = (IdentityProviderMapper)factory;
@ -218,7 +220,7 @@ public class IdentityProviderResource {
propRep.setHelpText(prop.getHelpText());
rep.getProperties().add(propRep);
}
types.add(rep);
types.put(rep.getId(), rep);
}
}

View file

@ -24,6 +24,7 @@ package org.keycloak.testsuite.account;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
@ -156,6 +157,11 @@ public class AccountTest {
});
}
@Test
public void ideTesting() throws Exception {
Thread.sleep(100000000);
}
@Test
public void returnToAppFromQueryParam() {
driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app");