[KEYCLOAK-18425] - Allow mapping user profile attributes

This commit is contained in:
Pedro Igor 2021-06-30 17:45:55 -03:00
parent 7af2133924
commit 3e07ca3c22
11 changed files with 300 additions and 8 deletions

View file

@ -62,6 +62,8 @@ public class ProviderConfigProperty {
*/ */
public static final String MAP_TYPE ="Map"; public static final String MAP_TYPE ="Map";
public static final String USER_PROFILE_ATTRIBUTE_LIST_TYPE="UserProfileAttributeList";
protected String name; protected String name;
protected String label; protected String label;
protected String helpText; protected String helpText;

View file

@ -48,6 +48,7 @@ public class ProtocolMapperUtils {
public static final String USER_MODEL_PROPERTY_HELP_TEXT = "usermodel.prop.tooltip"; public static final String USER_MODEL_PROPERTY_HELP_TEXT = "usermodel.prop.tooltip";
public static final String USER_MODEL_ATTRIBUTE_LABEL = "usermodel.attr.label"; public static final String USER_MODEL_ATTRIBUTE_LABEL = "usermodel.attr.label";
public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "usermodel.attr.tooltip"; public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "usermodel.attr.tooltip";
public static final String USER_PROFILE_ATTRIBUTE = "user.profile.attribute";
public static final String USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID = "usermodel.clientRoleMapping.clientId"; public static final String USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID = "usermodel.clientRoleMapping.clientId";
public static final String USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID_LABEL = "usermodel.clientRoleMapping.clientId.label"; public static final String USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID_LABEL = "usermodel.clientRoleMapping.clientId.label";

View file

@ -233,11 +233,29 @@ public class OIDCAttributeMapperHelper {
String tokenClaimName, String claimType, String tokenClaimName, String claimType,
boolean accessToken, boolean idToken, boolean accessToken, boolean idToken,
String mapperId) { String mapperId) {
return createClaimMapper(name, userAttribute,tokenClaimName, claimType, accessToken, idToken, true, mapperId); return createClaimMapper(name, userAttribute, null, tokenClaimName, claimType, accessToken, idToken, true, mapperId);
}
public static ProtocolMapperModel createClaimMapper(String name,
String userAttribute,
String userProfileAttribute,
String tokenClaimName, String claimType,
boolean accessToken, boolean idToken,
String mapperId) {
return createClaimMapper(name, userAttribute, userProfileAttribute, tokenClaimName, claimType, accessToken, idToken, true, mapperId);
}
public static ProtocolMapperModel createClaimMapper(String name,
String userAttribute,
String tokenClaimName, String claimType,
boolean accessToken, boolean idToken, boolean userinfo,
String mapperId) {
return createClaimMapper(name, userAttribute, null, tokenClaimName, claimType, accessToken, idToken, userinfo, mapperId);
} }
public static ProtocolMapperModel createClaimMapper(String name, public static ProtocolMapperModel createClaimMapper(String name,
String userAttribute, String userAttribute,
String userProfileAttribute,
String tokenClaimName, String claimType, String tokenClaimName, String claimType,
boolean accessToken, boolean idToken, boolean userinfo, boolean accessToken, boolean idToken, boolean userinfo,
String mapperId) { String mapperId) {
@ -247,6 +265,10 @@ public class OIDCAttributeMapperHelper {
mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
Map<String, String> config = new HashMap<String, String>(); Map<String, String> config = new HashMap<String, String>();
config.put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute); config.put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute);
if (userProfileAttribute != null) {
config.put(ProtocolMapperUtils.USER_PROFILE_ATTRIBUTE, userProfileAttribute);
}
config.put(TOKEN_CLAIM_NAME, tokenClaimName); config.put(TOKEN_CLAIM_NAME, tokenClaimName);
config.put(JSON_TYPE, claimType); config.put(JSON_TYPE, claimType);
if (accessToken) config.put(INCLUDE_IN_ACCESS_TOKEN, "true"); if (accessToken) config.put(INCLUDE_IN_ACCESS_TOKEN, "true");

View file

@ -18,16 +18,19 @@
package org.keycloak.protocol.oidc.mappers; package org.keycloak.protocol.oidc.mappers;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Mappings UserModel.attribute to an ID Token claim. Token claim name can be a full qualified nested object name, * Mappings UserModel.attribute to an ID Token claim. Token claim name can be a full qualified nested object name,
@ -49,6 +52,14 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT); property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT);
property.setType(ProviderConfigProperty.STRING_TYPE); property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property); configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(ProtocolMapperUtils.USER_PROFILE_ATTRIBUTE);
property.setLabel(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_LABEL);
property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT);
property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE);
configProperties.add(property);
OIDCAttributeMapperHelper.addAttributeConfig(configProperties, UserAttributeMapper.class); OIDCAttributeMapperHelper.addAttributeConfig(configProperties, UserAttributeMapper.class);
property = new ProviderConfigProperty(); property = new ProviderConfigProperty();
@ -96,13 +107,25 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); String attributeName = getAttributeName(mappingModel, userSession.getRealm());
boolean aggregateAttrs = Boolean.valueOf(mappingModel.getConfig().get(ProtocolMapperUtils.AGGREGATE_ATTRS)); boolean aggregateAttrs = Boolean.valueOf(mappingModel.getConfig().get(ProtocolMapperUtils.AGGREGATE_ATTRS));
Collection<String> attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName, aggregateAttrs); Collection<String> attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName, aggregateAttrs);
if (attributeValue == null) return; if (attributeValue == null) return;
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue); OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue);
} }
private String getAttributeName(ProtocolMapperModel mappingModel, RealmModel realm) {
Map<String, String> config = mappingModel.getConfig();
String name = config.get(ProtocolMapperUtils.USER_ATTRIBUTE);
if (realm.getAttribute(DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED, false)) {
// defaults to the default config property for backward compatibility
return config.getOrDefault(ProtocolMapperUtils.USER_PROFILE_ATTRIBUTE, name);
}
return name;
}
public static ProtocolMapperModel createClaimMapper(String name, public static ProtocolMapperModel createClaimMapper(String name,
String userAttribute, String userAttribute,
String tokenClaimName, String claimType, String tokenClaimName, String claimType,
@ -111,12 +134,23 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
accessToken, idToken, multivalued, false); accessToken, idToken, multivalued, false);
} }
public static ProtocolMapperModel createClaimMapper(String name,
String userAttribute,
String tokenClaimName, String claimType,
boolean accessToken, boolean idToken,
boolean multivalued, boolean aggregateAttrs) {
return createClaimMapper(name, userAttribute, null, tokenClaimName, claimType,
accessToken, idToken, multivalued, aggregateAttrs);
}
public static ProtocolMapperModel createClaimMapper(String name, public static ProtocolMapperModel createClaimMapper(String name,
String userAttribute, String userAttribute,
String userProfileAttribute,
String tokenClaimName, String claimType, String tokenClaimName, String claimType,
boolean accessToken, boolean idToken, boolean accessToken, boolean idToken,
boolean multivalued, boolean aggregateAttrs) { boolean multivalued, boolean aggregateAttrs) {
ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute, ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute,
userProfileAttribute,
tokenClaimName, claimType, tokenClaimName, claimType,
accessToken, idToken, accessToken, idToken,
PROVIDER_ID); PROVIDER_ID);

