This commit is contained in:
Bill Burke 2015-07-22 14:20:52 -04:00
commit 7dc05a45ac
107 changed files with 1616 additions and 375 deletions

View file

@ -58,6 +58,11 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
return null;
}
@Override
public Response performLogin(AuthenticationRequest request) {
return null;
}
@Override
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
return null;

View file

@ -8,6 +8,12 @@
<delete tableName="CLIENT_SESSION"/>
<delete tableName="USER_SESSION_NOTE"/>
<delete tableName="USER_SESSION"/>
<addColumn tableName="CLIENT">
<column name="SERVICE_ACCOUNTS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
<addColumn tableName="CLIENT_SESSION">
<column name="CURRENT_ACTION" type="VARCHAR(36)">
<constraints nullable="true"/>

View file

@ -29,6 +29,8 @@ public interface OAuth2Constants {
String PASSWORD = "password";
String CLIENT_CREDENTIALS = "client_credentials";
}

View file

@ -0,0 +1,20 @@
package org.keycloak.constants;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ServiceAccountConstants {
String CLIENT_AUTH = "client_auth";
String SERVICE_ACCOUNT_USER_PREFIX = "service-account-";
String SERVICE_ACCOUNT_CLIENT_ATTRIBUTE = "serviceAccountClient";
String CLIENT_ID_PROTOCOL_MAPPER = "Client ID";
String CLIENT_HOST_PROTOCOL_MAPPER = "Client Host";
String CLIENT_ADDRESS_PROTOCOL_MAPPER = "Client IP Address";
String CLIENT_ID = "clientId";
String CLIENT_HOST = "clientHost";
String CLIENT_ADDRESS = "clientAddress";
}

View file

@ -22,6 +22,7 @@ public class ClientRepresentation {
protected Integer notBefore;
protected Boolean bearerOnly;
protected Boolean consentRequired;
protected Boolean serviceAccountsEnabled;
protected Boolean directGrantsOnly;
protected Boolean publicClient;
protected Boolean frontchannelLogout;
@ -144,6 +145,14 @@ public class ClientRepresentation {
this.consentRequired = consentRequired;
}
public Boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
public Boolean isDirectGrantsOnly() {
return directGrantsOnly;
}

View file

@ -35,4 +35,10 @@ public interface Details {
String IMPERSONATOR_REALM = "impersonator_realm";
String IMPERSONATOR = "impersonator";
String CLIENT_AUTH_METHOD = "client_auth_method";
String CLIENT_AUTH_METHOD_VALUE_CLIENT_CREDENTIALS = "client_credentials";
String CLIENT_AUTH_METHOD_VALUE_CERTIFICATE = "client_certificate";
String CLIENT_AUTH_METHOD_VALUE_KERBEROS_KEYTAB = "kerberos_keytab";
String CLIENT_AUTH_METHOD_VALUE_SIGNED_JWT = "signed_jwt";
}

View file

@ -15,6 +15,9 @@ public enum EventType {
CODE_TO_TOKEN(true),
CODE_TO_TOKEN_ERROR(true),
CLIENT_LOGIN(true),
CLIENT_LOGIN_ERROR(true),
REFRESH_TOKEN(false),
REFRESH_TOKEN_ERROR(false),
VALIDATE_ACCESS_TOKEN(false),

View file

@ -210,6 +210,14 @@ An pure HTML5/Javascript example using Keycloak to secure it.
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
Step 10: Service Account Example
================================
An example for retrieve service account dedicated to the Client Application itself (not to any user).
[http://localhost:8080/service-account-portal](http://localhost:8080/service-account-portal)
Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed)
Admin Console
==========================

View file

@ -36,6 +36,7 @@
<module>database-service</module>
<module>third-party</module>
<module>third-party-cdi</module>
<module>service-account</module>
</modules>
<profiles>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.4.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak.example.demo</groupId>
<artifactId>service-account-example</artifactId>
<packaging>war</packaging>
<name>Service Account Example App</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>service-account-portal</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,234 @@
package org.keycloak.example;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ProductServiceAccountServlet extends HttpServlet {
public static final String ERROR = "error";
public static final String TOKEN = "token";
public static final String TOKEN_PARSED = "idTokenParsed";
public static final String REFRESH_TOKEN = "refreshToken";
public static final String PRODUCTS = "products";
@Override
public void init() throws ServletException {
InputStream config = getServletContext().getResourceAsStream("WEB-INF/keycloak.json");
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
HttpClient client = new DefaultHttpClient();
getServletContext().setAttribute(KeycloakDeployment.class.getName(), deployment);
getServletContext().setAttribute(HttpClient.class.getName(), client);
}
@Override
public void destroy() {
getHttpClient().getConnectionManager().shutdown();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String reqUri = req.getRequestURI();
if (reqUri.endsWith("/login")) {
serviceAccountLogin(req);
} else if (reqUri.endsWith("/refresh")) {
refreshToken(req);
} else if (reqUri.endsWith("/logout")){
logout(req);
}
// Don't load products if some error happened during login,refresh or logout
if (req.getAttribute(ERROR) == null) {
loadProducts(req);
}
req.getRequestDispatcher("/WEB-INF/page.jsp").forward(req, resp);
}
private void serviceAccountLogin(HttpServletRequest req) {
KeycloakDeployment deployment = getKeycloakDeployment();
HttpClient client = getHttpClient();
String clientId = deployment.getResourceName();
String clientSecret = deployment.getResourceCredentials().get("secret");
try {
HttpPost post = new HttpPost(deployment.getTokenUrl());
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
String authHeader = BasicAuthHelper.createHeader(clientId, clientSecret);
post.addHeader("Authorization", authHeader);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpResponse response = client.execute(post);
int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (status != 200) {
String json = getContent(entity);
String error = "Service account login failed. Bad status: " + status + " response: " + json;
req.setAttribute(ERROR, error);
} else if (entity == null) {
req.setAttribute(ERROR, "No entity");
} else {
String json = getContent(entity);
AccessTokenResponse tokenResp = JsonSerialization.readValue(json, AccessTokenResponse.class);
setTokens(req, deployment, tokenResp);
}
} catch (IOException ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Service account login failed. IOException occured. See server.log for details. Message is: " + ioe.getMessage());
} catch (VerificationException vfe) {
req.setAttribute(ERROR, "Service account login failed. Failed to verify token Message is: " + vfe.getMessage());
}
}
private void setTokens(HttpServletRequest req, KeycloakDeployment deployment, AccessTokenResponse tokenResponse) throws IOException, VerificationException {
String token = tokenResponse.getToken();
String refreshToken = tokenResponse.getRefreshToken();
AccessToken tokenParsed = RSATokenVerifier.verifyToken(token, deployment.getRealmKey(), deployment.getRealmInfoUrl());
req.getSession().setAttribute(TOKEN, token);
req.getSession().setAttribute(REFRESH_TOKEN, refreshToken);
req.getSession().setAttribute(TOKEN_PARSED, tokenParsed);
}
private void loadProducts(HttpServletRequest req) {
HttpClient client = getHttpClient();
String token = (String) req.getSession().getAttribute(TOKEN);
HttpGet get = new HttpGet("http://localhost:8080/database/products");
if (token != null) {
get.addHeader("Authorization", "Bearer " + token);
}
try {
HttpResponse response = client.execute(get);
HttpEntity entity = response.getEntity();
int status = response.getStatusLine().getStatusCode();
if (status != 200) {
String json = getContent(entity);
String error = "Failed retrieve products.";
if (status == 401) {
error = error + " You need to login first with the service account.";
} else if (status == 403) {
error = error + " Maybe service account user doesn't have needed role? Assign role 'user' in Keycloak admin console to user '" +
ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + getKeycloakDeployment().getResourceName() + "' and then logout and login again.";
}
error = error + " Status: " + status + ", Response: " + json;
req.setAttribute(ERROR, error);
} else if (entity == null) {
req.setAttribute(ERROR, "No entity");
} else {
String products = getContent(entity);
req.setAttribute(PRODUCTS, products);
}
} catch (IOException ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Failed retrieve products. IOException occured. See server.log for details. Message is: " + ioe.getMessage());
}
}
private void refreshToken(HttpServletRequest req) {
KeycloakDeployment deployment = getKeycloakDeployment();
String refreshToken = (String) req.getSession().getAttribute(REFRESH_TOKEN);
if (refreshToken == null) {
req.setAttribute(ERROR, "No refresh token available. Please login first");
} else {
try {
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
setTokens(req, deployment, tokenResponse);
} catch (ServerRequest.HttpFailure hfe) {
hfe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Status was: " + hfe.getStatus() + ", Error is: " + hfe.getError());
} catch (Exception ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Message is: " + ioe.getMessage());
}
}
}
private void logout(HttpServletRequest req) {
KeycloakDeployment deployment = getKeycloakDeployment();
String refreshToken = (String) req.getSession().getAttribute(REFRESH_TOKEN);
if (refreshToken == null) {
req.setAttribute(ERROR, "No refresh token available. Please login first");
} else {
try {
ServerRequest.invokeLogout(deployment, refreshToken);
req.getSession().removeAttribute(TOKEN);
req.getSession().removeAttribute(REFRESH_TOKEN);
req.getSession().removeAttribute(TOKEN_PARSED);
} catch (IOException ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Message is: " + ioe.getMessage());
} catch (ServerRequest.HttpFailure hfe) {
hfe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Status was: " + hfe.getStatus() + ", Error is: " + hfe.getError());
}
}
}
private String getContent(HttpEntity entity) throws IOException {
if (entity == null) return null;
InputStream is = entity.getContent();
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
int c;
while ((c = is.read()) != -1) {
os.write(c);
}
byte[] bytes = os.toByteArray();
String data = new String(bytes);
return data;
} finally {
try {
is.close();
} catch (IOException ignored) {
}
}
}
private KeycloakDeployment getKeycloakDeployment() {
return (KeycloakDeployment) getServletContext().getAttribute(KeycloakDeployment.class.getName());
}
private HttpClient getHttpClient() {
return (HttpClient) getServletContext().getAttribute(HttpClient.class.getName());
}
}

View file

@ -0,0 +1,9 @@
<jboss-deployment-structure>
<deployment>
<dependencies>
<!-- the Demo code uses classes in these modules. These are optional to import if you are not using
Apache Http Client or the HttpClientBuilder that comes with the adapter core -->
<module name="org.apache.httpcomponents"/>
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -0,0 +1,10 @@
{
"realm" : "demo",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"resource" : "product-sa-client",
"credentials": {
"secret": "password"
}
}

View file

@ -0,0 +1,52 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1" %>
<%@ page import="org.keycloak.example.ProductServiceAccountServlet" %>
<%@ page import="org.keycloak.representations.AccessToken" %>
<%@ page import="org.keycloak.constants.ServiceAccountConstants" %>
<%@ page import="org.keycloak.util.Time" %>
<html>
<head>
<title>Service account portal</title>
</head>
<body bgcolor="#FFFFFF">
<%
AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED);
String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS);
String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR);
%>
<h1>Service account portal</h1>
<p><a href="/service-account-portal/app/login">Login</a> | <a href="/service-account-portal/app/refresh">Refresh token</a> | <a
href="/service-account-portal/app/logout">Logout</a></p>
<hr />
<% if (appError != null) { %>
<p><font color="red">
<b>Error: </b> <%= appError %>
</font></p>
<hr />
<% } %>
<% if (token != null) { %>
<p>
<b>Service account available</b><br />
Client ID: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID) %><br />
Client hostname: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_HOST) %><br />
Client address: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ADDRESS) %><br />
Token expiration: <%= Time.toDate(token.getExpiration()) %><br />
<% if (token.isExpired()) { %>
<font color="red">Access token is expired. You may need to refresh</font><br />
<% } %>
</p>
<hr />
<% } %>
<% if (products != null) { %>
<p>
<b>Products retrieved successfully from REST endpoint</b><br />
Product list: <%= products %>
</p>
<hr />
<% } %>
</body>
</html>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>service-account-portal</module-name>
<servlet>
<servlet-name>ServiceAccountExample</servlet-name>
<servlet-class>org.keycloak.example.ProductServiceAccountServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServiceAccountExample</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>

