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 = {
@ -1813,3 +1843,19 @@ module.directive('kcTooltip', function($compile) {
}
};
});
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

@ -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>
<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>
<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>
<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,8 +4,6 @@
<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>
@ -25,7 +23,7 @@
<tr>
<th>Policy Type</th>
<th>Policy Value</th>
<th class="actions">Actions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -35,18 +33,17 @@
<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 class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" ng-click="removePolicy($index)">Delete</button>
</td>
</tr>
</tbody>
</table>
</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>
<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>
<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,4 +1,12 @@
<ul class="nav nav-tabs nav-tabs-pf" data-ng-hide="create && !path[4]">
<div data-ng-controller="ClientTabCtrl">
<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>
<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>
@ -12,7 +20,7 @@
<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] == '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>
@ -25,4 +33,9 @@
<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>
<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,4 +1,11 @@
<ul class="nav nav-tabs">
<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>
@ -7,4 +14,5 @@
<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>
</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">
<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>
</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();
// 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"})

View file

@ -101,6 +101,10 @@ public class ClientResource {
auth.requireManage();
try {
if (rep.isServiceAccountsEnabled() && !client.isServiceAccountsEnabled()) {
new ClientManager(new RealmManager(session)).enableServiceAccount(client);;
}
RepresentationToModel.updateClient(rep, client);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
return Response.noContent().build();

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