View file

@ -103,6 +103,7 @@ import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -288,10 +289,11 @@ public class UserResource {
if (rep.getAttributes() != null) { if (rep.getAttributes() != null) {
Map<String, List<String>> allowedAttributes = profile.getAttributes().getReadable(false); Map<String, List<String>> allowedAttributes = profile.getAttributes().getReadable(false);
Iterator<String> iterator = rep.getAttributes().keySet().iterator();
for (String attributeName : rep.getAttributes().keySet()) { while (iterator.hasNext()) {
if (!allowedAttributes.containsKey(attributeName)) { if (!allowedAttributes.containsKey(iterator.next())) {
rep.getAttributes().remove(attributeName); iterator.remove();
} }
} }
} }

View file

@ -1218,7 +1218,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
return rep; return rep;
} }
private OAuthClient.AccessTokenResponse browserLogin(String clientSecret, String username, String password) { protected OAuthClient.AccessTokenResponse browserLogin(String clientSecret, String username, String password) {
OAuthClient.AuthorizationEndpointResponse authzEndpointResponse = oauth.doLogin(username, password); OAuthClient.AuthorizationEndpointResponse authzEndpointResponse = oauth.doLogin(username, password);
return oauth.doAccessTokenRequest(authzEndpointResponse.getCode(), clientSecret); return oauth.doAccessTokenRequest(authzEndpointResponse.getCode(), clientSecret);
} }