View file

@ -0,0 +1,5 @@
<html>
<head>
<meta http-equiv="Refresh" content="0; URL=app">
</head>
</html>

View file

@ -162,6 +162,12 @@
"publicClient": true,
"directGrantsOnly": true,
"consentRequired": true
},
{
"clientId": "product-sa-client",
"enabled": true,
"secret": "password",
"serviceAccountsEnabled": true
}
],
"clientScopeMappings": {

View file

@ -762,6 +762,18 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ClientInstallationCtrl'
})
.when('/realms/:realm/clients/:client/service-accounts', {
templateUrl : resourceUrl + '/partials/client-service-accounts.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
client : function(ClientLoader) {
return ClientLoader();
}
},
controller : 'ClientServiceAccountsCtrl'
})
.when('/create/client/:realm', {
templateUrl : resourceUrl + '/partials/client-detail.html',
resolve : {
@ -1594,6 +1606,24 @@ module.directive('kcNavigationUser', function () {
}
});
module.directive('kcTabsIdentityProvider', function () {
return {
scope: true,
restrict: 'E',
replace: true,
templateUrl: resourceUrl + '/templates/kc-tabs-identity-provider.html'
}
});
module.directive('kcTabsUserFederation', function () {
return {
scope: true,
restrict: 'E',
replace: true,
templateUrl: resourceUrl + '/templates/kc-tabs-user-federation.html'
}
});
module.controller('RoleSelectorModalCtrl', function($scope, realm, config, configName, RealmRoles, Client, ClientRole, $modalInstance) {
console.log('realm: ' + realm.realm);
$scope.selectedRealmRole = {
@ -1812,4 +1842,20 @@ module.directive('kcTooltip', function($compile) {
$compile(label)(scope);
}
};
});
module.directive( 'kcOpen', function ( $location ) {
return function ( scope, element, attrs ) {
var path;
attrs.$observe( 'kcOpen', function (val) {
path = val;
});
element.bind( 'click', function () {
scope.$apply( function () {
$location.path(path);
});
});
};
});

View file

@ -4,6 +4,20 @@ Array.prototype.remove = function(from, to) {
return this.push.apply(this, rest);
};
module.controller('ClientTabCtrl', function(Dialog, $scope, Current, Notifications, $location) {
$scope.removeClient = function() {
Dialog.confirmDelete($scope.client.clientId, 'client', function() {
$scope.client.$remove({
realm : Current.realm.realm,
client : $scope.client.id
}, function() {
$location.url("/realms/" + Current.realm.realm + "/clients");
Notifications.success("The client has been deleted.");
});
});
};
});
module.controller('ClientRoleListCtrl', function($scope, $location, realm, client, roles) {
$scope.realm = realm;
$scope.roles = roles;
@ -834,20 +848,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
$scope.cancel = function() {
$location.url("/realms/" + realm.realm + "/clients");
};
$scope.remove = function() {
Dialog.confirmDelete($scope.client.clientId, 'client', function() {
$scope.client.$remove({
realm : realm.realm,
client : $scope.client.id
}, function() {
$location.url("/realms/" + realm.realm + "/clients");
Notifications.success("The client has been deleted.");
});
});
};
});
module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, client, clients, Notifications,
@ -1298,6 +1298,25 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv
});
module.controller('ClientServiceAccountsCtrl', function($scope, $http, realm, client, Notifications, Client) {
$scope.realm = realm;
$scope.client = angular.copy(client);
$scope.serviceAccountsEnabledChanged = function() {
if (client.serviceAccountsEnabled != $scope.client.serviceAccountsEnabled) {
Client.update({
realm : realm.realm,
client : client.id
}, $scope.client, function() {
$scope.changed = false;
client = angular.copy($scope.client);
Notifications.success("Service Account settings updated.");
});
}
}
});

View file

@ -83,7 +83,7 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, WhoAmI, Current, $
get impersonation() {
return getAccess('impersonation');
}
}
};
$scope.$watch(function() {
return $location.path();
@ -113,6 +113,18 @@ module.controller('HomeCtrl', function(Realm, Auth, $location) {
});
});
module.controller('RealmTabCtrl', function(Dialog, $scope, Current, Realm, Notifications, $location) {
$scope.removeRealm = function() {
Dialog.confirmDelete(Current.realm.realm, 'realm', function() {
Realm.remove({ id : Current.realm.realm }, function() {
Current.realms = Realm.query();
Notifications.success("The realm has been deleted.");
$location.url("/");
});
});
};
});
module.controller('RealmListCtrl', function($scope, Realm, Current) {
$scope.realms = Realm.query();
Current.realms = $scope.realms;
@ -286,16 +298,6 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser
$scope.cancel = function() {
window.history.back();
};
$scope.remove = function() {
Dialog.confirmDelete($scope.realm.realm, 'realm', function() {
Realm.remove({ id : $scope.realm.realm }, function() {
Current.realms = Realm.query();
Notifications.success("The realm has been deleted.");
$location.url("/");
});
});
};
});
function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, url) {
@ -593,6 +595,22 @@ module.controller('RealmDefaultRolesCtrl', function ($scope, Realm, realm, clien
});
module.controller('IdentityProviderTabCtrl', function(Dialog, $scope, Current, Notifications, $location) {
$scope.removeIdentityProvider = function() {
Dialog.confirmDelete($scope.identityProvider.alias, 'provider', function() {
$scope.identityProvider.$remove({
realm : Current.realm.realm,
alias : $scope.identityProvider.alias
}, function() {
$location.url("/realms/" + Current.realm.realm + "/identity-provider-settings");
Notifications.success("The identity provider has been deleted.");
});
});
};
});
module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, $route, realm, instance, providerFactory, IdentityProvider, serverInfo, $location, Notifications, Dialog) {
console.log('RealmIdentityProviderCtrl');
@ -802,18 +820,6 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
$location.url("/create/identity-provider/" + realm.realm + "/" + provider.id);
};
$scope.remove = function() {
Dialog.confirmDelete($scope.identityProvider.alias, 'provider', function() {
$scope.identityProvider.$remove({
realm : realm.realm,
alias : $scope.identityProvider.alias
}, function() {
$location.url("/realms/" + realm.realm + "/identity-provider-settings");
Notifications.success("The client has been deleted.");
});
});
};
$scope.save = function() {
if ($scope.newIdentityProvider) {
if (!$scope.identityProvider.alias) {

View file

@ -273,6 +273,21 @@ module.controller('UserListCtrl', function($scope, realm, User, UserImpersonatio
});
module.controller('UserTabCtrl', function($scope, $location, Dialog, Notifications, Current) {
$scope.removeUser = function() {
Dialog.confirmDelete($scope.user.id, 'user', function() {
$scope.user.$remove({
realm : Current.realm.realm,
userId : $scope.user.id
}, function() {
$location.url("/realms/" + Current.realm.realm + "/users");
Notifications.success("The user has been deleted.");
}, function() {
Notifications.error("User couldn't be deleted");
});
});
};
});
module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User, UserFederationInstances, UserImpersonation, RequiredActions, $location, Dialog, Notifications) {
$scope.realm = realm;
@ -420,20 +435,6 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
$scope.cancel = function() {
$location.url("/realms/" + realm.realm + "/users");
};
$scope.remove = function() {
Dialog.confirmDelete($scope.user.id, 'user', function() {
$scope.user.$remove({
realm : realm.realm,
userId : $scope.user.id
}, function() {
$location.url("/realms/" + realm.realm + "/users");
Notifications.success("The user has been deleted.");
}, function() {
Notifications.error("User couldn't be deleted");
});
});
};
});
module.controller('UserCredentialsCtrl', function($scope, realm, user, User, UserCredentials, Notifications, Dialog) {
@ -544,11 +545,27 @@ module.controller('UserFederationCtrl', function($scope, $location, realm, UserF
});
module.controller('UserFederationTabCtrl', function(Dialog, $scope, Current, Notifications, $location) {
$scope.removeUserFederation = function() {
Dialog.confirm('Delete', 'Are you sure you want to permanently delete this provider? All imported users will also be deleted.', function() {
$scope.instance.$remove({
realm : Current.realm.realm,
instance : $scope.instance.id
}, function() {
$location.url("/realms/" + Current.realm.realm + "/user-federation");
Notifications.success("The provider has been deleted.");
});
});
};
});
module.controller('GenericUserFederationCtrl', function($scope, $location, Notifications, $route, Dialog, realm, instance, providerFactory, UserFederationInstances, UserFederationSync) {
console.log('GenericUserFederationCtrl');
$scope.create = !instance.providerName;
$scope.providerFactory = providerFactory;
$scope.provider = instance;
console.log("providerFactory: " + providerFactory.id);
@ -644,18 +661,6 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
}
};
$scope.remove = function() {
Dialog.confirm('Delete', 'Are you sure you want to permanently delete this provider? All imported users will also be deleted.', function() {
$scope.instance.$remove({
realm : realm.realm,
instance : $scope.instance.id
}, function() {
$location.url("/realms/" + realm.realm + "/user-federation");
Notifications.success("The provider has been deleted.");
});
});
};
$scope.triggerFullSync = function() {
console.log('GenericCtrl: triggerFullSync');
triggerSync('triggerFullSync');
@ -906,6 +911,7 @@ module.controller('UserFederationMapperListCtrl', function($scope, $location, No
$scope.realm = realm;
$scope.provider = provider;
$scope.instance = provider;
$scope.mapperTypes = mapperTypes;
$scope.mappers = mappers;

View file

@ -38,8 +38,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -1,6 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Settings</h1>
<kc-tabs-realm></kc-tabs-realm>
<ul class="nav nav-tabs nav-tabs-pf">
@ -98,8 +96,8 @@
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -25,7 +25,7 @@
</fieldset>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button data-kc-save data-ng-show="create">Save</button>
<button data-kc-save data-ng-show="create">Save</button>
</div>
</div>
</form>

View file

@ -5,8 +5,6 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<form class="form-horizontal" name="clusteringForm" novalidate kc-read-only="!access.manageClients">
@ -34,8 +32,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button data-kc-save data-ng-show="changed">Save</button>
<button data-kc-reset data-ng-show="changed">Cancel</button>
<button data-kc-save data-ng-disabled="!changed">Save</button>
<button data-kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</fieldset>

View file

@ -5,8 +5,6 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients">

View file

@ -6,10 +6,6 @@
<li data-ng-hide="create">{{client.clientId}}</li>
</ol>
<h1 data-ng-show="create">Add Client</h1>
<h1 data-ng-hide="create">{{client.clientId|capitalize}}<i id="removeClient" class="pficon pficon-delete clickable" data-ng-show="!create && access.manageClients"
data-ng-hide="changed" data-ng-click="remove()"></i></h1>
<kc-tabs-client></kc-tabs-client>
<form class="form-horizontal" name="clientForm" novalidate kc-read-only="!access.manageClients">
@ -274,12 +270,12 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageClients">
<button kc-save data-ng-show="changed">Save</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-cancel data-ng-click="cancel()">Cancel</button>
</div>
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -5,8 +5,6 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<form class="form" name="realmForm" novalidate>

View file

@ -5,8 +5,6 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageRealm">

View file

@ -7,7 +7,7 @@
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="3">
<th class="kc-table-actions" colspan="4">
<div class="form-inline">
<div class="form-group">
<div class="input-group">
@ -29,6 +29,7 @@
<th>Client ID</th>
<th>Enabled</th>
<th>Base URL</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -38,6 +39,10 @@
<td ng-class="{'text-muted': !client.baseUrl}">
<a href="{{client.baseUrl}}" target="_blank" data-ng-show="client.baseUrl">{{client.baseUrl}}</a>
<span data-ng-hide="client.baseUrl">Not defined</span>
</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/clients/{{client.id}}">Edit</button>
</td>
</tr>
<tr data-ng-show="(clients | filter:search).length == 0">
<td class="text-muted" colspan="3" data-ng-show="search.clientId">No results</td>

View file

@ -22,10 +22,6 @@
</div>
</div>
</div>
<div class="pull-right" data-ng-show="access.manageRealm">
<button class="btn btn-primary" data-ng-click="add()">Add Selected</button>
</div>
</div>
</th>
</tr>
@ -48,6 +44,10 @@
</tr>
</tbody>
</table>
<div data-ng-show="access.manageRealm">
<button class="btn btn-primary" data-ng-click="add()">Add Selected</button>
</div>
</div>
<kc-menu></kc-menu>

View file

@ -5,8 +5,6 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<table class="table table-striped table-bordered">

View file

@ -5,8 +5,6 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageRealm">

View file

@ -43,14 +43,14 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageClients">
<button kc-save data-ng-show="changed">Save</button>
<button kc-save>Save</button>
<button kc-cancel data-ng-click="cancel()">Cancel</button>
</div>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>

View file

@ -5,14 +5,12 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="3" data-ng-show="access.manageClients">
<th class="kc-table-actions" colspan="4" data-ng-show="access.manageClients">
<div class="pull-right">
<a class="btn btn-default" href="#/create/role/{{realm.realm}}/clients/{{client.id}}">Add Role</a>
</div>
@ -22,6 +20,7 @@
<th>Role Name</th>
<th>Composite</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -29,6 +28,9 @@
<td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}">{{role.name}}</a></td>
<td>{{role.composite}}</td>
<td>{{role.description}}</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}">Edit</button>
</td>
</tr>
<tr data-ng-show="!roles || roles.length == 0">
<td>No client roles available</td>

View file

@ -5,8 +5,6 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<h2><span>{{client.clientId}}</span> Scope Mappings </h2>

View file

@ -0,0 +1,28 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<h2><span>{{client.clientId}}</span> Service Accounts </h2>
<p class="subtitle"></p>
<form class="form-horizontal" name="serviceAccountsEnabledForm" novalidate kc-read-only="!access.manageClients">
<fieldset class="border-top">
<div class="form-group">
<label class="col-md-2 control-label" for="serviceAccountsEnabled">Service Accounts Enabled</label>
<kc-tooltip>Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client.</kc-tooltip>
<div class="col-md-6">
<input ng-model="client.serviceAccountsEnabled" ng-click="serviceAccountsEnabledChanged()" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch />
</div>
</div>
</fieldset>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -5,8 +5,6 @@
<li>{{client.clientId}}</li>
</ol>
<h1>{{client.clientId|capitalize}}</h1>
<kc-tabs-client></kc-tabs-client>
<form class="form-horizontal" name="sessionStats">

View file

@ -1,6 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Settings</h1>
<kc-tabs-realm></kc-tabs-realm>
<ul class="nav nav-tabs nav-tabs-pf">
@ -27,8 +25,8 @@
</fieldset>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -5,14 +5,7 @@
<li data-ng-show="create">Add User Federation Provider</li>
</ol>
<h1 data-ng-hide="create">{{instance.providerName|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers"
data-ng-hide="changed" data-ng-click="remove()"></i></h1>
<h1 data-ng-show="create">Add {{instance.providerName|capitalize}} User Federation Provide</h1>
<ul class="nav nav-tabs" data-ng-hide="create">
<li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
<li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
</ul>
<kc-tabs-user-federation></kc-tabs-user-federation>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset>
@ -87,8 +80,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
<button class="btn btn-primary" data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed">Synchronize changed users</button>
<button class="btn btn-primary" data-ng-click="triggerFullSync()" data-ng-hide="changed">Synchronize all users</button>
</div>

View file

@ -5,14 +5,7 @@
<li data-ng-show="create">Add User Federation Provider</li>
</ol>
<h1 data-ng-hide="create">Kerberos<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers"
data-ng-hide="changed" data-ng-click="remove()"></i></h1>
<h1 data-ng-show="create">Add Kerberos User Federation Provider</h1>
<ul class="nav nav-tabs" data-ng-hide="create">
<li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
<li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
</ul>
<kc-tabs-user-federation></kc-tabs-user-federation>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset>
@ -106,8 +99,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -5,14 +5,7 @@
<li data-ng-show="create">Add User Federation Provider</li>
</ol>
<h1 data-ng-hide="create">LDAP<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers"
data-ng-hide="changed" data-ng-click="remove()"></i></h1>
<h1 data-ng-show="create">Add LDAP User Federation Provider</h1>
<ul class="nav nav-tabs" data-ng-hide="create">
<li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
<li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
</ul>
<kc-tabs-user-federation></kc-tabs-user-federation>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@ -281,8 +274,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
<button class="btn btn-primary" data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed">Synchronize changed users</button>
<button class="btn btn-primary" data-ng-click="triggerFullSync()" data-ng-hide="changed">Synchronize all users</button>
</div>

View file

@ -48,14 +48,19 @@
<kc-provider-config config="mapper.config" properties="mapperType.properties" realm="realm" clients="clients"></kc-provider-config>
</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 class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
<button kc-save>Save</button>
<button kc-cancel data-ng-click="cancel()">Cancel</button>
</div>
</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>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Clear changes</button>
</div>
</div>
</form>
</div>

View file

@ -5,12 +5,7 @@
<li>User Federation Mappers</li>
</ol>
<h1>{{provider.providerName === 'ldap' ? 'LDAP' : (provider.providerName|capitalize)}}</h1>
<ul class="nav nav-tabs" data-ng-hide="create">
<li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}">Settings</a></li>
<li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}/mappers">Mappers</a></li>
</ul>
<kc-tabs-user-federation></kc-tabs-user-federation>
<table class="table table-striped table-bordered">
<thead>

View file

@ -47,14 +47,19 @@
</div>
<kc-provider-config config="mapper.config" properties="mapperType.properties" realm="realm"></kc-provider-config>
</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 class="form-group" data-ng-show="create && access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save>Save</button>
<button kc-cancel data-ng-click="cancel()">Cancel</button>
</div>
</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>
<div class="form-group" data-ng-show="!create && access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>
</div>

View file

@ -4,13 +4,7 @@
<li>{{identityProvider.alias}}</li>
</ol>
<h1>{{identityProvider.alias|capitalize}}</h1>
<ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
<li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider && identityProvider.providerId == 'saml'">Export</a></li>
</ul>
<kc-tabs-identity-provider></kc-tabs-identity-provider>
<table class="table table-striped table-bordered">
<thead>

View file

@ -4,49 +4,46 @@
<kc-tabs-authentication></kc-tabs-authentication>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top">
<legend><span class="text">Realm Password Policy</span> <kc-tooltip>Specify required password format. You can also set how many times a password is hashed before it is stored in database. Multiple Regex patterns, separated by comma, can be added.</kc-tooltip></legend>
<table class="table table-striped table-bordered">
<caption class="hidden">Table of Password Policies</caption>
<thead>
<tr ng-show="(allPolicies|remove:policy:'name').length > 0">
<th colspan="5" class="kc-table-actions">
<div class="pull-right">
<div>
<select class="form-control" ng-model="selectedPolicy"
ng-options="(p.name|capitalize) for p in (allPolicies|remove:policy:'name')"
data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null">
<option value="" disabled selected>Add policy...</option>
</select>
</div>
<table class="table table-striped table-bordered">
<caption class="hidden">Table of Password Policies</caption>
<thead>
<tr ng-show="(allPolicies|remove:policy:'name').length > 0">
<th colspan="5" class="kc-table-actions">
<div class="pull-right">
<div>
<select class="form-control" ng-model="selectedPolicy"
ng-options="(p.name|capitalize) for p in (allPolicies|remove:policy:'name')"
data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null">
<option value="" disabled selected>Add policy...</option>
</select>
</div>
</th>
</tr>
<tr>
<th>Policy Type</th>
<th>Policy Value</th>
<th class="actions">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="p in policy">
<td>{{p.name|capitalize}}</td>
<td>
<input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' "
placeholder="No value assigned" min="1" required>
</td>
<td class="actions">
<div class="action-div"><i class="pficon pficon-delete" ng-click="removePolicy($index)" tooltip-placement="right" tooltip="Remove Policy"></i></div>
</td>
</tr>
</tbody>
</table>
</fieldset>
</div>
</th>
</tr>
<tr>
<th>Policy Type</th>
<th>Policy Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="p in policy">
<td>{{p.name|capitalize}}</td>
<td>
<input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' "
placeholder="No value assigned" min="1" required>
</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" ng-click="removePolicy($index)">Delete</button>
</td>
</tr>
</tbody>
</table>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<div class="col-md-12">
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -81,8 +81,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -1,6 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Settings</h1>
<kc-tabs-realm></kc-tabs-realm>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@ -21,8 +19,8 @@
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -1,7 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Add Realm</h1>
<form class="form-horizontal" name="realmForm" novalidate>
<fieldset>
<legend><span class="text">Import Realm</span></legend>
@ -15,10 +12,10 @@
<span class="kc-uploaded-file" data-ng-show="files.length > 0">{{files[0].name}}</span>
</div>
</div>
<div class="form-group" data-ng-show="files.length > 0">
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button type="submit" data-ng-click="uploadFile()" class="btn btn-primary">Upload</button>
<button type="submit" data-ng-click="clearFileSelect()" class="btn btn-default">Cancel</button>
<button type="submit" data-ng-disabled="files.length == 0" data-ng-click="uploadFile()" class="btn btn-primary">Upload</button>
<button type="submit" data-ng-disabled="files.length == 0" data-ng-click="clearFileSelect()" class="btn btn-default">Cancel</button>
</div>
</div>
</fieldset>
@ -44,7 +41,7 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-save data-ng-disabled="!changed">Create</button>
</div>
</div>
</form>

View file

@ -1,7 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1 data-ng-hide="createRealm">Settings</h1>
<h1 data-ng-show="createRealm">Add Realm</h1>
<kc-tabs-realm></kc-tabs-realm>
<form class="form-horizontal " name="realmForm" novalidate kc-read-only="!access.manageRealm">
@ -27,9 +24,8 @@
</div>
<div class="col-md-10 col-md-offset-2" data-ng-show="!createRealm && access.manageRealm">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete Realm</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -4,14 +4,7 @@
<li>{{identityProvider.alias}}</li>
</ol>
<h1 data-ng-hide="create">{{identityProvider.alias|capitalize}}</h1>
<h1 data-ng-show="create">Add OpenID Connect Identity Provider</h1>
<ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
<li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider">Export</a></li>
</ul>
<kc-tabs-identity-provider></kc-tabs-identity-provider>
<form class="form-horizontal" name="realmForm" novalidate>
<fieldset class="border-top">

View file

@ -4,9 +4,7 @@
<li>{{identityProvider.alias}}</li>
</ol>
<h1 data-ng-hide="create">{{identityProvider.alias|capitalize}}<i class="pficon pficon-delete clickable"
data-ng-hide="newIdentityProvider || changed" data-ng-click="remove()"></i></h1>
<h1 data-ng-show="create">Add OpenID Connect Identity Provider</h1>
<kc-tabs-identity-provider></kc-tabs-identity-provider>
<ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
<li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
@ -217,8 +215,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-cancel data-ng-click="cancel()" data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -4,15 +4,7 @@
<li>{{identityProvider.alias}}</li>
</ol>
<h1 data-ng-hide="create">{{identityProvider.alias|capitalize}}<i class="pficon pficon-delete clickable"
data-ng-hide="newIdentityProvider || changed" data-ng-click="remove()"></i></h1>
<h1 data-ng-show="create">Add SAML Identity Provider</h1>
<ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
<li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider && identityProvider.providerId == 'saml'">Export</a></li>
</ul>
<kc-tabs-identity-provider></kc-tabs-identity-provider>
<form class="form-horizontal" name="realmForm" novalidate>
<fieldset>
@ -196,8 +188,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-cancel data-ng-click="cancel()" data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -4,14 +4,7 @@
<li>{{identityProvider.alias}}</li>
</ol>
<h1 data-ng-hide="create">{{identityProvider.alias|capitalize}}<i class="pficon pficon-delete clickable"
data-ng-hide="newIdentityProvider || changed" data-ng-click="remove()"></i></h1>
<h1 data-ng-show="create">Add Social Identity Provider</h1>
<ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
<li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
<li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
</ul>
<kc-tabs-identity-provider></kc-tabs-identity-provider>
<form class="form-horizontal" name="realmForm" novalidate>
<fieldset>
@ -108,8 +101,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-cancel data-ng-click="cancel()" data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -8,7 +8,7 @@
<caption class="hidden">Table of identity providers</caption>
<thead>
<tr>
<th colspan="4" class="kc-table-actions">
<th colspan="5" class="kc-table-actions">
<div class="dropdown pull-right">
<select class="form-control" ng-model="provider"
ng-options="p.name group by p.groupName for p in allProviders track by p.id"
@ -23,6 +23,7 @@
<th>Provider</th>
<th>Enabled</th>
<th width="15%">GUI order</th>
<th>Actions</th>
</tr>
</thead>
<tbody ng-show="configuredProviders.length > 0">
@ -33,6 +34,9 @@
<td>{{identityProvider.providerId}}</td>
<td>{{identityProvider.enabled}}</td>
<td>{{identityProvider.config.guiOrder}}</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Edit</button>
</td>
</tr>
</tbody>
</table>

View file

@ -1,6 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Settings</h1>
<kc-tabs-realm></kc-tabs-realm>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">

View file

@ -1,6 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Settings</h1>
<kc-tabs-realm></kc-tabs-realm>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@ -64,8 +62,8 @@
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -1,6 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Settings</h1>
<kc-tabs-realm></kc-tabs-realm>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@ -56,8 +54,8 @@
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button data-kc-save data-ng-show="changed">Save</button>
<button data-kc-reset data-ng-show="changed">Cancel</button>
<button data-kc-save data-ng-disabled="!changed">Save</button>
<button data-kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -1,6 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Settings</h1>
<kc-tabs-realm></kc-tabs-realm>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@ -88,8 +86,8 @@
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -1,6 +1,4 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Settings</h1>
<kc-tabs-realm></kc-tabs-realm>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@ -116,8 +114,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>

View file

@ -46,8 +46,8 @@
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
<button kc-save data-ng-show="changed">Save</button>
<button kc-reset data-ng-show="changed">Cancel</button>
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>

View file

@ -9,7 +9,7 @@
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="3">
<th class="kc-table-actions" colspan="4">
<div class="form-inline">
<div class="form-group">
<div class="input-group">
@ -30,6 +30,7 @@
<th>Role Name</th>
<th>Composite</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -37,6 +38,9 @@
<td><a href="#/realms/{{realm.realm}}/roles/{{role.id}}">{{role.name}}</a></td>
<td>{{role.composite}}</td>
<td>{{role.description}}</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/roles/{{role.id}}">Edit</button>
</td>
</tr>
<tr data-ng-show="(roles | filter:{name: searchQuery}).length == 0">
<td class="text-muted" colspan="3" data-ng-show="searchQuery">No results</td>

View file

@ -4,8 +4,6 @@
<li>{{user.username}}</li>
</ol>
<h1>{{user.username|capitalize}}</h1>
<kc-tabs-user></kc-tabs-user>
<form class="form-horizontal" name="realmForm" novalidate>

View file

@ -4,8 +4,6 @@
<li>{{user.username}}</li>
</ol>
<h1>{{user.username|capitalize}}</h1>
<kc-tabs-user></kc-tabs-user>
<table class="table table-striped table-bordered">
@ -37,9 +35,9 @@
</span>
</span>
</td>
<td>
<button class="btn btn-danger" ng-click="revokeConsent(consent.clientId)">
<i class="pficon pficon-delete"></i>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" ng-click="revokeConsent(consent.clientId)">
<i class="pficon pficon-delete"></i> Revoke
</button>
</td>
</tr>

View file

@ -4,8 +4,6 @@
<li>{{user.username}}</li>
</ol>
<h1>{{user.username|capitalize}}</h1>
<kc-tabs-user></kc-tabs-user>
<form class="form-horizontal" name="userForm" novalidate>

View file

@ -5,10 +5,6 @@
<li data-ng-show="create">Add User</li>
</ol>
<h1 data-ng-hide="create">{{user.username|capitalize}}<i id="removeUser" class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers && !changed"
data-ng-click="remove()"></i></h1>
<h1 data-ng-show="create">Add User</h1>
<kc-tabs-user></kc-tabs-user>
<form class="form-horizontal" name="userForm" novalidate kc-read-only="!access.manageUsers">
@ -126,9 +122,9 @@
<button kc-cancel data-ng-click="cancel()">Cancel</button>
</div>
<div class="col-md-10 col-md-offset-2" data-ng-show="!create">
<button kc-save data-ng-show="access.manageUsers && changed">Save</button>
<button kc-reset data-ng-show="access.manageUsers && changed">Cancel</button>
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
<button data-ng-show="access.impersonation" class="btn btn-default" data-ng-click="impersonate()" tooltip="Login as this user. If user is in same realm as you, your current login session will be logged out before you are logged in as this user.">Impersonate</button>
</div>
</div>

View file

@ -39,9 +39,12 @@
</fieldset>
<div class="pull-right form-actions" data-ng-show="access.manageRealm">
<button kc-cancel data-ng-click="cancel()">Cancel</button>
<button kc-save>Save</button>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save>Save</button>
<button kc-cancel data-ng-click="cancel()">Cancel</button>
</div>
</div>
</form>

View file

@ -4,16 +4,14 @@
<li>{{user.username}}</li>
</ol>
<h1>{{user.username|capitalize}}</h1>
<kc-tabs-user></kc-tabs-user>
<table class="table table-striped table-bordered">
<thead>
<tr>
<tr data-ng-show="hasAnyProvidersToCreate()">
<th class="kc-table-actions" colspan="4">
<div class="form-inline">
<div class="pull-right" data-ng-show="hasAnyProvidersToCreate()">
<div class="pull-right">
<a class="btn btn-primary" href="#/create/federated-identity/{{realm.realm}}/{{user.id}}">Create</a>
</div>
</div>
@ -31,8 +29,8 @@
<td>{{identity.identityProvider}}</td>
<td>{{identity.userId}}</td>
<td>{{identity.userName}}</td>
<td class="actions">
<div class="action-div"><i class="pficon pficon-delete" ng-click="removeProviderLink(identity)" tooltip-placement="right" tooltip="Remove Provider Link"></i></div>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" ng-click="removeProviderLink(identity)">Remove</button>
</td>
</tr>
<tr data-ng-show="federatedIdentities.length == 0">

View file

@ -6,7 +6,7 @@
<table class="table table-striped table-bordered">
<thead>
<tr ng-show="providers.length > 0 && access.manageUsers">
<th colspan="3" class="kc-table-actions">
<th colspan="4" class="kc-table-actions">
<div class="pull-right">
<div>
<select class="form-control" ng-model="selectedProvider"
@ -22,6 +22,7 @@
<th>ID</th>
<th>Provider Name</th>
<th>Priority</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -29,6 +30,9 @@
<td><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">{{instance.displayName}}</a></td>
<td>{{instance.providerName|capitalize}}</td>
<td>{{instance.priority}}</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Edit</button>
</td>
</tr>
<tr data-ng-show="!instances || instances.length == 0">
<td class="text-muted">No user federation providers configured</td>

View file

@ -5,7 +5,7 @@
<caption data-ng-show="users" class="hidden">Table of realm users</caption>
<thead>
<tr>
<th colspan="{{access.impersonation == true ? '5' : '4'}}">
<th colspan="{{access.impersonation == true ? '6' : '5'}}">
<div class="form-inline">
<div class="form-group">
<div class="input-group">
@ -30,7 +30,7 @@
<th>Last Name</th>
<th>First Name</th>
<th>Email</th>
<th data-ng-show="access.impersonation"></th>
<th colspan="{{access.impersonation == true ? '2' : '1'}}">Actions</th>
</tr>
</tr>
</thead>
@ -51,7 +51,12 @@
<td>{{user.lastName}}</td>
<td>{{user.firstName}}</td>
<td>{{user.email}}</td>
<td data-ng-show="access.impersonation"><button class="btn btn-default" data-ng-click="impersonate(user.id)" tooltip="Login as this user. If user is in same realm as you, your current login session will be logged out before you are logged in as this user.">Impersonate</button></td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/users/{{user.id}}">Edit</button>
</td>
<td data-ng-show="access.impersonation" class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" data-ng-click="impersonate(user.id)" tooltip="Login as this user. If user is in same realm as you, your current login session will be logged out before you are logged in as this user.">Impersonate</button>
</td>
</tr>
<tr data-ng-show="!users || users.length == 0">
<td class="text-muted" data-ng-show="!users">Please enter a search, or click on view all users</td>

View file

@ -4,8 +4,6 @@
<li>{{user.username}}</li>
</ol>
<h1>{{user.username|capitalize}}</h1>
<kc-tabs-user></kc-tabs-user>
<table class="table table-striped table-bordered">
@ -36,7 +34,9 @@
</div>
</ul>
</td>
<td data-ng-show="access.manageUsers"><a href="" ng-click="logoutSession(session.id)">logout</a> </td>
<td class="kc-action-cell" data-ng-show="access.manageUsers">
<button class="btn btn-default btn-block btn-sm" ng-click="logoutSession(session.id)">Logout</button>
</td>
</tr>
</tbody>
</table>

View file

@ -27,7 +27,7 @@
|| path[2] == 'cache-settings'
|| path[2] == 'defense'
|| path[2] == 'keys-settings' || path[2] == 'smtp-settings' || path[2] == 'ldap-settings' || path[2] == 'auth-settings') && path[3] != 'clients') && 'active'">
<a href="#/realms/{{realm.realm}}"><span class="pficon pficon-settings"></span> Settings</a>
<a href="#/realms/{{realm.realm}}"><span class="pficon pficon-settings"></span> Realm Settings</a>
</li>
<li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'clients' || path[1] == 'client' || path[3] == 'clients') && 'active'"><a href="#/realms/{{realm.realm}}/clients"><i class="fa fa-cubes"></i> Clients</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'roles' || path[2] == 'default-roles' || (path[1] == 'role' && path[3] != 'clients')) && 'active'"><a href="#/realms/{{realm.realm}}/roles"><i class="fa fa-tasks"></i> Roles</a></li>

View file

@ -1,28 +1,41 @@
<ul class="nav nav-tabs nav-tabs-pf" data-ng-hide="create && !path[4]">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">Settings</a></li>
<li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">Credentials</a></li>
<li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">SAML Keys</a></li>
<li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">Roles</a></li>
<li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers">Mappers</a>
<kc-tooltip>Protocol mappers perform transformation on tokens and documents. They an do things like map user data into protocol claims, or just transform any requests going between the client and auth server.</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/scope-mappings">Scope</a>
<kc-tooltip>Scope mappings allow you to restrict which user role mappings are included within the access token requested by the client.</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">Revocation</a></li>
<!-- <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
<li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/sessions">Sessions</a>
<kc-tooltip>View active sessions for this client. Allows you to see which users are active and when they logged in.</kc-tooltip>
</li>
<div data-ng-controller="ClientTabCtrl">
<li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">Clustering</a></li>
<h1 data-ng-show="create">Add Client</h1>
<h1 data-ng-hide="create">
{{client.clientId|capitalize}}
<i id="removeClient" class="pficon pficon-delete clickable" data-ng-show="access.manageClients" data-ng-click="removeClient()"></i>
</h1>
<li ng-class="{active: path[4] == 'installation'}" data-ng-show="client.protocol != 'saml'">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/installation">Installation</a>
<kc-tooltip>Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients.</kc-tooltip>
</li>
<ul class="nav nav-tabs nav-tabs-pf" data-ng-hide="create && !path[4]">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">Settings</a></li>
<li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">Credentials</a></li>
<li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">SAML Keys</a></li>
<li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">Roles</a></li>
<li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers">Mappers</a>
<kc-tooltip>Protocol mappers perform transformation on tokens and documents. They an do things like map user data into protocol claims, or just transform any requests going between the client and auth server.</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/scope-mappings">Scope</a>
<kc-tooltip>Scope mappings allow you to restrict which user role mappings are included within the access token requested by the client.</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">Revocation</a></li>
<!-- <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
<li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/sessions">Sessions</a>
<kc-tooltip>View active sessions for this client. Allows you to see which users are active and when they logged in.</kc-tooltip>
</li>
</ul>
<li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">Clustering</a></li>
<li ng-class="{active: path[4] == 'installation'}" data-ng-show="client.protocol != 'saml'">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/installation">Installation</a>
<kc-tooltip>Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients.</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'service-accounts'}" data-ng-show="!client.publicClient && !client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-accounts">Service Accounts</a>
<kc-tooltip>Allows you to authenticate this client to Keycloak and retrieve access tokens dedicated to this client.</kc-tooltip>
</li>
</ul>
</div>

View file

@ -0,0 +1,13 @@
<div data-ng-controller="IdentityProviderTabCtrl">
<h1 data-ng-hide="path[0] == 'create'">
{{identityProvider.alias|capitalize}}
<i class="pficon pficon-delete clickable" data-ng-hide="newIdentityProvider || changed" data-ng-click="removeIdentityProvider()"></i>
</h1>
<h1 data-ng-show="path[0] == 'create'">Add Identity Provider</h1>
<ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
<li ng-class="{active: !path[6] && path.length > 5}"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
<li ng-class="{active: path[4] == 'mappers'}"><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
<li ng-class="{active: path[6] == 'export'}"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider && identityProvider.providerId == 'saml'">Export</a></li>
</ul>
</div>

View file

@ -1,10 +1,18 @@
<ul class="nav nav-tabs">
<li ng-class="{active: !path[2]}"><a href="#/realms/{{realm.realm}}">General</a></li>
<li ng-class="{active: path[2] == 'login-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/login-settings">Login</a></li>
<li ng-class="{active: path[2] == 'keys-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys-settings">Keys</a></li>
<li ng-class="{active: path[2] == 'smtp-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/smtp-settings">Email</a></li>
<li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">Themes</a></li>
<li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">Cache</a></li>
<li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">Tokens</a></li>
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">Security Defenses</a></li>
</ul>
<div data-ng-controller="RealmTabCtrl">
<h1 data-ng-hide="createRealm">
{{realm.realm|capitalize}}
<i id="removeRealm" class="pficon pficon-delete clickable" data-ng-show="access.manageRealm" data-ng-click="removeRealm()"></i>
</h1>
<h1 data-ng-show="createRealm">Add Realm</h1>
<ul class="nav nav-tabs">
<li ng-class="{active: !path[2]}"><a href="#/realms/{{realm.realm}}">General</a></li>
<li ng-class="{active: path[2] == 'login-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/login-settings">Login</a></li>
<li ng-class="{active: path[2] == 'keys-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys-settings">Keys</a></li>
<li ng-class="{active: path[2] == 'smtp-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/smtp-settings">Email</a></li>
<li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">Themes</a></li>
<li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">Cache</a></li>
<li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">Tokens</a></li>
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">Security Defenses</a></li>
</ul>
</div>