View file

@ -0,0 +1,139 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createAddressMapper;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createClaimMapper;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedRole;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createScriptMapper;
import static org.keycloak.userprofile.DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED;
import javax.ws.rs.core.Response;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientScopeResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
import org.keycloak.common.util.UriUtils;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AccountRoles;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.oidc.mappers.AddressMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AddressClaimSet;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.ProtocolMappersUpdater;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ProtocolMapperUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCProtocolMappersUserProfileTest extends OIDCProtocolMappersTest {
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
super.addTestRealms(testRealms);
final RealmRepresentation testRealm = testRealms.get(0);
if (testRealm.getAttributes() == null) {
testRealm.setAttributes(new HashMap<>());
}
testRealm.getAttributes().put(REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString());
}
@Test
public void testMappingFromAttribute() {
ClientResource app = findClientResourceByClientId(adminClient.realm("test"), "test-app");
app.getProtocolMappers().createMapper(createClaimMapper("user profile attribute precedence", "lastName", "firstName", "c_fn", "String", true, true, false)).close();
OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
Object firstName = idToken.getOtherClaims().get("c_fn");
assertThat(firstName, instanceOf(String.class));
assertThat(firstName, is("Tom"));
oauth.openLogout();
}
@Test
public void testFallbackToDefaultConfigProperty() {
ClientResource app = findClientResourceByClientId(adminClient.realm("test"), "test-app");
app.getProtocolMappers().createMapper(createClaimMapper("user profile default config property", "lastName", null, "c_fn_from_default", "String", true, true, false)).close();
OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
Object firstName = idToken.getOtherClaims().get("c_fn_from_default");
assertThat(firstName, instanceOf(String.class));
assertThat(firstName, is("Brady"));
oauth.openLogout();
}
}

View file