View file

@ -0,0 +1,12 @@
<div data-ng-controller="UserFederationTabCtrl">
<h1 data-ng-hide="create">
{{instance.displayName|capitalize}}
<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUserFederation()"></i>
</h1>
<h1 data-ng-show="create">Add User Federation Provider</h1>
<ul class="nav nav-tabs" data-ng-hide="create">
<li ng-class="{active: !path[6]}"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
<li ng-class="{active: path[6] == 'mappers'}"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
</ul>
</div>

View file

@ -1,8 +1,16 @@
<ul class="nav nav-tabs" data-ng-show="!create">
<li ng-class="{active: !path[4] && path[0] != 'create'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}">Attributes</a></li>
<li ng-class="{active: path[4] == 'user-credentials'}" data-ng-show="access.manageUsers"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/user-credentials">Credentials</a></li>
<li ng-class="{active: path[4] == 'role-mappings'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/role-mappings">Role Mappings</a></li>
<li ng-class="{active: path[4] == 'consents'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/consents">Consents</a></li>
<li ng-class="{active: path[4] == 'sessions'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/sessions">Sessions</a></li>
<li ng-class="{active: path[4] == 'federated-identity' || path[1] == 'federated-identity'}" data-ng-show="user.federatedIdentities != null"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/federated-identity">Identity Provider Links</a></li>
</ul>
<div data-ng-controller="UserTabCtrl">
<h1 data-ng-hide="create">
{{user.username|capitalize}}
<i id="removeUser" class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUser()"></i>
</h1>
<h1 data-ng-show="create">Add User</h1>
<ul class="nav nav-tabs" data-ng-show="!create">
<li ng-class="{active: !path[4] && path[0] != 'create'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}">Attributes</a></li>
<li ng-class="{active: path[4] == 'user-credentials'}" data-ng-show="access.manageUsers"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/user-credentials">Credentials</a></li>
<li ng-class="{active: path[4] == 'role-mappings'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/role-mappings">Role Mappings</a></li>
<li ng-class="{active: path[4] == 'consents'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/consents">Consents</a></li>
<li ng-class="{active: path[4] == 'sessions'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/sessions">Sessions</a></li>
<li ng-class="{active: path[4] == 'federated-identity' || path[1] == 'federated-identity'}" data-ng-show="user.federatedIdentities != null"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/federated-identity">Identity Provider Links</a></li>
</ul>
</div>