@ -91,6 +91,15 @@ public class ProtocolMapperUtil {
} }
public static ProtocolMapperRepresentation createClaimMapper(String name,
String userAttribute, String userProfileAttribute,
String tokenClaimName, String claimType,
boolean accessToken, boolean idToken, boolean multivalued) {
return ModelToRepresentation.toRepresentation(UserAttributeMapper.createClaimMapper(name, userAttribute, userProfileAttribute, tokenClaimName,
claimType, accessToken, idToken, multivalued, false));
}
public static ProtocolMapperRepresentation createClaimMapper(String name, public static ProtocolMapperRepresentation createClaimMapper(String name,
String userAttribute, String userAttribute,
String tokenClaimName, String claimType, String tokenClaimName, String claimType,

View file

@ -3048,8 +3048,9 @@ module.controller('RoleSelectorModalCtrl', function($scope, realm, config, confi
}) })
}); });
module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, ComponentUtils, Client) { module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, ComponentUtils, Client, UserProfile, Current) {
clientSelectControl($scope, $route.current.params.realm, Client); clientSelectControl($scope, $route.current.params.realm, Client);
userProfileAttributeSelectControl($scope, $route.current.params.realm, UserProfile);
$scope.fileNames = {}; $scope.fileNames = {};
$scope.newMapEntries = {}; $scope.newMapEntries = {};
var cachedMaps = {}; var cachedMaps = {};
@ -3073,6 +3074,38 @@ module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, Compon
} }
} }
$scope.initSelectedUserProfileAttributes = function(configName, config) {
$scope.selectedUserAttribute = {};
if(config[configName]) {
UserProfile.get({realm: $route.current.params.realm}, function(data) {
if (!data.attributes) {
$scope.userProfileDisabled = true;
return;
}
for (var i = 0; i < data.attributes.length; i++) {
if (data.attributes[i].name == config[configName]) {
$scope.selectedUserAttribute = data.attributes[i];
$scope.selectedUserAttribute.text = data.attributes[i].name;
}
}
});
}
}
$scope.isPropertyDisabled = function(configName) {
var userProfileEnabled = Current.realm.attributes['userProfileEnabled'] == 'true';
if (configName == 'user.profile.attribute' && !userProfileEnabled) {
return true;
}
if (configName == 'user.attribute' && userProfileEnabled) {
return true;
}
return false;
}
$scope.openRoleSelector = function (configName, config) { $scope.openRoleSelector = function (configName, config) {
$modal.open({ $modal.open({
templateUrl: resourceUrl + '/partials/modal/role-selector.html', templateUrl: resourceUrl + '/partials/modal/role-selector.html',
@ -3105,6 +3138,18 @@ module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, Compon
} }
}; };
$scope.changeUserAttribute = function(configName, config, userAttribute, multivalued) {
if (!$scope.selectedUserAttribute) {
return;
}
$scope.selectedUserAttribute = userAttribute;
if (multivalued) {
config[configName][0] = userAttribute.name;
} else {
config[configName] = userAttribute.name;
}
};
ComponentUtils.convertAllMultivaluedStringValuesToList($scope.properties, $scope.config); ComponentUtils.convertAllMultivaluedStringValuesToList($scope.properties, $scope.config);
ComponentUtils.addLastEmptyValueToMultivaluedLists($scope.properties, $scope.config); ComponentUtils.addLastEmptyValueToMultivaluedLists($scope.properties, $scope.config);

View file

@ -962,6 +962,39 @@ function clientSelectControl($scope, realm, Client) {
}; };
} }
function userProfileAttributeSelectControl($scope, realm, UserProfile) {
$scope.userProfileAttributesUiSelect = {
minimumInputLength: 0,
delay: 500,
allowClear: true,
id: function(e) { return e.name; },
query: function (query) {
var data = {results: []};
UserProfile.get({realm: realm}, function(config) {
var attributes = [];
if ('' == query.term.trim()) {
attributes = config.attributes;
} else {
for (var i = 0; i < config.attributes.length; i++) {
if (config.attributes[i].name.indexOf(query.term.trim()) != -1) {
attributes.push(config.attributes[i]);
}
}
}
query.callback({results: attributes});
});
},
formatResult: function(object, container, query) {
object.text = object.name;
return object.name;
},
formatSelection: function(object, container, query) {
return object.name;
}
};
}
function roleControl($scope, $route, realm, role, roles, Client, function roleControl($scope, $route, realm, role, roles, Client,
ClientRole, RoleById, RoleRealmComposites, RoleClientComposites, ClientRole, RoleById, RoleRealmComposites, RoleClientComposites,
$http, $location, Notifications, Dialog, ComponentUtils) { $http, $location, Notifications, Dialog, ComponentUtils) {

View file

@ -1,5 +1,6 @@
<div> <div>
<div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl"> <div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl"
ng-if="!isPropertyDisabled(option.name)">
<label class="col-md-2 control-label">{{:: option.label | translate}}</label> <label class="col-md-2 control-label">{{:: option.label | translate}}</label>
<div class="col-md-6" data-ng-if="option.type == 'String'"> <div class="col-md-6" data-ng-if="option.type == 'String'">
@ -33,6 +34,10 @@
<input type="hidden" ui-select2="clientsUiSelect" id="clients" data-ng-init="initSelectedClient(option.name, config)" data-ng-model="selectedClient" data-ng-change="changeClient(option.name, config, selectedClient, false);" data-placeholder="{{:: 'selectOne' | translate}}..."> <input type="hidden" ui-select2="clientsUiSelect" id="clients" data-ng-init="initSelectedClient(option.name, config)" data-ng-model="selectedClient" data-ng-change="changeClient(option.name, config, selectedClient, false);" data-placeholder="{{:: 'selectOne' | translate}}...">
</input> </input>
</div> </div>
<div class="col-md-4" data-ng-if="option.type == 'UserProfileAttributeList'">
<input type="hidden" ui-select2="userProfileAttributesUiSelect" id="userProfileAttributes" data-ng-init="initSelectedUserProfileAttributes(option.name, config)" data-ng-model="selectedUserAttribute" data-ng-change="changeUserAttribute(option.name, config, selectedUserAttribute, false);" data-placeholder="{{:: 'selectOne' | translate}}...">
</input>
</div>
<div class="col-md-6" data-ng-if="option.type == 'Script'"> <div class="col-md-6" data-ng-if="option.type == 'Script'">
<div ng-model="config[option.name]" placeholder="Enter your script..." ui-ace="{ onLoad : initEditor, useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}"> <div ng-model="config[option.name]" placeholder="Enter your script..." ui-ace="{ onLoad : initEditor, useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">