View file

@ -299,3 +299,17 @@ h1 i {
margin-left: 10px;
}
/* Action cell */
.kc-action-cell {
position: relative;
width: 100px;
}
.kc-action-cell .btn {
border: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

View file

@ -103,6 +103,9 @@ public interface ClientModel extends RoleContainerModel {
boolean isConsentRequired();
void setConsentRequired(boolean consentRequired);
boolean isServiceAccountsEnabled();
void setServiceAccountsEnabled(boolean serviceAccountsEnabled);
Set<RoleModel> getScopeMappings();
void addScopeMapping(RoleModel role);
void deleteScopeMapping(RoleModel role);

View file

@ -304,6 +304,11 @@ public class UserFederationManager implements UserProvider {
}, realm, firstResult, maxResults);
}
@Override
public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
return session.userStorage().searchForUserByUserAttributes(attributes, realm);
}
@Override
public Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm) {
validateUser(realm, user);

View file

@ -32,6 +32,10 @@ public interface UserProvider extends Provider {
List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults);
List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm);
List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults);
// Searching by UserModel.attribute (not property)
List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm);
Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm);
FederatedIdentityModel getFederatedIdentity(UserModel user, String socialProvider, RealmModel realm);

View file

@ -26,6 +26,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private String baseUrl;
private boolean bearerOnly;
private boolean consentRequired;
private boolean serviceAccountsEnabled;
private boolean directGrantsOnly;
private int nodeReRegistrationTimeout;
@ -210,6 +211,14 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.consentRequired = consentRequired;
}
public boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
public boolean isDirectGrantsOnly() {
return directGrantsOnly;
}

View file

@ -351,6 +351,6 @@ public final class KeycloakModelUtils {
}
public static String toLowerCaseSafe(String str) {
return str==null ? str : str.toLowerCase();
return str==null ? null : str.toLowerCase();
}
}

View file

@ -289,6 +289,7 @@ public class ModelToRepresentation {
rep.setFullScopeAllowed(clientModel.isFullScopeAllowed());
rep.setBearerOnly(clientModel.isBearerOnly());
rep.setConsentRequired(clientModel.isConsentRequired());
rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled());
rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly());
rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired());
rep.setBaseUrl(clientModel.getBaseUrl());

View file

@ -625,6 +625,7 @@ public class RepresentationToModel {
if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
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.isDirectGrantsOnly() != null) client.setDirectGrantsOnly(resourceRep.isDirectGrantsOnly());
if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
@ -714,6 +715,7 @@ public class RepresentationToModel {
if (rep.isEnabled() != null) resource.setEnabled(rep.isEnabled());
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.isDirectGrantsOnly() != null) resource.setDirectGrantsOnly(rep.isDirectGrantsOnly());
if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());

View file

@ -38,6 +38,7 @@ import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -225,6 +226,25 @@ public class FileUserProvider implements UserProvider {
return sortedSubList(found, firstResult, maxResults);
}
@Override
public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
Collection<UserModel> users = inMemoryModel.getUsers(realm.getId());
for (Map.Entry<String, String> entry : attributes.entrySet()) {
List<UserModel> matchedUsers = new ArrayList<>();
for (UserModel user : users) {
List<String> vals = user.getAttribute(entry.getKey());
if (vals.contains(entry.getValue())) {
matchedUsers.add(user);
}
}
users = matchedUsers;
}
return (List<UserModel>) users;
}
@Override
public Set<FederatedIdentityModel> getFederatedIdentities(UserModel userModel, RealmModel realm) {
UserEntity userEntity = ((UserAdapter)getUserById(userModel.getId(), realm)).getUserEntity();

View file

@ -441,6 +441,16 @@ public class ClientAdapter implements ClientModel {
entity.setConsentRequired(consentRequired);
}
@Override
public boolean isServiceAccountsEnabled() {
return entity.isServiceAccountsEnabled();
}
@Override
public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
entity.setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
public boolean isDirectGrantsOnly() {
return entity.isDirectGrantsOnly();

View file

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

View file

@ -241,6 +241,11 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
return getDelegate().searchForUserByAttributes(attributes, realm, firstResult, maxResults);
}
@Override
public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
return getDelegate().searchForUserByUserAttributes(attributes, realm);
}
@Override
public Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm) {
return getDelegate().getFederatedIdentities(user, realm);

View file

@ -108,6 +108,11 @@ public class NoCacheUserProvider implements CacheUserProvider {
return getDelegate().searchForUserByAttributes(attributes, realm, firstResult, maxResults);
}
@Override
public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
return getDelegate().searchForUserByUserAttributes(attributes, realm);
}
@Override
public Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm) {
return getDelegate().getFederatedIdentities(user, realm);

View file

@ -46,6 +46,7 @@ public class CachedClient implements Serializable {
private List<String> defaultRoles = new LinkedList<String>();
private boolean bearerOnly;
private boolean consentRequired;
private boolean serviceAccountsEnabled;
private Map<String, String> roles = new HashMap<String, String>();
private int nodeReRegistrationTimeout;
private Map<String, Integer> registeredNodes;
@ -78,6 +79,7 @@ public class CachedClient implements Serializable {
defaultRoles.addAll(model.getDefaultRoles());
bearerOnly = model.isBearerOnly();
consentRequired = model.isConsentRequired();
serviceAccountsEnabled = model.isServiceAccountsEnabled();
for (RoleModel role : model.getRoles()) {
roles.put(role.getName(), role.getId());
cache.addCachedRole(new CachedClientRole(id, role, realm));
@ -178,6 +180,10 @@ public class CachedClient implements Serializable {
return consentRequired;
}
public boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
public Map<String, String> getRoles() {
return roles;
}

View file

@ -460,6 +460,16 @@ public class ClientAdapter implements ClientModel {
entity.setConsentRequired(consentRequired);
}
@Override
public boolean isServiceAccountsEnabled() {
return entity.isServiceAccountsEnabled();
}
@Override
public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
entity.setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
public boolean isDirectGrantsOnly() {
return entity.isDirectGrantsOnly();

View file

@ -18,8 +18,10 @@ import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -379,6 +381,38 @@ public class JpaUserProvider implements UserProvider {
return users;
}
@Override
public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
StringBuilder builder = new StringBuilder("select attr.user,count(attr.user) from UserAttributeEntity attr where attr.user.realmId = :realmId");
boolean first = true;
for (Map.Entry<String, String> entry : attributes.entrySet()) {
String attrName = entry.getKey();
if (first) {
builder.append(" and ");
first = false;
} else {
builder.append(" or ");
}
builder.append(" ( attr.name like :").append(attrName);
builder.append(" and attr.value like :").append(attrName).append("val )");
}
builder.append(" group by attr.user having count(attr.user) = " + attributes.size());
Query query = em.createQuery(builder.toString());
query.setParameter("realmId", realm.getId());
for (Map.Entry<String, String> entry : attributes.entrySet()) {
query.setParameter(entry.getKey(), entry.getKey());
query.setParameter(entry.getKey() + "val", entry.getValue());
}
List results = query.getResultList();
List<UserModel> users = new ArrayList<UserModel>();
for (Object o : results) {
UserEntity user = (UserEntity) ((Object[])o)[0];
users.add(new UserAdapter(realm, em, user));
}
return users;
}
private FederatedIdentityEntity findFederatedIdentity(UserModel user, String identityProvider) {
TypedQuery<FederatedIdentityEntity> query = em.createNamedQuery("findFederatedIdentityByUserAndProvider", FederatedIdentityEntity.class);
UserEntity userEntity = em.getReference(UserEntity.class, user.getId());

View file

@ -95,6 +95,9 @@ public class ClientEntity {
@Column(name="CONSENT_REQUIRED")
private boolean consentRequired;
@Column(name="SERVICE_ACCOUNTS_ENABLED")
private boolean serviceAccountsEnabled;
@Column(name="NODE_REREG_TIMEOUT")
private int nodeReRegistrationTimeout;
@ -295,6 +298,14 @@ public class ClientEntity {
this.consentRequired = consentRequired;
}
public boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
public boolean isDirectGrantsOnly() {
return directGrantsOnly;
}

View file

@ -461,6 +461,17 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
updateMongoEntity();
}
@Override
public boolean isServiceAccountsEnabled() {
return getMongoEntity().isServiceAccountsEnabled();
}
@Override
public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
getMongoEntity().setServiceAccountsEnabled(serviceAccountsEnabled);
updateMongoEntity();
}
@Override
public boolean isDirectGrantsOnly() {
return getMongoEntity().isDirectGrantsOnly();

View file

@ -214,6 +214,19 @@ public class MongoUserProvider implements UserProvider {
return convertUserEntities(realm, users);
}
@Override
public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
QueryBuilder queryBuilder = new QueryBuilder()
.and("realmId").is(realm.getId());
for (Map.Entry<String, String> entry : attributes.entrySet()) {
queryBuilder.and("attributes." + entry.getKey()).is(entry.getValue());
}
List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, queryBuilder.get(), invocationContext);
return convertUserEntities(realm, users);
}
@Override
public Set<FederatedIdentityModel> getFederatedIdentities(UserModel userModel, RealmModel realm) {
UserModel user = getUserById(userModel.getId(), realm);

View file

@ -0,0 +1,165 @@
package org.keycloak.protocol.oidc;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
/**
* Endpoint for authenticate clients and retrieve service accounts
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ServiceAccountManager {
protected static final Logger logger = Logger.getLogger(ServiceAccountManager.class);
private TokenManager tokenManager;
private AuthenticationManager authManager;
private EventBuilder event;
private HttpRequest request;
private MultivaluedMap<String, String> formParams;
private KeycloakSession session;
private RealmModel realm;
private HttpHeaders headers;
private UriInfo uriInfo;
private ClientConnection clientConnection;
private ClientModel client;
private UserModel clientUser;
public ServiceAccountManager(TokenManager tokenManager, AuthenticationManager authManager, EventBuilder event, HttpRequest request, MultivaluedMap<String, String> formParams, KeycloakSession session) {
this.tokenManager = tokenManager;
this.authManager = authManager;
this.event = event;
this.request = request;
this.formParams = formParams;
this.session = session;
this.realm = session.getContext().getRealm();
this.headers = session.getContext().getRequestHeaders();
this.uriInfo = session.getContext().getUri();
this.clientConnection = session.getContext().getConnection();
}
public Response buildClientCredentialsGrant() {
authenticateClient();
checkClient();
return finishClientAuthorization();
}
protected void authenticateClient() {
// TODO: This should be externalized into pluggable SPI for client authentication (hopefully Authentication SPI can be reused).
// Right now, just Client Credentials Grants (as per OAuth2 specs) is supported
String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formParams, event, realm);
event.detail(Details.CLIENT_AUTH_METHOD, Details.CLIENT_AUTH_METHOD_VALUE_CLIENT_CREDENTIALS);
}
protected void checkClient() {
if (client.isBearerOnly()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
}
if (client.isPublicClient()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Public client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
}
if (!client.isServiceAccountsEnabled()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
}
}
protected Response finishClientAuthorization() {
event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
Map<String, String> search = new HashMap<>();
search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
List<UserModel> users = session.users().searchForUserByUserAttributes(search, realm);
if (users.size() == 0) {
// May need to handle bootstrap here as well
logger.warnf("Service account user for client '%s' not found. Creating now", client.getClientId());
new ClientManager(new RealmManager(session)).enableServiceAccount(client);
users = session.users().searchForUserByUserAttributes(search, realm);
clientUser = users.get(0);
} else if (users.size() == 1) {
clientUser = users.get(0);
} else {
throw new ModelDuplicateException("Multiple service account users found for client '" + client.getClientId() + "' . Check your DB");
}
String clientUsername = clientUser.getUsername();
event.detail(Details.USERNAME, clientUsername);
event.user(clientUser);
if (!clientUser.isEnabled()) {
event.error(Errors.USER_DISABLED);
throw new ErrorResponseException("invalid_request", "User '" + clientUsername + "' disabled", Response.Status.UNAUTHORIZED);
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
UserSessionProvider sessions = session.sessions();
// TODO: Once more requirements are added, clientSession will be likely created earlier by authentication mechanism
ClientSessionModel clientSession = sessions.createClientSession(realm, client);
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
// TODO: Should rather obtain authMethod from client session?
UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
event.session(userSession);
TokenManager.attachClientSession(userSession, clientSession);
// Notes about client details
userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
.generateAccessToken(session, scope, client, clientUser, userSession, clientSession)
.generateRefreshToken()
.generateIDToken()
.build();
event.success();
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.ServiceAccountManager;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
@ -53,7 +54,7 @@ public class TokenEndpoint {
private ClientModel client;
private enum Action {
AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD
AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS
}
@Context
@ -97,7 +98,11 @@ public class TokenEndpoint {
checkSsl();
checkRealm();
checkGrantType();
checkClient();
// client grant type will do it's own verification of client
if (!grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) {
checkClient();
}
switch (action) {
case AUTHORIZATION_CODE:
@ -106,6 +111,8 @@ public class TokenEndpoint {
return buildRefreshToken();
case PASSWORD:
return buildResourceOwnerPasswordCredentialsGrant();
case CLIENT_CREDENTIALS:
return buildClientCredentialsGrant();
}
throw new RuntimeException("Unknown action " + action);
@ -144,7 +151,7 @@ public class TokenEndpoint {
String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formParams, event, realm);
if ((client instanceof ClientModel) && ((ClientModel) client).isBearerOnly()) {
if (client.isBearerOnly()) {
throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST);
}
}
@ -167,6 +174,9 @@ public class TokenEndpoint {
} else if (grantType.equals(OAuth2Constants.PASSWORD)) {
event.event(EventType.LOGIN);
action = Action.PASSWORD;
} else if (grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) {
event.event(EventType.CLIENT_LOGIN);
action = Action.CLIENT_CREDENTIALS;
} else {
throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
}
@ -355,4 +365,9 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
public Response buildClientCredentialsGrant() {
ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, authManager, event, request, formParams, session);
return serviceAccountManager.buildClientCredentialsGrant();
}
}

View file

@ -3,10 +3,15 @@ package org.keycloak.services.managers;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.annotate.JsonPropertyOrder;
import org.jboss.logging.Logger;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.adapters.config.BaseRealmConfig;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.Time;
@ -84,6 +89,57 @@ public class ClientManager {
return validatedNodes;
}
public void enableServiceAccount(ClientModel client) {
client.setServiceAccountsEnabled(true);
// Add dedicated user for this service account
RealmModel realm = client.getRealm();
Map<String, String> search = new HashMap<>();
search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
List<UserModel> serviceAccountUsers = realmManager.getSession().users().searchForUserByUserAttributes(search, realm);
if (serviceAccountUsers.size() == 0) {
String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + client.getClientId();
logger.infof("Creating service account user '%s'", username);
UserModel user = realmManager.getSession().users().addUser(realm, username);
user.setEnabled(true);
user.setEmail(username + "@placeholder.org");
user.setSingleAttribute(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
}
// Add protocol mappers to retrieve clientId in access token
if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER, client.getClientId());
ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER,
ServiceAccountConstants.CLIENT_ID,
ServiceAccountConstants.CLIENT_ID, "String",
false, "",
true, true);
client.addProtocolMapper(protocolMapper);
}
// Add protocol mappers to retrieve hostname and IP address of client in access token
if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_HOST_PROTOCOL_MAPPER) == null) {
logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_HOST_PROTOCOL_MAPPER, client.getClientId());
ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_HOST_PROTOCOL_MAPPER,
ServiceAccountConstants.CLIENT_HOST,
ServiceAccountConstants.CLIENT_HOST, "String",
false, "",
true, true);
client.addProtocolMapper(protocolMapper);
}
if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ADDRESS_PROTOCOL_MAPPER) == null) {
logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_ADDRESS_PROTOCOL_MAPPER, client.getClientId());
ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_ADDRESS_PROTOCOL_MAPPER,
ServiceAccountConstants.CLIENT_ADDRESS,
ServiceAccountConstants.CLIENT_ADDRESS, "String",
false, "",
true, true);
client.addProtocolMapper(protocolMapper);
}
}
@JsonPropertyOrder({"realm", "realm-public-key", "bearer-only", "auth-server-url", "ssl-required",
"resource", "public-client", "credentials",
"use-resource-role-mappings"})

Some files were not shown because too many files have changed in this diff Show more