Removed old account console (#21098)
Co-authored-by: Jon Koops <jonkoops@gmail.com> Closes #9864
This commit is contained in:
parent
3246a15442
commit
f82577a7f3
57 changed files with 109 additions and 5079 deletions
|
@ -41,12 +41,16 @@ public class MigrateTo22_0_0 implements Migration {
|
|||
|
||||
@Override
|
||||
public void migrate(KeycloakSession session) {
|
||||
session.realms().getRealmsStream().forEach(this::removeHttpChallengeFlow);
|
||||
session.realms().getRealmsStream().forEach((realm) -> {
|
||||
removeHttpChallengeFlow(realm);
|
||||
updateAccountTheme(realm);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
|
||||
removeHttpChallengeFlow(realm);
|
||||
updateAccountTheme(realm);
|
||||
}
|
||||
|
||||
private void removeHttpChallengeFlow(RealmModel realm) {
|
||||
|
@ -63,6 +67,13 @@ public class MigrateTo22_0_0 implements Migration {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateAccountTheme(RealmModel realm) {
|
||||
String accountTheme = realm.getAccountTheme();
|
||||
if ("keycloak".equals(accountTheme) || "rh-sso".equals(accountTheme)) {
|
||||
realm.setAccountTheme("keycloak.v2");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelVersion getVersion() {
|
||||
return VERSION;
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public enum AccountPages {
|
||||
|
||||
ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS, APPLICATIONS, RESOURCES, RESOURCE_DETAIL;
|
||||
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account;
|
||||
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface AccountProvider extends Provider {
|
||||
|
||||
AccountProvider setUriInfo(UriInfo uriInfo);
|
||||
|
||||
AccountProvider setHttpHeaders(HttpHeaders httpHeaders);
|
||||
|
||||
Response createResponse(AccountPages page);
|
||||
|
||||
AccountProvider setError(Response.Status status, String message, Object ... parameters);
|
||||
|
||||
AccountProvider setErrors(Response.Status status, List<FormMessage> messages);
|
||||
|
||||
AccountProvider setSuccess(String message, Object ... parameters);
|
||||
|
||||
AccountProvider setWarning(String message, Object ... parameters);
|
||||
|
||||
AccountProvider setUser(UserModel user);
|
||||
|
||||
AccountProvider setProfileFormData(MultivaluedMap<String, String> formData);
|
||||
|
||||
AccountProvider setRealm(RealmModel realm);
|
||||
|
||||
AccountProvider setReferrer(String[] referrer);
|
||||
|
||||
AccountProvider setEvents(List<Event> events);
|
||||
|
||||
AccountProvider setSessions(List<UserSessionModel> sessions);
|
||||
|
||||
AccountProvider setPasswordSet(boolean passwordSet);
|
||||
|
||||
AccountProvider setStateChecker(String stateChecker);
|
||||
|
||||
AccountProvider setIdTokenHint(String idTokenHint);
|
||||
|
||||
AccountProvider setFeatures(boolean social, boolean events, boolean passwordUpdateSupported, boolean authorizationSupported);
|
||||
|
||||
AccountProvider setAttribute(String key, String value);
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface AccountProviderFactory extends ProviderFactory {
|
||||
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class AccountSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "account";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return AccountProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return AccountProviderFactory.class;
|
||||
}
|
||||
|
||||
}
|
|
@ -46,7 +46,6 @@ org.keycloak.protocol.ProtocolMapperSpi
|
|||
org.keycloak.broker.provider.IdentityProviderSpi
|
||||
org.keycloak.broker.provider.IdentityProviderMapperSpi
|
||||
org.keycloak.broker.social.SocialProviderSpi
|
||||
org.keycloak.forms.account.AccountSpi
|
||||
org.keycloak.forms.login.LoginFormsSpi
|
||||
org.keycloak.email.EmailSenderSpi
|
||||
org.keycloak.email.EmailTemplateSpi
|
||||
|
|
|
@ -1,400 +0,0 @@
|
|||
/*
|
||||
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.forms.account.freemarker;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.forms.account.AccountPages;
|
||||
import org.keycloak.forms.account.AccountProvider;
|
||||
import org.keycloak.forms.account.freemarker.model.AccountBean;
|
||||
import org.keycloak.forms.account.freemarker.model.AccountFederatedIdentityBean;
|
||||
import org.keycloak.forms.account.freemarker.model.ApplicationsBean;
|
||||
import org.keycloak.forms.account.freemarker.model.AuthorizationBean;
|
||||
import org.keycloak.forms.account.freemarker.model.FeaturesBean;
|
||||
import org.keycloak.forms.account.freemarker.model.LogBean;
|
||||
import org.keycloak.forms.account.freemarker.model.PasswordBean;
|
||||
import org.keycloak.forms.account.freemarker.model.RealmBean;
|
||||
import org.keycloak.forms.account.freemarker.model.ReferrerBean;
|
||||
import org.keycloak.forms.account.freemarker.model.SessionsBean;
|
||||
import org.keycloak.forms.account.freemarker.model.TotpBean;
|
||||
import org.keycloak.forms.account.freemarker.model.UrlBean;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.theme.FreeMarkerException;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
|
||||
import org.keycloak.theme.beans.LocaleBean;
|
||||
import org.keycloak.theme.beans.MessageBean;
|
||||
import org.keycloak.theme.beans.MessageFormatterMethod;
|
||||
import org.keycloak.theme.beans.MessageType;
|
||||
import org.keycloak.theme.beans.MessagesPerFieldBean;
|
||||
import org.keycloak.theme.freemarker.FreeMarkerProvider;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class FreeMarkerAccountProvider implements AccountProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(FreeMarkerAccountProvider.class);
|
||||
|
||||
protected UserModel user;
|
||||
protected MultivaluedMap<String, String> profileFormData;
|
||||
protected Response.Status status = Response.Status.OK;
|
||||
protected RealmModel realm;
|
||||
protected String[] referrer;
|
||||
protected List<Event> events;
|
||||
protected String stateChecker;
|
||||
protected String idTokenHint;
|
||||
protected List<UserSessionModel> sessions;
|
||||
protected boolean identityProviderEnabled;
|
||||
protected boolean eventsEnabled;
|
||||
protected boolean passwordUpdateSupported;
|
||||
protected boolean passwordSet;
|
||||
protected KeycloakSession session;
|
||||
protected FreeMarkerProvider freeMarker;
|
||||
protected HttpHeaders headers;
|
||||
protected Map<String, Object> attributes;
|
||||
|
||||
protected UriInfo uriInfo;
|
||||
|
||||
protected List<FormMessage> messages = null;
|
||||
protected MessageType messageType = MessageType.ERROR;
|
||||
private boolean authorizationSupported;
|
||||
|
||||
public FreeMarkerAccountProvider(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.freeMarker = session.getProvider(FreeMarkerProvider.class);
|
||||
}
|
||||
|
||||
public AccountProvider setUriInfo(UriInfo uriInfo) {
|
||||
this.uriInfo = uriInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setHttpHeaders(HttpHeaders httpHeaders) {
|
||||
this.headers = httpHeaders;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response createResponse(AccountPages page) {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
if (this.attributes != null) {
|
||||
attributes.putAll(this.attributes);
|
||||
}
|
||||
|
||||
Theme theme;
|
||||
try {
|
||||
theme = getTheme();
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create theme", e);
|
||||
return Response.serverError().build();
|
||||
}
|
||||
|
||||
Locale locale = session.getContext().resolveLocale(user);
|
||||
Properties messagesBundle = handleThemeResources(theme, locale, attributes);
|
||||
|
||||
URI baseUri = uriInfo.getBaseUri();
|
||||
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
|
||||
for (Map.Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
|
||||
baseUriBuilder.queryParam(e.getKey(), e.getValue().toArray());
|
||||
}
|
||||
URI baseQueryUri = baseUriBuilder.build();
|
||||
|
||||
if (stateChecker != null) {
|
||||
attributes.put("stateChecker", stateChecker);
|
||||
}
|
||||
|
||||
handleMessages(locale, messagesBundle, attributes);
|
||||
|
||||
if (referrer != null) {
|
||||
attributes.put("referrer", new ReferrerBean(referrer));
|
||||
}
|
||||
|
||||
if(realm != null){
|
||||
attributes.put("realm", new RealmBean(realm));
|
||||
}
|
||||
|
||||
attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri(), idTokenHint));
|
||||
|
||||
if (realm.isInternationalizationEnabled()) {
|
||||
UriBuilder b = UriBuilder.fromUri(baseQueryUri).path(uriInfo.getPath());
|
||||
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
|
||||
}
|
||||
|
||||
attributes.put("features", new FeaturesBean(identityProviderEnabled, eventsEnabled, passwordUpdateSupported, authorizationSupported));
|
||||
attributes.put("account", new AccountBean(user, profileFormData));
|
||||
|
||||
switch (page) {
|
||||
case TOTP:
|
||||
attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder()));
|
||||
break;
|
||||
case FEDERATED_IDENTITY:
|
||||
attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));
|
||||
break;
|
||||
case LOG:
|
||||
attributes.put("log", new LogBean(events));
|
||||
break;
|
||||
case SESSIONS:
|
||||
attributes.put("sessions", new SessionsBean(realm, sessions));
|
||||
break;
|
||||
case APPLICATIONS:
|
||||
attributes.put("applications", new ApplicationsBean(session, realm, user));
|
||||
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
|
||||
break;
|
||||
case PASSWORD:
|
||||
attributes.put("password", new PasswordBean(passwordSet));
|
||||
break;
|
||||
case RESOURCES:
|
||||
if (!realm.isUserManagedAccessAllowed()) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
attributes.put("authorization", new AuthorizationBean(session, realm, user, uriInfo));
|
||||
case RESOURCE_DETAIL:
|
||||
if (!realm.isUserManagedAccessAllowed()) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
attributes.put("authorization", new AuthorizationBean(session, realm, user, uriInfo));
|
||||
}
|
||||
|
||||
return processTemplate(theme, page, attributes, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Theme used for page rendering.
|
||||
*
|
||||
* @return theme for page rendering, never null
|
||||
* @throws IOException in case of Theme loading problem
|
||||
*/
|
||||
protected Theme getTheme() throws IOException {
|
||||
return session.theme().getTheme(Theme.Type.ACCOUNT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load message bundle and place it into <code>msg</code> template attribute. Also load Theme properties and place them into <code>properties</code> template attribute.
|
||||
*
|
||||
* @param theme actual Theme to load bundle from
|
||||
* @param locale to load bundle for
|
||||
* @param attributes template attributes to add resources to
|
||||
* @return message bundle for other use
|
||||
*/
|
||||
protected Properties handleThemeResources(Theme theme, Locale locale, Map<String, Object> attributes) {
|
||||
Properties messagesBundle;
|
||||
try {
|
||||
messagesBundle = theme.getEnhancedMessages(realm, locale);
|
||||
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to load messages", e);
|
||||
messagesBundle = new Properties();
|
||||
}
|
||||
try {
|
||||
attributes.put("properties", theme.getProperties());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to load properties", e);
|
||||
}
|
||||
return messagesBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages to be shown on the page - set them to template attributes
|
||||
*
|
||||
* @param locale to be used for message text loading
|
||||
* @param messagesBundle to be used for message text loading
|
||||
* @param attributes template attributes to messages related info to
|
||||
* @see #messageType
|
||||
* @see #messages
|
||||
*/
|
||||
protected void handleMessages(Locale locale, Properties messagesBundle, Map<String, Object> attributes) {
|
||||
MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean();
|
||||
if (messages != null) {
|
||||
MessageBean wholeMessage = new MessageBean(null, messageType);
|
||||
for (FormMessage message : this.messages) {
|
||||
String formattedMessageText = formatMessage(message, messagesBundle, locale);
|
||||
if (formattedMessageText != null) {
|
||||
wholeMessage.appendSummaryLine(formattedMessageText);
|
||||
messagesPerField.addMessage(message.getField(), formattedMessageText, messageType);
|
||||
}
|
||||
}
|
||||
attributes.put("message", wholeMessage);
|
||||
}
|
||||
attributes.put("messagesPerField", messagesPerField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process FreeMarker template and prepare Response. Some fields are used for rendering also.
|
||||
*
|
||||
* @param theme to be used (provided by <code>getTheme()</code>)
|
||||
* @param page to be rendered
|
||||
* @param attributes pushed to the template
|
||||
* @param locale to be used
|
||||
* @return Response object to be returned to the browser, never null
|
||||
*/
|
||||
protected Response processTemplate(Theme theme, AccountPages page, Map<String, Object> attributes, Locale locale) {
|
||||
try {
|
||||
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
|
||||
Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result);
|
||||
builder.cacheControl(CacheControlUtil.noCache());
|
||||
return builder.build();
|
||||
} catch (FreeMarkerException e) {
|
||||
logger.error("Failed to process template", e);
|
||||
return Response.serverError().build();
|
||||
}
|
||||
}
|
||||
|
||||
public AccountProvider setPasswordSet(boolean passwordSet) {
|
||||
this.passwordSet = passwordSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected void setMessage(MessageType type, String message, Object... parameters) {
|
||||
messageType = type;
|
||||
messages = new ArrayList<>();
|
||||
messages.add(new FormMessage(null, message, parameters));
|
||||
}
|
||||
|
||||
protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) {
|
||||
if (message == null)
|
||||
return null;
|
||||
if (messagesBundle.containsKey(message.getMessage())) {
|
||||
return new MessageFormat(messagesBundle.getProperty(message.getMessage()), locale).format(message.getParameters());
|
||||
} else {
|
||||
return message.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setErrors(Response.Status status, List<FormMessage> messages) {
|
||||
this.status = status;
|
||||
this.messageType = MessageType.ERROR;
|
||||
this.messages = new ArrayList<>(messages);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public AccountProvider setError(Response.Status status, String message, Object ... parameters) {
|
||||
this.status = status;
|
||||
setMessage(MessageType.ERROR, message, parameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setSuccess(String message, Object ... parameters) {
|
||||
setMessage(MessageType.SUCCESS, message, parameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setWarning(String message, Object ... parameters) {
|
||||
setMessage(MessageType.WARNING, message, parameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setUser(UserModel user) {
|
||||
this.user = user;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setProfileFormData(MultivaluedMap<String, String> formData) {
|
||||
this.profileFormData = formData;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setRealm(RealmModel realm) {
|
||||
this.realm = realm;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setReferrer(String[] referrer) {
|
||||
this.referrer = referrer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setEvents(List<Event> events) {
|
||||
this.events = events;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setSessions(List<UserSessionModel> sessions) {
|
||||
this.sessions = sessions;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setStateChecker(String stateChecker) {
|
||||
this.stateChecker = stateChecker;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setIdTokenHint(String idTokenHint) {
|
||||
this.idTokenHint = idTokenHint;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setFeatures(boolean identityProviderEnabled, boolean eventsEnabled, boolean passwordUpdateSupported, boolean authorizationSupported) {
|
||||
this.identityProviderEnabled = identityProviderEnabled;
|
||||
this.eventsEnabled = eventsEnabled;
|
||||
this.passwordUpdateSupported = passwordUpdateSupported;
|
||||
this.authorizationSupported = authorizationSupported;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setAttribute(String key, String value) {
|
||||
if (attributes == null) {
|
||||
attributes = new HashMap<>();
|
||||
}
|
||||
attributes.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.forms.account.AccountProvider;
|
||||
import org.keycloak.forms.account.AccountProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class FreeMarkerAccountProviderFactory implements AccountProviderFactory {
|
||||
|
||||
@Override
|
||||
public AccountProvider create(KeycloakSession session) {
|
||||
return new FreeMarkerAccountProvider(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "freemarker";
|
||||
}
|
||||
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker;
|
||||
|
||||
import org.keycloak.forms.account.AccountPages;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class Templates {
|
||||
|
||||
public static String getTemplate(AccountPages page) {
|
||||
switch (page) {
|
||||
case ACCOUNT:
|
||||
return "account.ftl";
|
||||
case PASSWORD:
|
||||
return "password.ftl";
|
||||
case TOTP:
|
||||
return "totp.ftl";
|
||||
case FEDERATED_IDENTITY:
|
||||
return "federatedIdentity.ftl";
|
||||
case LOG:
|
||||
return "log.ftl";
|
||||
case SESSIONS:
|
||||
return "sessions.ftl";
|
||||
case APPLICATIONS:
|
||||
return "applications.ftl";
|
||||
case RESOURCES:
|
||||
return "resources.ftl";
|
||||
case RESOURCE_DETAIL:
|
||||
return "resource-detail.ftl";
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class AccountBean {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AccountBean.class);
|
||||
|
||||
private final UserModel user;
|
||||
private final MultivaluedMap<String, String> profileFormData;
|
||||
|
||||
// TODO: More proper multi-value attribute support
|
||||
private final Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
public AccountBean(UserModel user, MultivaluedMap<String, String> profileFormData) {
|
||||
this.user = user;
|
||||
this.profileFormData = profileFormData;
|
||||
|
||||
for (Map.Entry<String, List<String>> attr : user.getAttributes().entrySet()) {
|
||||
List<String> attrValue = attr.getValue();
|
||||
if (attrValue.size() > 0) {
|
||||
attributes.put(attr.getKey(), attrValue.get(0));
|
||||
}
|
||||
|
||||
if (attrValue.size() > 1) {
|
||||
logger.warnf("There are more values for attribute '%s' of user '%s' . Will display just first value", attr.getKey(), user.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
if (profileFormData != null) {
|
||||
for (String key : profileFormData.keySet()) {
|
||||
if (key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
|
||||
String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
|
||||
attributes.put(attribute, profileFormData.getFirst(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return profileFormData != null ? profileFormData.getFirst("firstName") : user.getFirstName();
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return profileFormData != null ? profileFormData.getFirst("lastName") :user.getLastName();
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
if (profileFormData != null && profileFormData.containsKey("username")) {
|
||||
return profileFormData.getFirst("username");
|
||||
} else {
|
||||
return user.getUsername();
|
||||
}
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return profileFormData != null ? profileFormData.getFirst("email") :user.getEmail();
|
||||
}
|
||||
|
||||
public Map<String, String> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrderedModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.services.resources.account.AccountFormService;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
* @author <a href="mailto:velias@redhat.com">Vlastimil Elias</a>
|
||||
*/
|
||||
public class AccountFederatedIdentityBean {
|
||||
|
||||
private static OrderedModel.OrderedModelComparator<FederatedIdentityEntry> IDP_COMPARATOR_INSTANCE = new OrderedModel.OrderedModelComparator<>();
|
||||
|
||||
private final List<FederatedIdentityEntry> identities;
|
||||
private final boolean removeLinkPossible;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public AccountFederatedIdentityBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri, String stateChecker) {
|
||||
this.session = session;
|
||||
|
||||
AtomicInteger availableIdentities = new AtomicInteger(0);
|
||||
this.identities = realm.getIdentityProvidersStream()
|
||||
.filter(IdentityProviderModel::isEnabled)
|
||||
.map(provider -> {
|
||||
String providerId = provider.getAlias();
|
||||
|
||||
FederatedIdentityModel identity = getIdentity(session.users().getFederatedIdentitiesStream(realm, user), providerId);
|
||||
|
||||
if (identity != null) {
|
||||
availableIdentities.getAndIncrement();
|
||||
}
|
||||
|
||||
String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, provider);
|
||||
return new FederatedIdentityEntry(identity, displayName, provider.getAlias(), provider.getAlias(),
|
||||
provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null);
|
||||
})
|
||||
.sorted(IDP_COMPARATOR_INSTANCE)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Removing last social provider is not possible if you don't have other possibility to authenticate
|
||||
this.removeLinkPossible = availableIdentities.get() > 1 || user.getFederationLink() != null || AccountFormService.isPasswordSet(session, realm, user);
|
||||
}
|
||||
|
||||
private FederatedIdentityModel getIdentity(Stream<FederatedIdentityModel> identities, String providerId) {
|
||||
return identities.filter(federatedIdentityModel -> Objects.equals(federatedIdentityModel.getIdentityProvider(), providerId))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public List<FederatedIdentityEntry> getIdentities() {
|
||||
return identities;
|
||||
}
|
||||
|
||||
public boolean isRemoveLinkPossible() {
|
||||
return removeLinkPossible;
|
||||
}
|
||||
|
||||
public static class FederatedIdentityEntry implements OrderedModel {
|
||||
|
||||
private FederatedIdentityModel federatedIdentityModel;
|
||||
private final String providerId;
|
||||
private final String providerName;
|
||||
private final String guiOrder;
|
||||
private final String displayName;
|
||||
|
||||
public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String displayName, String providerId,
|
||||
String providerName, String guiOrder) {
|
||||
this.federatedIdentityModel = federatedIdentityModel;
|
||||
this.displayName = displayName;
|
||||
this.providerId = providerId;
|
||||
this.providerName = providerName;
|
||||
this.guiOrder = guiOrder;
|
||||
}
|
||||
|
||||
public String getProviderId() {
|
||||
return providerId;
|
||||
}
|
||||
|
||||
public String getProviderName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return federatedIdentityModel != null ? federatedIdentityModel.getUserId() : null;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return federatedIdentityModel != null ? federatedIdentityModel.getUserName() : null;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return federatedIdentityModel != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGuiOrder() {
|
||||
return guiOrder;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrderedModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserConsentModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
import org.keycloak.services.util.ResolveRelative;
|
||||
import org.keycloak.storage.StorageId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ApplicationsBean {
|
||||
|
||||
private List<ApplicationEntry> applications = new LinkedList<>();
|
||||
|
||||
public ApplicationsBean(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
Set<ClientModel> offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user);
|
||||
|
||||
this.applications = this.getApplications(session, realm, user)
|
||||
.filter(client -> !isAdminClient(client) || AdminPermissions.realms(session, realm, user).isAdmin())
|
||||
.map(client -> toApplicationEntry(session, realm, user, client, offlineClients))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static boolean isAdminClient(ClientModel client) {
|
||||
return client.getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID)
|
||||
|| client.getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID);
|
||||
}
|
||||
|
||||
private Stream<ClientModel> getApplications(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
Predicate<ClientModel> bearerOnly = ClientModel::isBearerOnly;
|
||||
Stream<ClientModel> clients = realm.getClientsStream().filter(bearerOnly.negate());
|
||||
|
||||
Predicate<ClientModel> isLocal = client -> new StorageId(client.getId()).isLocal();
|
||||
return Stream.concat(clients, session.users().getConsentsStream(realm, user.getId())
|
||||
.map(UserConsentModel::getClient)
|
||||
.filter(isLocal.negate())).distinct();
|
||||
}
|
||||
|
||||
private void processRoles(Set<RoleModel> inputRoles, List<RoleModel> realmRoles, MultivaluedHashMap<String, ClientRoleEntry> clientRoles) {
|
||||
for (RoleModel role : inputRoles) {
|
||||
if (role.getContainer() instanceof RealmModel) {
|
||||
realmRoles.add(role);
|
||||
} else {
|
||||
ClientModel currentClient = (ClientModel) role.getContainer();
|
||||
ClientRoleEntry clientRole = new ClientRoleEntry(currentClient.getClientId(), currentClient.getName(),
|
||||
role.getName(), role.getDescription());
|
||||
clientRoles.add(currentClient.getClientId(), clientRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<ApplicationEntry> getApplications() {
|
||||
return applications;
|
||||
}
|
||||
|
||||
public static class ApplicationEntry {
|
||||
|
||||
private KeycloakSession session;
|
||||
private final List<RoleModel> realmRolesAvailable;
|
||||
private final MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable;
|
||||
private final ClientModel client;
|
||||
private final List<String> clientScopesGranted;
|
||||
private final List<String> additionalGrants;
|
||||
|
||||
public ApplicationEntry(KeycloakSession session, List<RoleModel> realmRolesAvailable, MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable,
|
||||
ClientModel client, List<String> clientScopesGranted, List<String> additionalGrants) {
|
||||
this.session = session;
|
||||
this.realmRolesAvailable = realmRolesAvailable;
|
||||
this.resourceRolesAvailable = resourceRolesAvailable;
|
||||
this.client = client;
|
||||
this.clientScopesGranted = clientScopesGranted;
|
||||
this.additionalGrants = additionalGrants;
|
||||
}
|
||||
|
||||
public List<RoleModel> getRealmRolesAvailable() {
|
||||
return realmRolesAvailable;
|
||||
}
|
||||
|
||||
public MultivaluedHashMap<String, ClientRoleEntry> getResourceRolesAvailable() {
|
||||
return resourceRolesAvailable;
|
||||
}
|
||||
|
||||
public List<String> getClientScopesGranted() {
|
||||
return clientScopesGranted;
|
||||
}
|
||||
|
||||
public String getEffectiveUrl() {
|
||||
return ResolveRelative.resolveRelativeUri(session, getClient().getRootUrl(), getClient().getBaseUrl());
|
||||
}
|
||||
|
||||
public ClientModel getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public List<String> getAdditionalGrants() {
|
||||
return additionalGrants;
|
||||
}
|
||||
}
|
||||
|
||||
// Same class used in OAuthGrantBean as well. Maybe should be merged into common-freemarker...
|
||||
public static class ClientRoleEntry {
|
||||
|
||||
private final String clientId;
|
||||
private final String clientName;
|
||||
private final String roleName;
|
||||
private final String roleDescription;
|
||||
|
||||
public ClientRoleEntry(String clientId, String clientName, String roleName, String roleDescription) {
|
||||
this.clientId = clientId;
|
||||
this.clientName = clientName;
|
||||
this.roleName = roleName;
|
||||
this.roleDescription = roleDescription;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public String getClientName() {
|
||||
return clientName;
|
||||
}
|
||||
|
||||
public String getRoleName() {
|
||||
return roleName;
|
||||
}
|
||||
|
||||
public String getRoleDescription() {
|
||||
return roleDescription;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@link ApplicationEntry} from the specified parameters.
|
||||
*
|
||||
* @param session a reference to the {@code Keycloak} session.
|
||||
* @param realm a reference to the realm.
|
||||
* @param user a reference to the user.
|
||||
* @param client a reference to the client that contains the applications.
|
||||
* @param offlineClients a {@link Set} containing the offline clients.
|
||||
* @return the constructed {@link ApplicationEntry} instance or {@code null} if the user can't access the applications
|
||||
* in the specified client.
|
||||
*/
|
||||
private ApplicationEntry toApplicationEntry(final KeycloakSession session, final RealmModel realm, final UserModel user,
|
||||
final ClientModel client, final Set<ClientModel> offlineClients) {
|
||||
|
||||
// Construct scope parameter with all optional scopes to see all potentially available roles
|
||||
Stream<ClientScopeModel> allClientScopes = Stream.concat(
|
||||
client.getClientScopes(true).values().stream(),
|
||||
client.getClientScopes(false).values().stream());
|
||||
allClientScopes = Stream.concat(allClientScopes, Stream.of(client)).distinct();
|
||||
|
||||
Set<RoleModel> availableRoles = TokenManager.getAccess(user, client, allClientScopes);
|
||||
|
||||
// Don't show applications, which user doesn't have access into (any available roles)
|
||||
// unless this is can be changed by approving/revoking consent
|
||||
if (! isAdminClient(client) && availableRoles.isEmpty() && ! client.isConsentRequired()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<RoleModel> realmRolesAvailable = new LinkedList<>();
|
||||
MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable = new MultivaluedHashMap<>();
|
||||
processRoles(availableRoles, realmRolesAvailable, resourceRolesAvailable);
|
||||
|
||||
List<ClientScopeModel> orderedScopes = new LinkedList<>();
|
||||
if (client.isConsentRequired()) {
|
||||
UserConsentModel consent = session.users().getConsentByClient(realm, user.getId(), client.getId());
|
||||
|
||||
if (consent != null) {
|
||||
orderedScopes.addAll(consent.getGrantedClientScopes());
|
||||
}
|
||||
}
|
||||
List<String> clientScopesGranted = orderedScopes.stream()
|
||||
.sorted(OrderedModel.OrderedModelComparator.getInstance())
|
||||
.map(ClientScopeModel::getConsentScreenText)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<String> additionalGrants = new ArrayList<>();
|
||||
if (offlineClients.contains(client)) {
|
||||
additionalGrants.add("${offlineToken}");
|
||||
}
|
||||
return new ApplicationEntry(session, realmRolesAvailable, resourceRolesAvailable, client, clientScopesGranted, additionalGrants);
|
||||
}
|
||||
}
|
|
@ -1,456 +0,0 @@
|
|||
/*
|
||||
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.model.PermissionTicket;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.model.Resource;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.authorization.model.Scope;
|
||||
import org.keycloak.authorization.store.PermissionTicketStore;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||
import org.keycloak.services.util.ResolveRelative;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class AuthorizationBean {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final RealmModel realm;
|
||||
private final UserModel user;
|
||||
private final AuthorizationProvider authorization;
|
||||
private final UriInfo uriInfo;
|
||||
private ResourceBean resource;
|
||||
private List<ResourceBean> resources;
|
||||
private Collection<ResourceBean> userSharedResources;
|
||||
private Collection<ResourceBean> requestsWaitingPermission;
|
||||
private Collection<ResourceBean> resourcesWaitingOthersApproval;
|
||||
|
||||
public AuthorizationBean(KeycloakSession session, RealmModel realm, UserModel user, UriInfo uriInfo) {
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
this.user = user;
|
||||
this.uriInfo = uriInfo;
|
||||
authorization = session.getProvider(AuthorizationProvider.class);
|
||||
List<String> pathParameters = uriInfo.getPathParameters().get("resource_id");
|
||||
|
||||
if (pathParameters != null && !pathParameters.isEmpty()) {
|
||||
Resource resource = authorization.getStoreFactory().getResourceStore().findById(realm, null, pathParameters.get(0));
|
||||
|
||||
if (resource != null && !resource.getOwner().equals(user.getId())) {
|
||||
throw new RuntimeException("User [" + user.getUsername() + "] can not access resource [" + resource.getId() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<ResourceBean> getResourcesWaitingOthersApproval() {
|
||||
if (resourcesWaitingOthersApproval == null) {
|
||||
Map<PermissionTicket.FilterOption, String> filters = new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||
|
||||
filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId());
|
||||
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString());
|
||||
|
||||
resourcesWaitingOthersApproval = toResourceRepresentation(findPermissions(filters));
|
||||
}
|
||||
|
||||
return resourcesWaitingOthersApproval;
|
||||
}
|
||||
|
||||
public Collection<ResourceBean> getResourcesWaitingApproval() {
|
||||
if (requestsWaitingPermission == null) {
|
||||
Map<PermissionTicket.FilterOption, String> filters = new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||
|
||||
filters.put(PermissionTicket.FilterOption.OWNER, user.getId());
|
||||
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString());
|
||||
|
||||
requestsWaitingPermission = toResourceRepresentation(findPermissions(filters));
|
||||
}
|
||||
|
||||
return requestsWaitingPermission;
|
||||
}
|
||||
|
||||
public List<ResourceBean> getResources() {
|
||||
if (resources == null) {
|
||||
resources = authorization.getStoreFactory().getResourceStore().findByOwner(realm, null, user.getId()).stream()
|
||||
.filter(Resource::isOwnerManagedAccess)
|
||||
.map(ResourceBean::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
public Collection<ResourceBean> getSharedResources() {
|
||||
if (userSharedResources == null) {
|
||||
Map<PermissionTicket.FilterOption, String> filters = new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||
|
||||
filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId());
|
||||
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString());
|
||||
|
||||
PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore();
|
||||
|
||||
userSharedResources = toResourceRepresentation(ticketStore.find(realm,null, filters, null, null));
|
||||
}
|
||||
return userSharedResources;
|
||||
}
|
||||
|
||||
public ResourceBean getResource() {
|
||||
if (resource == null) {
|
||||
String resourceId = uriInfo.getPathParameters().getFirst("resource_id");
|
||||
|
||||
if (resourceId != null) {
|
||||
resource = getResource(resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private ResourceBean getResource(String id) {
|
||||
return new ResourceBean(authorization.getStoreFactory().getResourceStore().findById(realm, null, id));
|
||||
}
|
||||
|
||||
public static class RequesterBean {
|
||||
|
||||
private final Long createdTimestamp;
|
||||
private final Long grantedTimestamp;
|
||||
private UserModel requester;
|
||||
private List<PermissionScopeBean> scopes = new ArrayList<>();
|
||||
private boolean granted;
|
||||
|
||||
public RequesterBean(PermissionTicket ticket, AuthorizationProvider authorization) {
|
||||
this.requester = authorization.getKeycloakSession().users().getUserById(authorization.getRealm(), ticket.getRequester());
|
||||
granted = ticket.isGranted();
|
||||
createdTimestamp = ticket.getCreatedTimestamp();
|
||||
grantedTimestamp = ticket.getGrantedTimestamp();
|
||||
}
|
||||
|
||||
public UserModel getRequester() {
|
||||
return requester;
|
||||
}
|
||||
|
||||
public List<PermissionScopeBean> getScopes() {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
private void addScope(PermissionTicket ticket) {
|
||||
if (ticket != null) {
|
||||
scopes.add(new PermissionScopeBean(ticket));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isGranted() {
|
||||
return (granted && scopes.isEmpty()) || scopes.stream().filter(permissionScopeBean -> permissionScopeBean.isGranted()).count() > 0;
|
||||
}
|
||||
|
||||
public Date getCreatedDate() {
|
||||
return Time.toDate(createdTimestamp);
|
||||
}
|
||||
|
||||
public Date getGrantedDate() {
|
||||
if (grantedTimestamp == null) {
|
||||
PermissionScopeBean permission = scopes.stream().filter(permissionScopeBean -> permissionScopeBean.isGranted()).findFirst().orElse(null);
|
||||
|
||||
if (permission == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return permission.getGrantedDate();
|
||||
}
|
||||
return Time.toDate(grantedTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PermissionScopeBean {
|
||||
|
||||
private final Scope scope;
|
||||
private final PermissionTicket ticket;
|
||||
|
||||
public PermissionScopeBean(PermissionTicket ticket) {
|
||||
this.ticket = ticket;
|
||||
scope = ticket.getScope();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return ticket.getId();
|
||||
}
|
||||
|
||||
public Scope getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public boolean isGranted() {
|
||||
return ticket.isGranted();
|
||||
}
|
||||
|
||||
private Date getGrantedDate() {
|
||||
if (isGranted()) {
|
||||
return Time.toDate(ticket.getGrantedTimestamp());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class ResourceBean {
|
||||
|
||||
private final ResourceServerBean resourceServer;
|
||||
private final String ownerName;
|
||||
private final UserModel userOwner;
|
||||
private ClientModel clientOwner;
|
||||
private Resource resource;
|
||||
private Map<String, RequesterBean> permissions = new HashMap<>();
|
||||
private Collection<RequesterBean> shares;
|
||||
|
||||
public ResourceBean(Resource resource) {
|
||||
RealmModel realm = authorization.getRealm();
|
||||
ResourceServer resourceServerModel = resource.getResourceServer();
|
||||
resourceServer = new ResourceServerBean(realm.getClientById(resourceServerModel.getClientId()), resourceServerModel);
|
||||
this.resource = resource;
|
||||
userOwner = authorization.getKeycloakSession().users().getUserById(realm, resource.getOwner());
|
||||
if (userOwner == null) {
|
||||
clientOwner = realm.getClientById(resource.getOwner());
|
||||
ownerName = clientOwner.getClientId();
|
||||
} else if (userOwner.getEmail() != null) {
|
||||
ownerName = userOwner.getEmail();
|
||||
} else {
|
||||
ownerName = userOwner.getUsername();
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return resource.getId();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return resource.getName();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return resource.getDisplayName();
|
||||
}
|
||||
|
||||
public String getIconUri() {
|
||||
return resource.getIconUri();
|
||||
}
|
||||
|
||||
public String getOwnerName() {
|
||||
return ownerName;
|
||||
}
|
||||
|
||||
public UserModel getUserOwner() {
|
||||
return userOwner;
|
||||
}
|
||||
|
||||
public ClientModel getClientOwner() {
|
||||
return clientOwner;
|
||||
}
|
||||
|
||||
public List<ScopeRepresentation> getScopes() {
|
||||
return resource.getScopes().stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Collection<RequesterBean> getShares() {
|
||||
if (shares == null) {
|
||||
Map<PermissionTicket.FilterOption, String> filters = new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||
|
||||
filters.put(PermissionTicket.FilterOption.RESOURCE_ID, this.resource.getId());
|
||||
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString());
|
||||
|
||||
shares = toPermissionRepresentation(findPermissions(filters));
|
||||
}
|
||||
|
||||
return shares;
|
||||
}
|
||||
|
||||
public Collection<ManagedPermissionBean> getPolicies() {
|
||||
ResourceServer resourceServer = getResourceServer().getResourceServerModel();
|
||||
RealmModel realm = resourceServer.getRealm();
|
||||
Map<Policy.FilterOption, String[]> filters = new EnumMap<>(Policy.FilterOption.class);
|
||||
|
||||
filters.put(Policy.FilterOption.TYPE, new String[] {"uma"});
|
||||
filters.put(Policy.FilterOption.RESOURCE_ID, new String[] {this.resource.getId()});
|
||||
if (getUserOwner() != null) {
|
||||
filters.put(Policy.FilterOption.OWNER, new String[] {getUserOwner().getId()});
|
||||
} else {
|
||||
filters.put(Policy.FilterOption.OWNER, new String[] {getClientOwner().getId()});
|
||||
}
|
||||
|
||||
List<Policy> policies = authorization.getStoreFactory().getPolicyStore().find(realm, resourceServer, filters, null, null);
|
||||
|
||||
if (policies.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return policies.stream()
|
||||
.filter(policy -> {
|
||||
Map<PermissionTicket.FilterOption, String> filters1 = new EnumMap<>(PermissionTicket.FilterOption.class);
|
||||
|
||||
filters1.put(PermissionTicket.FilterOption.POLICY_ID, policy.getId());
|
||||
|
||||
return authorization.getStoreFactory().getPermissionTicketStore().find(realm, resourceServer, filters1, -1, 1)
|
||||
.isEmpty();
|
||||
})
|
||||
.map(ManagedPermissionBean::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public ResourceServerBean getResourceServer() {
|
||||
return resourceServer;
|
||||
}
|
||||
|
||||
public Collection<RequesterBean> getPermissions() {
|
||||
return permissions.values();
|
||||
}
|
||||
|
||||
private void addPermission(PermissionTicket ticket, AuthorizationProvider authorization) {
|
||||
permissions.computeIfAbsent(ticket.getRequester(), key -> new RequesterBean(ticket, authorization)).addScope(ticket);
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<RequesterBean> toPermissionRepresentation(List<PermissionTicket> permissionRequests) {
|
||||
Map<String, RequesterBean> requests = new HashMap<>();
|
||||
|
||||
for (PermissionTicket ticket : permissionRequests) {
|
||||
Resource resource = ticket.getResource();
|
||||
|
||||
if (!resource.isOwnerManagedAccess()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
requests.computeIfAbsent(ticket.getRequester(), resourceId -> new RequesterBean(ticket, authorization)).addScope(ticket);
|
||||
}
|
||||
|
||||
return requests.values();
|
||||
}
|
||||
|
||||
private Collection<ResourceBean> toResourceRepresentation(List<PermissionTicket> tickets) {
|
||||
Map<String, ResourceBean> requests = new HashMap<>();
|
||||
|
||||
for (PermissionTicket ticket : tickets) {
|
||||
Resource resource = ticket.getResource();
|
||||
|
||||
if (!resource.isOwnerManagedAccess()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
requests.computeIfAbsent(resource.getId(), resourceId -> getResource(resourceId)).addPermission(ticket, authorization);
|
||||
}
|
||||
|
||||
return requests.values();
|
||||
}
|
||||
|
||||
private List<PermissionTicket> findPermissions(Map<PermissionTicket.FilterOption, String> filters) {
|
||||
return authorization.getStoreFactory().getPermissionTicketStore().find(realm, null, filters, null, null);
|
||||
}
|
||||
|
||||
public class ResourceServerBean {
|
||||
|
||||
private ClientModel clientModel;
|
||||
private ResourceServer resourceServer;
|
||||
|
||||
public ResourceServerBean(ClientModel clientModel, ResourceServer resourceServer) {
|
||||
this.clientModel = clientModel;
|
||||
this.resourceServer = resourceServer;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return resourceServer.getId();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
String name = clientModel.getName();
|
||||
|
||||
if (name != null) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return clientModel.getClientId();
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientModel.getClientId();
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
Set<String> redirectUris = clientModel.getRedirectUris();
|
||||
|
||||
if (redirectUris.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return redirectUris.iterator().next();
|
||||
}
|
||||
|
||||
public String getBaseUri() {
|
||||
return ResolveRelative.resolveRelativeUri(session, clientModel.getRootUrl(), clientModel.getBaseUrl());
|
||||
}
|
||||
|
||||
public ResourceServer getResourceServerModel() {
|
||||
return resourceServer;
|
||||
}
|
||||
}
|
||||
|
||||
public class ManagedPermissionBean {
|
||||
|
||||
private final Policy policy;
|
||||
private List<ManagedPermissionBean> policies;
|
||||
|
||||
public ManagedPermissionBean(Policy policy) {
|
||||
this.policy = policy;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return policy.getId();
|
||||
}
|
||||
|
||||
public Collection<ScopeRepresentation> getScopes() {
|
||||
return policy.getScopes().stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.policy.getDescription();
|
||||
}
|
||||
|
||||
public Collection<ManagedPermissionBean> getPolicies() {
|
||||
if (this.policies == null) {
|
||||
this.policies = policy.getAssociatedPolicies().stream().map(ManagedPermissionBean::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return this.policies;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class FeaturesBean {
|
||||
|
||||
private final boolean identityFederation;
|
||||
private final boolean log;
|
||||
private final boolean passwordUpdateSupported;
|
||||
private boolean authorization;
|
||||
|
||||
public FeaturesBean(boolean identityFederation, boolean log, boolean passwordUpdateSupported, boolean authorization) {
|
||||
this.identityFederation = identityFederation;
|
||||
this.log = log;
|
||||
this.passwordUpdateSupported = passwordUpdateSupported;
|
||||
this.authorization = authorization;
|
||||
}
|
||||
|
||||
public boolean isIdentityFederation() {
|
||||
return identityFederation;
|
||||
}
|
||||
|
||||
public boolean isLog() {
|
||||
return log;
|
||||
}
|
||||
|
||||
public boolean isPasswordUpdateSupported() {
|
||||
return passwordUpdateSupported;
|
||||
}
|
||||
|
||||
public boolean isAuthorization() {
|
||||
return authorization;
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.keycloak.events.Event;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class LogBean {
|
||||
|
||||
private List<EventBean> events;
|
||||
|
||||
public LogBean(List<Event> events) {
|
||||
this.events = new LinkedList<EventBean>();
|
||||
for (Event e : events) {
|
||||
this.events.add(new EventBean(e));
|
||||
}
|
||||
}
|
||||
|
||||
public List<EventBean> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public static class EventBean {
|
||||
|
||||
private Event event;
|
||||
|
||||
public EventBean(Event event) {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return new Date(event.getTime());
|
||||
}
|
||||
|
||||
public String getEvent() {
|
||||
return event.getType().toString().toLowerCase().replace("_", " ");
|
||||
}
|
||||
|
||||
public String getClient() {
|
||||
return event.getClientId();
|
||||
}
|
||||
|
||||
public String getIpAddress() {
|
||||
return event.getIpAddress();
|
||||
}
|
||||
|
||||
public List<DetailBean> getDetails() {
|
||||
List<DetailBean> details = new LinkedList<DetailBean>();
|
||||
if (event.getDetails() != null) {
|
||||
for (Map.Entry<String, String> e : event.getDetails().entrySet()) {
|
||||
details.add(new DetailBean(e));
|
||||
}
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class DetailBean {
|
||||
|
||||
private Map.Entry<String, String> entry;
|
||||
|
||||
public DetailBean(Map.Entry<String, String> entry) {
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return entry.getKey();
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return entry.getValue().replace("_", " ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class PasswordBean {
|
||||
|
||||
private boolean passwordSet;
|
||||
|
||||
public PasswordBean(boolean passwordSet) {
|
||||
this.passwordSet = passwordSet;
|
||||
}
|
||||
|
||||
public boolean isPasswordSet() {
|
||||
return passwordSet;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
|
||||
*/
|
||||
public class RealmBean {
|
||||
|
||||
private RealmModel realm;
|
||||
|
||||
public RealmBean(RealmModel realmModel) {
|
||||
realm = realmModel;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return realm.getName();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
String displayName = realm.getDisplayName();
|
||||
if (displayName != null && displayName.length() > 0) {
|
||||
return displayName;
|
||||
} else {
|
||||
return getName();
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayNameHtml() {
|
||||
String displayNameHtml = realm.getDisplayNameHtml();
|
||||
if (displayNameHtml != null && displayNameHtml.length() > 0) {
|
||||
return displayNameHtml;
|
||||
} else {
|
||||
return getDisplayName();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInternationalizationEnabled() {
|
||||
return realm.isInternationalizationEnabled();
|
||||
}
|
||||
|
||||
public Set<String> getSupportedLocales(){
|
||||
return realm.getSupportedLocalesStream().collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public boolean isEditUsernameAllowed() {
|
||||
return realm.isEditUsernameAllowed();
|
||||
}
|
||||
|
||||
public boolean isRegistrationEmailAsUsername() {
|
||||
return realm.isRegistrationEmailAsUsername();
|
||||
}
|
||||
|
||||
public boolean isUserManagedAccessAllowed() {
|
||||
return realm.isUserManagedAccessAllowed();
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ReferrerBean {
|
||||
|
||||
private String[] referrer;
|
||||
|
||||
public ReferrerBean(String[] referrer) {
|
||||
this.referrer = referrer;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return referrer[0];
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return referrer[1];
|
||||
}
|
||||
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class SessionsBean {
|
||||
|
||||
private List<UserSessionBean> events;
|
||||
private RealmModel realm;
|
||||
|
||||
public SessionsBean(RealmModel realm, List<UserSessionModel> sessions) {
|
||||
this.events = new LinkedList<>();
|
||||
for (UserSessionModel session : sessions) {
|
||||
this.events.add(new UserSessionBean(realm, session));
|
||||
}
|
||||
}
|
||||
|
||||
public List<UserSessionBean> getSessions() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public static class UserSessionBean {
|
||||
|
||||
private UserSessionModel session;
|
||||
private RealmModel realm;
|
||||
|
||||
public UserSessionBean(RealmModel realm, UserSessionModel session) {
|
||||
this.realm = realm;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public String getId() {return session.getId(); }
|
||||
|
||||
public String getIpAddress() {
|
||||
return session.getIpAddress();
|
||||
}
|
||||
|
||||
public Date getStarted() {
|
||||
return Time.toDate(session.getStarted());
|
||||
}
|
||||
|
||||
public Date getLastAccess() {
|
||||
return Time.toDate(session.getLastSessionRefresh());
|
||||
}
|
||||
|
||||
public Date getExpires() {
|
||||
int maxLifespan = session.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
|
||||
int max = session.getStarted() + maxLifespan;
|
||||
return Time.toDate(max);
|
||||
}
|
||||
|
||||
public Set<String> getClients() {
|
||||
Set<String> clients = new HashSet<>();
|
||||
for (String clientUUID : session.getAuthenticatedClientSessions().keySet()) {
|
||||
ClientModel client = realm.getClientById(clientUUID);
|
||||
clients.add(client.getClientId());
|
||||
}
|
||||
return clients;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.keycloak.authentication.otp.OTPApplicationProvider;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OTPPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.models.utils.HmacOTP;
|
||||
import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.utils.TotpUtils;
|
||||
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.utils.CredentialHelper.createUserStorageCredentialRepresentation;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class TotpBean {
|
||||
|
||||
private final RealmModel realm;
|
||||
private final String totpSecret;
|
||||
private final String totpSecretEncoded;
|
||||
private final String totpSecretQrCode;
|
||||
private final boolean enabled;
|
||||
private KeycloakSession session;
|
||||
private final UriBuilder uriBuilder;
|
||||
private final List<CredentialModel> otpCredentials;
|
||||
private final List<String> supportedApplications;
|
||||
|
||||
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) {
|
||||
this.session = session;
|
||||
this.uriBuilder = uriBuilder;
|
||||
this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE);
|
||||
if (enabled) {
|
||||
List<CredentialModel> otpCredentials = user.credentialManager().getStoredCredentialsByTypeStream(OTPCredentialModel.TYPE).collect(Collectors.toList());
|
||||
|
||||
if (otpCredentials.isEmpty()) {
|
||||
// Credential is configured on userStorage side. Create the "fake" credential similar like we do for the new account console
|
||||
CredentialRepresentation credential = createUserStorageCredentialRepresentation(OTPCredentialModel.TYPE);
|
||||
this.otpCredentials = Collections.singletonList(RepresentationToModel.toModel(credential));
|
||||
} else {
|
||||
this.otpCredentials = otpCredentials;
|
||||
}
|
||||
} else {
|
||||
this.otpCredentials = Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
this.realm = realm;
|
||||
this.totpSecret = HmacOTP.generateSecret(20);
|
||||
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
||||
|
||||
OTPPolicy otpPolicy = realm.getOTPPolicy();
|
||||
this.supportedApplications = session.getAllProviders(OTPApplicationProvider.class).stream()
|
||||
.filter(p -> p.supports(otpPolicy))
|
||||
.map(OTPApplicationProvider::getName)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public String getTotpSecret() {
|
||||
return totpSecret;
|
||||
}
|
||||
|
||||
public String getTotpSecretEncoded() {
|
||||
return totpSecretEncoded;
|
||||
}
|
||||
|
||||
public String getTotpSecretQrCode() {
|
||||
return totpSecretQrCode;
|
||||
}
|
||||
|
||||
public String getManualUrl() {
|
||||
return uriBuilder.replaceQueryParam("mode", "manual").build().toString();
|
||||
}
|
||||
|
||||
public String getQrUrl() {
|
||||
return uriBuilder.replaceQueryParam("mode", "qr").build().toString();
|
||||
}
|
||||
|
||||
public OTPPolicy getPolicy() {
|
||||
return realm.getOTPPolicy();
|
||||
}
|
||||
|
||||
public List<String> getSupportedApplications() {
|
||||
return supportedApplications;
|
||||
}
|
||||
|
||||
public List<CredentialModel> getOtpCredentials() {
|
||||
return otpCredentials;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.forms.account.freemarker.model;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class UrlBean {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(UrlBean.class);
|
||||
private String realm;
|
||||
private Theme theme;
|
||||
private URI baseURI;
|
||||
private URI baseQueryURI;
|
||||
private URI currentURI;
|
||||
private String idTokenHint;
|
||||
|
||||
public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI baseQueryURI, URI currentURI, String idTokenHint) {
|
||||
this.realm = realm.getName();
|
||||
this.theme = theme;
|
||||
this.baseURI = baseURI;
|
||||
this.baseQueryURI = baseQueryURI;
|
||||
this.currentURI = currentURI;
|
||||
this.idTokenHint = idTokenHint;
|
||||
}
|
||||
|
||||
public String getApplicationsUrl() {
|
||||
return Urls.accountApplicationsPage(baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getAccountUrl() {
|
||||
return Urls.accountPage(baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getPasswordUrl() {
|
||||
return Urls.accountPasswordPage(baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getSocialUrl() {
|
||||
return Urls.accountFederatedIdentityPage(baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getTotpUrl() {
|
||||
return Urls.accountTotpPage(baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getLogUrl() {
|
||||
return Urls.accountLogPage(baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getSessionsUrl() {
|
||||
return Urls.accountSessionsPage(baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getLogoutUrl() {
|
||||
return Urls.accountLogout(baseQueryURI, currentURI, realm, idTokenHint).toString();
|
||||
}
|
||||
|
||||
public String getResourceUrl() {
|
||||
return Urls.accountResourcesPage(baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getResourceDetailUrl(String id) {
|
||||
return Urls.accountResourceDetailPage(id, baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getResourceGrant(String id) {
|
||||
return Urls.accountResourceGrant(id, baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getResourceShare(String id) {
|
||||
return Urls.accountResourceShare(id, baseQueryURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getResourcesPath() {
|
||||
URI uri = Urls.themeRoot(baseURI);
|
||||
return uri.getPath() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName();
|
||||
}
|
||||
|
||||
public String getResourcesCommonPath() {
|
||||
URI uri = Urls.themeRoot(baseURI);
|
||||
String commonPath = "";
|
||||
try {
|
||||
commonPath = theme.getProperties().getProperty("import");
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Failed to load properties", ex);
|
||||
}
|
||||
if (commonPath == null || commonPath.isEmpty()) {
|
||||
commonPath = "/common/keycloak";
|
||||
}
|
||||
return uri.getPath() + "/" + commonPath;
|
||||
}
|
||||
}
|
|
@ -16,14 +16,12 @@
|
|||
*/
|
||||
package org.keycloak.services;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.services.resources.account.AccountFormService;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
|
@ -42,34 +40,10 @@ public class Urls {
|
|||
return UriBuilder.fromUri(baseUri).path(AdminRoot.class).path("{realm}/console/").build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountApplicationsPage(URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "applicationsPage").build(realmName);
|
||||
}
|
||||
|
||||
public static UriBuilder accountBase(URI baseUri) {
|
||||
return realmBase(baseUri).path(RealmsResource.class, "getAccountService");
|
||||
}
|
||||
|
||||
public static URI accountPage(URI baseUri, String realmName) {
|
||||
return accountPageBuilder(baseUri).build(realmName);
|
||||
}
|
||||
|
||||
public static UriBuilder accountPageBuilder(URI baseUri) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "accountPage");
|
||||
}
|
||||
|
||||
public static URI accountPasswordPage(URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "passwordPage").build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountFederatedIdentityPage(URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "federatedIdentityPage").build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountFederatedIdentityUpdate(URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "processFederatedIdentityUpdate").build(realmName);
|
||||
}
|
||||
|
||||
public static URI identityProviderAuthnResponse(URI baseUri, String providerId, String realmName) {
|
||||
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||
.path(IdentityBrokerService.class, "getEndpoint")
|
||||
|
@ -129,42 +103,10 @@ public class Urls {
|
|||
.build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountTotpPage(URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "totpPage").build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountLogPage(URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "logPage").build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountSessionsPage(URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "sessionsPage").build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountLogout(URI baseUri, URI redirectUri, String realmName, String idTokenHint) {
|
||||
return realmLogout(baseUri).queryParam(OAuth2Constants.POST_LOGOUT_REDIRECT_URI, redirectUri).queryParam(OAuth2Constants.ID_TOKEN_HINT, idTokenHint).build(realmName);
|
||||
}
|
||||
|
||||
public static URI logoutConfirm(URI baseUri, String realmName) {
|
||||
return realmLogout(baseUri).path(LogoutEndpoint.class, "logoutConfirmAction").build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountResourcesPage(URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "resourcesPage").build(realmName);
|
||||
}
|
||||
|
||||
public static URI accountResourceDetailPage(String resourceId, URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "resourceDetailPage").build(realmName, resourceId);
|
||||
}
|
||||
|
||||
public static URI accountResourceGrant(String resourceId, URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "grantPermission").build(realmName, resourceId);
|
||||
}
|
||||
|
||||
public static URI accountResourceShare(String resourceId, URI baseUri, String realmName) {
|
||||
return accountBase(baseUri).path(AccountFormService.class, "shareResource").build(realmName, resourceId);
|
||||
}
|
||||
|
||||
public static URI loginActionUpdatePassword(URI baseUri, String realmName) {
|
||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "updatePassword").build(realmName);
|
||||
}
|
||||
|
@ -182,10 +124,6 @@ public class Urls {
|
|||
return loginActionsBase(baseUri).path(LoginActionsService.class, "updateProfile").build(realmName);
|
||||
}
|
||||
|
||||
public static URI loginActionEmailVerification(URI baseUri, String realmName) {
|
||||
return loginActionEmailVerificationBuilder(baseUri).build(realmName);
|
||||
}
|
||||
|
||||
public static UriBuilder loginActionEmailVerificationBuilder(URI baseUri) {
|
||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "emailVerification");
|
||||
}
|
||||
|
@ -256,10 +194,6 @@ public class Urls {
|
|||
.build(realmName);
|
||||
}
|
||||
|
||||
public static String localeCookiePath(URI baseUri, String realmName){
|
||||
return realmBase(baseUri).path(realmName).build().getRawPath();
|
||||
}
|
||||
|
||||
public static URI themeRoot(URI baseUri) {
|
||||
return themeBase(baseUri).path(Version.RESOURCES_VERSION).build();
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ import org.keycloak.services.managers.AuthenticationSessionManager;
|
|||
import org.keycloak.services.managers.BruteForceProtector;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.account.AccountFormService;
|
||||
import org.keycloak.services.resources.account.AccountConsole;
|
||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||
import org.keycloak.services.util.BrowserHistoryHelper;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
|
@ -1184,7 +1184,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
FormMessage errorMessage = new FormMessage(message, parameters);
|
||||
try {
|
||||
String serializedError = JsonSerialization.writeValueAsString(errorMessage);
|
||||
authSession.setAuthNote(AccountFormService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError);
|
||||
authSession.setAuthNote(AccountConsole.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.idm.PublishedRealmRepresentation;
|
||||
import org.keycloak.services.resources.account.AccountFormService;
|
||||
import org.keycloak.services.Urls;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.OPTIONS;
|
||||
|
@ -87,7 +87,7 @@ public class PublicRealmResource {
|
|||
PublishedRealmRepresentation rep = new PublishedRealmRepresentation();
|
||||
rep.setRealm(realm.getName());
|
||||
rep.setTokenServiceUrl(OIDCLoginProtocolService.tokenServiceBaseUrl(uriInfo).build(realm.getName()).toString());
|
||||
rep.setAccountServiceUrl(AccountFormService.accountServiceBaseUrl(uriInfo).build(realm.getName()).toString());
|
||||
rep.setAccountServiceUrl(Urls.accountBase(uriInfo.getBaseUri()).build(realm.getName()).toString());
|
||||
rep.setPublicKeyPem(PemUtils.encodeKey(session.keys().getActiveRsaKey(realm).getPublicKey()));
|
||||
rep.setNotBefore(realm.getNotBefore());
|
||||
return rep;
|
||||
|
|
|
@ -51,6 +51,9 @@ import org.keycloak.utils.MediaType;
|
|||
*/
|
||||
public class AccountConsole {
|
||||
|
||||
// Used when some other context (ie. IdentityBrokerService) wants to forward error to account management and display it here
|
||||
public static final String ACCOUNT_MGMT_FORWARDED_ERROR_NOTE = "ACCOUNT_MGMT_FORWARDED_ERROR";
|
||||
|
||||
private final Pattern bundleParamPattern = Pattern.compile("(\\{\\s*(\\d+)\\s*\\})");
|
||||
|
||||
protected final KeycloakSession session;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,6 +17,7 @@
|
|||
package org.keycloak.services.resources.account;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.http.HttpResponse;
|
||||
import org.keycloak.common.enums.AccountRestApiVersion;
|
||||
|
@ -75,23 +76,18 @@ public class AccountLoader {
|
|||
List<MediaType> accepts = headers.getAcceptableMediaTypes();
|
||||
|
||||
Theme theme = getTheme(session);
|
||||
boolean deprecatedAccount = isDeprecatedFormsAccountConsole(theme);
|
||||
UriInfo uriInfo = session.getContext().getUri();
|
||||
|
||||
if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {
|
||||
return new CorsPreflightService(request);
|
||||
} else if ((accepts.contains(MediaType.APPLICATION_JSON_TYPE) || MediaType.APPLICATION_JSON_TYPE.equals(content)) && !uriInfo.getPath().endsWith("keycloak.json")) {
|
||||
return getAccountRestService(client, null);
|
||||
} else if (Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2) || Profile.isFeatureEnabled(Profile.Feature.ACCOUNT3)) {
|
||||
AccountConsole console = new AccountConsole(session, client, theme);
|
||||
console.init();
|
||||
return console;
|
||||
} else {
|
||||
if (deprecatedAccount) {
|
||||
AccountFormService accountFormService = new AccountFormService(session, client, event);
|
||||
accountFormService.init();
|
||||
return accountFormService;
|
||||
} else {
|
||||
AccountConsole console = new AccountConsole(session, client, theme);
|
||||
console.init();
|
||||
return console;
|
||||
}
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,14 +108,6 @@ public class AccountLoader {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isDeprecatedFormsAccountConsole(Theme theme) {
|
||||
try {
|
||||
return Boolean.parseBoolean(theme.getProperties().getProperty("deprecatedMode", "true"));
|
||||
} catch (IOException e) {
|
||||
throw new InternalServerErrorException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private AccountRestService getAccountRestService(ClientModel client, String versionStr) {
|
||||
AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)
|
||||
.setAudience(client.getClientId())
|
||||
|
|
|
@ -66,12 +66,12 @@ import org.keycloak.services.ErrorResponse;
|
|||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.ForbiddenException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.BruteForceProtector;
|
||||
import org.keycloak.services.managers.UserConsentManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.resources.account.AccountFormService;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
|
@ -354,7 +354,7 @@ public class UserResource {
|
|||
userSession.setNote(IMPERSONATOR_USERNAME.toString(), impersonator);
|
||||
|
||||
AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, session.getContext().getUri(), clientConnection);
|
||||
URI redirect = AccountFormService.accountServiceBaseUrl(session.getContext().getUri()).build(realm.getName());
|
||||
URI redirect = Urls.accountBase(session.getContext().getUri().getBaseUri()).build(realm.getName());
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("sameRealm", sameRealm);
|
||||
result.put("redirect", redirect.toString());
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
#
|
||||
# Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
# and other contributors as indicated by the @author tags.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.forms.account.freemarker.FreeMarkerAccountProviderFactory
|
|
@ -19,15 +19,15 @@
|
|||
|
||||
package org.keycloak.examples.providersoverride;
|
||||
|
||||
import org.keycloak.forms.account.freemarker.FreeMarkerAccountProvider;
|
||||
import org.keycloak.email.DefaultEmailSenderProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CustomFreemarkerAccountProvider2 extends FreeMarkerAccountProvider {
|
||||
public class CustomDefaultEmailSenderProvider1 extends DefaultEmailSenderProvider {
|
||||
|
||||
public CustomFreemarkerAccountProvider2(KeycloakSession session) {
|
||||
public CustomDefaultEmailSenderProvider1(KeycloakSession session) {
|
||||
super(session);
|
||||
}
|
||||
}
|
|
@ -19,15 +19,15 @@
|
|||
|
||||
package org.keycloak.examples.providersoverride;
|
||||
|
||||
import org.keycloak.forms.account.freemarker.FreeMarkerAccountProvider;
|
||||
import org.keycloak.email.DefaultEmailSenderProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CustomFreemarkerAccountProvider1 extends FreeMarkerAccountProvider {
|
||||
public class CustomDefaultEmailSenderProvider2 extends DefaultEmailSenderProvider {
|
||||
|
||||
public CustomFreemarkerAccountProvider1(KeycloakSession session) {
|
||||
public CustomDefaultEmailSenderProvider2(KeycloakSession session) {
|
||||
super(session);
|
||||
}
|
||||
}
|
|
@ -19,14 +19,14 @@
|
|||
|
||||
package org.keycloak.examples.providersoverride;
|
||||
|
||||
import org.keycloak.forms.account.AccountProvider;
|
||||
import org.keycloak.forms.account.freemarker.FreeMarkerAccountProviderFactory;
|
||||
import org.keycloak.email.DefaultEmailSenderProviderFactory;
|
||||
import org.keycloak.email.EmailSenderProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* Won't be used due lower order than CustomFreemarkerAccountProviderFactory2
|
||||
*/
|
||||
public class CustomFreemarkerAccountProviderFactory1 extends FreeMarkerAccountProviderFactory {
|
||||
public class CustomDefaultEmailSenderProviderFactory1 extends DefaultEmailSenderProviderFactory {
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
|
@ -34,7 +34,7 @@ public class CustomFreemarkerAccountProviderFactory1 extends FreeMarkerAccountPr
|
|||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider create(KeycloakSession session) {
|
||||
return new CustomFreemarkerAccountProvider1(session);
|
||||
public EmailSenderProvider create(KeycloakSession session) {
|
||||
return new CustomDefaultEmailSenderProvider1(session);
|
||||
}
|
||||
}
|
|
@ -19,14 +19,14 @@
|
|||
|
||||
package org.keycloak.examples.providersoverride;
|
||||
|
||||
import org.keycloak.forms.account.AccountProvider;
|
||||
import org.keycloak.forms.account.freemarker.FreeMarkerAccountProviderFactory;
|
||||
import org.keycloak.email.DefaultEmailSenderProviderFactory;
|
||||
import org.keycloak.email.EmailSenderProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* Test for order (This one should be called in favour of FreemarkerAccountProviderFactory and CustomFreemarkerAccountProviderFactory1 as it has highest order)
|
||||
*/
|
||||
public class CustomFreemarkerAccountProviderFactory2 extends FreeMarkerAccountProviderFactory {
|
||||
public class CustomDefaultEmailSenderProviderFactory2 extends DefaultEmailSenderProviderFactory {
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
|
@ -34,7 +34,7 @@ public class CustomFreemarkerAccountProviderFactory2 extends FreeMarkerAccountPr
|
|||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider create(KeycloakSession session) {
|
||||
return new CustomFreemarkerAccountProvider2(session);
|
||||
public EmailSenderProvider create(KeycloakSession session) {
|
||||
return new CustomDefaultEmailSenderProvider2(session);
|
||||
}
|
||||
}
|
|
@ -17,5 +17,5 @@
|
|||
#
|
||||
#
|
||||
|
||||
org.keycloak.examples.providersoverride.CustomFreemarkerAccountProviderFactory1
|
||||
org.keycloak.examples.providersoverride.CustomFreemarkerAccountProviderFactory2
|
||||
org.keycloak.examples.providersoverride.CustomDefaultEmailSenderProviderFactory1
|
||||
org.keycloak.examples.providersoverride.CustomDefaultEmailSenderProviderFactory2
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.pages;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public abstract class AbstractAccountPage extends AbstractPage {
|
||||
|
||||
@FindBy(linkText = "Sign out")
|
||||
private WebElement logoutLink;
|
||||
|
||||
@FindBy(id = "kc-current-locale-link")
|
||||
private WebElement languageText;
|
||||
|
||||
@FindBy(id = "kc-locale-dropdown")
|
||||
private WebElement localeDropdown;
|
||||
|
||||
public void logout() {
|
||||
logoutLink.click();
|
||||
}
|
||||
|
||||
public String getLanguageDropdownText() {
|
||||
return languageText.getText();
|
||||
}
|
||||
|
||||
public void openLanguage(String language){
|
||||
WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" +language +"']"));
|
||||
String url = langLink.getAttribute("href");
|
||||
driver.navigate().to(url);
|
||||
}
|
||||
}
|
|
@ -47,7 +47,6 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
|||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.resources.account.AccountFormService;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||
import org.keycloak.testsuite.arquillian.KcArquillian;
|
||||
|
@ -723,17 +722,6 @@ public abstract class AbstractKeycloakTest {
|
|||
return log;
|
||||
}
|
||||
|
||||
protected String getAccountRedirectUrl(String realm) {
|
||||
return AccountFormService
|
||||
.loginRedirectUrl(UriBuilder.fromUri(oauth.AUTH_SERVER_ROOT))
|
||||
.build(realm)
|
||||
.toString();
|
||||
}
|
||||
|
||||
protected String getAccountRedirectUrl() {
|
||||
return getAccountRedirectUrl("test");
|
||||
}
|
||||
|
||||
protected static InputStream httpsAwareConfigurationStream(InputStream input) throws IOException {
|
||||
if (!AUTH_SERVER_SSL_REQUIRED) {
|
||||
return input;
|
||||
|
|
|
@ -59,6 +59,8 @@ import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
|||
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
|
||||
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AuthorizationEndpointResponse;
|
||||
|
@ -354,8 +356,9 @@ public class ConsentsTest extends AbstractKeycloakTest {
|
|||
RealmRepresentation providerRealmRep = providerRealm.toRepresentation();
|
||||
providerRealmRep.setAccountTheme("keycloak");
|
||||
providerRealm.update(providerRealmRep);
|
||||
providerRealm.clients().create(ClientBuilder.create().clientId("test-app").redirectUris("*").addWebOrigin("*").publicClient().build());
|
||||
|
||||
ClientRepresentation providerAccountRep = providerRealm.clients().findByClientId("account").get(0);
|
||||
ClientRepresentation providerAccountRep = providerRealm.clients().findByClientId("test-app").get(0);
|
||||
|
||||
// add offline_scope to default account-console client scope
|
||||
ClientScopeRepresentation offlineAccessScope = providerRealm.getDefaultOptionalClientScopes().stream()
|
||||
|
@ -371,7 +374,7 @@ public class ConsentsTest extends AbstractKeycloakTest {
|
|||
List<UserRepresentation> searchResult = providerRealm.users().search(getUserLogin());
|
||||
UserRepresentation user = searchResult.get(0);
|
||||
|
||||
driver.navigate().to(getAccountUrl(providerRealmName()));
|
||||
accountLoginPage.open(providerRealmName());
|
||||
|
||||
waitForPage("Sign in to provider");
|
||||
log.debug("Logging in");
|
||||
|
@ -382,8 +385,6 @@ public class ConsentsTest extends AbstractKeycloakTest {
|
|||
Assert.assertTrue(consentPage.isCurrent());
|
||||
consentPage.confirm();
|
||||
|
||||
waitForPage("keycloak account console");
|
||||
|
||||
// disable consent required again to enable direct grant token retrieval.
|
||||
providerAccountRep.setConsentRequired(false);
|
||||
providerRealm.clients().get(providerAccountRep.getId()).update(providerAccountRep);
|
||||
|
|
|
@ -18,21 +18,28 @@
|
|||
package org.keycloak.testsuite.authz;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.client.resource.PermissionResource;
|
||||
import org.keycloak.forms.account.freemarker.model.AuthorizationBean;
|
||||
import org.keycloak.forms.account.freemarker.model.AuthorizationBean.ResourceBean;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
//import org.keycloak.forms.account.freemarker.model.AuthorizationBean;
|
||||
//import org.keycloak.forms.account.freemarker.model.AuthorizationBean.ResourceBean;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.*;
|
||||
import java.util.List;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.representations.idm.authorization.DecisionEffect;
|
||||
import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.PolicyEvaluationRequest;
|
||||
import org.keycloak.representations.idm.authorization.PolicyEvaluationResponse;
|
||||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Ignore
|
||||
public class UmaRepresentationTest extends AbstractResourceServerTest {
|
||||
private ResourceRepresentation resource;
|
||||
private PermissionResource permission;
|
||||
|
@ -148,25 +155,25 @@ public class UmaRepresentationTest extends AbstractResourceServerTest {
|
|||
}
|
||||
|
||||
public static void testCanRepresentResourceBeanOfResourceOwnedByUser(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName("authz-test");
|
||||
session.getContext().setRealm(realm);
|
||||
AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
|
||||
|
||||
AuthorizationBean authorizationBean = new AuthorizationBean(session, realm, null, session.getContext().getUri());
|
||||
ClientModel client = session.getContext().getRealm().getClientByClientId("resource-server-test");
|
||||
UserModel user = session.users().getUserByUsername(session.getContext().getRealm(), "marta");
|
||||
ResourceServer resourceServer = authorization.getStoreFactory().getResourceServerStore().findByClient(client);
|
||||
ResourceBean resourceBean = authorizationBean.new ResourceBean(
|
||||
authorization.getStoreFactory().getResourceStore().findByName(
|
||||
resourceServer, "Resource A", user.getId()
|
||||
)
|
||||
);
|
||||
|
||||
Assert.assertEquals("Resource A", resourceBean.getName());
|
||||
Assert.assertEquals("marta", resourceBean.getOwnerName());
|
||||
Assert.assertNotNull(resourceBean.getUserOwner());
|
||||
Assert.assertEquals("marta", resourceBean.getUserOwner().getUsername());
|
||||
Assert.assertNull(resourceBean.getClientOwner());
|
||||
// RealmModel realm = session.realms().getRealmByName("authz-test");
|
||||
// session.getContext().setRealm(realm);
|
||||
// AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
|
||||
//
|
||||
// AuthorizationBean authorizationBean = new AuthorizationBean(session, realm, null, session.getContext().getUri());
|
||||
// ClientModel client = session.getContext().getRealm().getClientByClientId("resource-server-test");
|
||||
// UserModel user = session.users().getUserByUsername(session.getContext().getRealm(), "marta");
|
||||
// ResourceServer resourceServer = authorization.getStoreFactory().getResourceServerStore().findByClient(client);
|
||||
// ResourceBean resourceBean = authorizationBean.new ResourceBean(
|
||||
// authorization.getStoreFactory().getResourceStore().findByName(
|
||||
// resourceServer, "Resource A", user.getId()
|
||||
// )
|
||||
// );
|
||||
//
|
||||
// Assert.assertEquals("Resource A", resourceBean.getName());
|
||||
// Assert.assertEquals("marta", resourceBean.getOwnerName());
|
||||
// Assert.assertNotNull(resourceBean.getUserOwner());
|
||||
// Assert.assertEquals("marta", resourceBean.getUserOwner().getUsername());
|
||||
// Assert.assertNull(resourceBean.getClientOwner());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -176,23 +183,23 @@ public class UmaRepresentationTest extends AbstractResourceServerTest {
|
|||
}
|
||||
|
||||
public static void testCanRepresentResourceBeanOfResourceOwnedByClient(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName("authz-test");
|
||||
session.getContext().setRealm(realm);
|
||||
AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
|
||||
|
||||
AuthorizationBean authorizationBean = new AuthorizationBean(session, realm, null, session.getContext().getUri());
|
||||
ClientModel client = session.getContext().getRealm().getClientByClientId("resource-server-test");
|
||||
ResourceServer resourceServer = authorization.getStoreFactory().getResourceServerStore().findByClient(client);
|
||||
ResourceBean resourceBean = authorizationBean.new ResourceBean(
|
||||
authorization.getStoreFactory().getResourceStore().findByName(
|
||||
resourceServer, "Resource A", client.getId()
|
||||
)
|
||||
);
|
||||
|
||||
Assert.assertEquals("Resource A", resourceBean.getName());
|
||||
Assert.assertEquals("resource-server-test", resourceBean.getOwnerName());
|
||||
Assert.assertNotNull(resourceBean.getClientOwner());
|
||||
Assert.assertEquals("resource-server-test", resourceBean.getClientOwner().getClientId());
|
||||
Assert.assertNull(resourceBean.getUserOwner());
|
||||
// RealmModel realm = session.realms().getRealmByName("authz-test");
|
||||
// session.getContext().setRealm(realm);
|
||||
// AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
|
||||
//
|
||||
// AuthorizationBean authorizationBean = new AuthorizationBean(session, realm, null, session.getContext().getUri());
|
||||
// ClientModel client = session.getContext().getRealm().getClientByClientId("resource-server-test");
|
||||
// ResourceServer resourceServer = authorization.getStoreFactory().getResourceServerStore().findByClient(client);
|
||||
// ResourceBean resourceBean = authorizationBean.new ResourceBean(
|
||||
// authorization.getStoreFactory().getResourceStore().findByName(
|
||||
// resourceServer, "Resource A", client.getId()
|
||||
// )
|
||||
// );
|
||||
//
|
||||
// Assert.assertEquals("Resource A", resourceBean.getName());
|
||||
// Assert.assertEquals("resource-server-test", resourceBean.getOwnerName());
|
||||
// Assert.assertNotNull(resourceBean.getClientOwner());
|
||||
// Assert.assertEquals("resource-server-test", resourceBean.getClientOwner().getClientId());
|
||||
// Assert.assertNull(resourceBean.getUserOwner());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1127,7 +1127,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
@Test
|
||||
public void resetPasswordLinkNewTabAndProperRedirectAccount() throws IOException {
|
||||
final String REQUIRED_URI = getAuthServerRoot() + "realms/test/account/login-redirect?path=applications";
|
||||
final String REDIRECT_URI = getAccountRedirectUrl() + "?path=applications";
|
||||
final String REDIRECT_URI = getAuthServerRoot() + "realms/test/account/login-redirect?path=applications";
|
||||
final String CLIENT_ID = "account";
|
||||
final String ACCOUNT_MANAGEMENT_TITLE = "Keycloak Account Management";
|
||||
|
||||
|
|
|
@ -19,23 +19,23 @@
|
|||
|
||||
package org.keycloak.testsuite.providers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.authenticators.directgrant.ValidateOTP;
|
||||
import org.keycloak.authentication.authenticators.directgrant.ValidatePassword;
|
||||
import org.keycloak.authentication.authenticators.directgrant.ValidateUsername;
|
||||
import org.keycloak.email.EmailSenderProvider;
|
||||
import org.keycloak.examples.providersoverride.CustomDefaultEmailSenderProvider2;
|
||||
import org.keycloak.examples.providersoverride.CustomLoginFormsProvider;
|
||||
import org.keycloak.examples.providersoverride.CustomValidatePassword2;
|
||||
import org.keycloak.examples.providersoverride.CustomValidateUsername;
|
||||
import org.keycloak.forms.account.AccountProvider;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.examples.providersoverride.CustomFreemarkerAccountProvider2;
|
||||
import org.keycloak.examples.providersoverride.CustomLoginFormsProvider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Test for having multiple providerFactory of smae SPI with same providerId
|
||||
|
@ -66,7 +66,7 @@ public class ProvidersOverrideTest extends AbstractKeycloakTest {
|
|||
testProviderImplementationClass(LoginFormsProvider.class, null, CustomLoginFormsProvider.class);
|
||||
|
||||
// The provider with highest order is chosen
|
||||
testProviderImplementationClass(AccountProvider.class, null, CustomFreemarkerAccountProvider2.class);
|
||||
testProviderImplementationClass(EmailSenderProvider.class, null, CustomDefaultEmailSenderProvider2.class);
|
||||
}
|
||||
|
||||
private void testProviderImplementationClass(Class<? extends Provider> providerClass, String providerId, Class<? extends Provider> expectedProviderImplClass) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
The dependencies will be downloaded at build time, based on the contents of `package-lock.json`. You should verify the new set of packages don't break anything before committing the new `package-lock.json`.
|
||||
|
||||
## For login and old account console
|
||||
## For the login
|
||||
|
||||
```bash
|
||||
cd src/main/resources/theme/keycloak/common/resources
|
||||
|
@ -11,7 +11,7 @@ git add package-lock.json
|
|||
cd -
|
||||
```
|
||||
|
||||
## For the new account console
|
||||
## For account console v2
|
||||
|
||||
```bash
|
||||
cd src/main/resources/theme/keycloak.v2/account/src
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"types": [ "admin", "account", "login", "email" ]
|
||||
}, {
|
||||
"name" : "keycloak",
|
||||
"types": [ "account", "login", "common", "email", "welcome" ]
|
||||
"types": [ "login", "common", "email", "welcome" ]
|
||||
}, {
|
||||
"name" : "keycloak.v2",
|
||||
"types": [ "account", "admin" ]
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='account' bodyClass='user'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("editAccountHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="${url.accountUrl}" class="form-horizontal" method="post">
|
||||
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
|
||||
<#if !realm.registrationEmailAsUsername>
|
||||
<div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="username" class="control-label">${msg("username")}</label> <#if realm.editUsernameAllowed><span class="required">*</span></#if>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="username" name="username" <#if !realm.editUsernameAllowed>disabled="disabled"</#if> value="${(account.username!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('email','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="email" class="control-label">${msg("email")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="email" name="email" autofocus value="${(account.email!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('firstName','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="firstName" class="control-label">${msg("firstName")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="firstName" name="firstName" value="${(account.firstName!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('lastName','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="lastName" class="control-label">${msg("lastName")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="lastName" name="lastName" value="${(account.lastName!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<#if url.referrerURI??><a href="${url.referrerURI}">${kcSanitize(msg("backToApplication")?no_esc)}</a></#if>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,76 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='applications' bodyClass='applications'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("applicationsHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="${url.applicationsUrl}" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<input type="hidden" id="referrer" name="referrer" value="${stateChecker}">
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>${msg("application")}</td>
|
||||
<td>${msg("availableRoles")}</td>
|
||||
<td>${msg("grantedPermissions")}</td>
|
||||
<td>${msg("additionalGrants")}</td>
|
||||
<td>${msg("action")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<#list applications.applications as application>
|
||||
<tr>
|
||||
<td>
|
||||
<#if application.effectiveUrl?has_content><a href="${application.effectiveUrl}"></#if>
|
||||
<#if application.client.name?has_content>${advancedMsg(application.client.name)}<#else>${application.client.clientId}</#if>
|
||||
<#if application.effectiveUrl?has_content></a></#if>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<#list application.realmRolesAvailable as role>
|
||||
<#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)}</#if>
|
||||
<#if role_has_next>, </#if>
|
||||
</#list>
|
||||
<#list application.resourceRolesAvailable?keys as resource>
|
||||
<#if application.realmRolesAvailable?has_content>, </#if>
|
||||
<#list application.resourceRolesAvailable[resource] as clientRole>
|
||||
<#if clientRole.roleDescription??>${advancedMsg(clientRole.roleDescription)}<#else>${advancedMsg(clientRole.roleName)}</#if>
|
||||
${msg("inResource")} <strong><#if clientRole.clientName??>${advancedMsg(clientRole.clientName)}<#else>${clientRole.clientId}</#if></strong>
|
||||
<#if clientRole_has_next>, </#if>
|
||||
</#list>
|
||||
</#list>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<#if application.client.consentRequired>
|
||||
<#list application.clientScopesGranted as claim>
|
||||
${advancedMsg(claim)}<#if claim_has_next>, </#if>
|
||||
</#list>
|
||||
<#else>
|
||||
<strong>${msg("fullAccess")}</strong>
|
||||
</#if>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<#list application.additionalGrants as grant>
|
||||
${advancedMsg(grant)}<#if grant_has_next>, </#if>
|
||||
</#list>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<#if (application.client.consentRequired && application.clientScopesGranted?has_content) || application.additionalGrants?has_content>
|
||||
<button type='submit' class='${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!}' id='revoke-${application.client.clientId}' name='clientId' value="${application.client.id}">${msg("revoke")}</button>
|
||||
</#if>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,42 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='social' bodyClass='social'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("federatedIdentitiesHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="federated-identities">
|
||||
<#list federatedIdentity.identities as identity>
|
||||
<div class="row margin-bottom">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="${identity.providerId!}" class="control-label">${identity.displayName!}</label>
|
||||
</div>
|
||||
<div class="col-sm-5 col-md-5">
|
||||
<input disabled="true" class="form-control" value="${identity.userName!}">
|
||||
</div>
|
||||
<div class="col-sm-5 col-md-5">
|
||||
<#if identity.connected>
|
||||
<#if federatedIdentity.removeLinkPossible>
|
||||
<form action="${url.socialUrl}" method="post" class="form-inline">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<input type="hidden" id="action" name="action" value="remove">
|
||||
<input type="hidden" id="providerId" name="providerId" value="${identity.providerId!}">
|
||||
<button id="remove-link-${identity.providerId!}" class="btn btn-default">${msg("doRemove")}</button>
|
||||
</form>
|
||||
</#if>
|
||||
<#else>
|
||||
<form action="${url.socialUrl}" method="post" class="form-inline">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<input type="hidden" id="action" name="action" value="add">
|
||||
<input type="hidden" id="providerId" name="providerId" value="${identity.providerId!}">
|
||||
<button id="add-link-${identity.providerId!}" class="btn btn-default">${msg("doAdd")}</button>
|
||||
</form>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,35 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='log' bodyClass='log'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("accountLogHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>${msg("date")}</td>
|
||||
<td>${msg("event")}</td>
|
||||
<td>${msg("ip")}</td>
|
||||
<td>${msg("client")}</td>
|
||||
<td>${msg("details")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<#list log.events as event>
|
||||
<tr>
|
||||
<td>${event.date?datetime}</td>
|
||||
<td>${event.event}</td>
|
||||
<td>${event.ipAddress}</td>
|
||||
<td>${event.client!}</td>
|
||||
<td><#list event.details as detail>${detail.key} = ${detail.value} <#if detail_has_next>, </#if></#list></td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,59 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='password' bodyClass='password'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("changePasswordHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle">${msg("allFieldsRequired")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="${url.passwordUrl}" class="form-horizontal" method="post">
|
||||
<input type="text" id="username" name="username" value="${(account.username!'')}" autocomplete="username" readonly="readonly" style="display:none;">
|
||||
|
||||
<#if password.passwordSet>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="password" class="control-label">${msg("password")}</label>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="password" class="form-control" id="password" name="password" autofocus autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="password-new" class="control-label">${msg("passwordNew")}</label>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="password" class="form-control" id="password-new" name="password-new" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="password-confirm" class="control-label" class="two-lines">${msg("passwordConfirm")}</label>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="password" class="form-control" id="password-confirm" name="password-confirm" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,277 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='authorization' bodyClass='authorization'; section>
|
||||
|
||||
<style>
|
||||
.search-box,.close-icon,.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.search-wrapper {
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.search-box {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border: 1px solid #006e9c;
|
||||
outline: 0;
|
||||
border-radius: 15px;
|
||||
background-color: #0085cf;
|
||||
padding: 2px 5px;
|
||||
|
||||
}
|
||||
.search-box:focus {
|
||||
box-shadow: 0 0 15px 5px #b0e0ee;
|
||||
border: 2px solid #bebede;
|
||||
}
|
||||
.close-icon {
|
||||
border:1px solid transparent;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-icon:after {
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #FA9595;
|
||||
z-index:1;
|
||||
right: 35px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 0 2px #E50F0F;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-box:not(:valid) ~ .close-icon {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function removeScopeElm(elm) {
|
||||
elm.parentNode.removeChild(elm);
|
||||
}
|
||||
|
||||
function removeAllScopes(id) {
|
||||
var scopesElm = document.getElementsByName('removeScope-' + id);
|
||||
|
||||
for (i = 0; i < scopesElm.length; i++) {
|
||||
var td = scopesElm[i].parentNode.parentNode;
|
||||
var tr = td.parentNode;
|
||||
var tbody = tr.parentNode;
|
||||
tbody.removeChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function getChildren(parent, childId) {
|
||||
var childNodes = [];
|
||||
|
||||
for (i = 0; i < parent.childNodes.length; i++) {
|
||||
if (parent.childNodes[i].id == childId) {
|
||||
childNodes.push(parent.childNodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return childNodes;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>
|
||||
<a href="${url.resourceUrl}">${msg("myResources")}</a> <i class="fa fa-angle-right"></i> <#if authorization.resource.displayName??>${authorization.resource.displayName}<#else>${authorization.resource.name}</#if>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#if authorization.resource.iconUri??>
|
||||
<img src="${authorization.resource.iconUri}">
|
||||
<br/>
|
||||
</#if>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h3>
|
||||
${msg("peopleAccessResource")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${msg("user")}</th>
|
||||
<th>${msg("permission")}</th>
|
||||
<th>${msg("date")}</th>
|
||||
<th>${msg("action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#if authorization.resource.shares?size != 0>
|
||||
<#list authorization.resource.shares as permission>
|
||||
<form action="${url.getResourceGrant(authorization.resource.id)}" name="revokeForm-${authorization.resource.id}-${permission.requester.username}" method="post">
|
||||
<input type="hidden" name="action" value="revoke">
|
||||
<input type="hidden" name="requester" value="${permission.requester.username}">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<tr>
|
||||
<td>
|
||||
<#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username}</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if permission.scopes?size != 0>
|
||||
<#list permission.scopes as scope>
|
||||
<#if scope.granted && scope.scope??>
|
||||
<div class="search-box">
|
||||
<#if scope.scope.displayName??>
|
||||
${scope.scope.displayName}
|
||||
<#else>
|
||||
${scope.scope.name}
|
||||
</#if>
|
||||
<button class="close-icon" type="button" name="removeScope-${authorization.resource.id}-${permission.requester.username}" onclick="removeScopeElm(this.parentNode);document.forms['revokeForm-${authorization.resource.id}-${permission.requester.username}'].submit();"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
<input type="hidden" name="permission_id" value="${scope.id}"/>
|
||||
</div>
|
||||
<#else>
|
||||
${msg("anyPermission")}
|
||||
</#if>
|
||||
</#list>
|
||||
<#else>
|
||||
Any action
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
${permission.createdDate?datetime}
|
||||
</td>
|
||||
<td width="20%" align="middle" style="vertical-align: middle">
|
||||
<a href="#" id="revoke-${authorization.resource.name}-${permission.requester.username}" onclick="removeAllScopes('${authorization.resource.id}-${permission.requester.username}');document.forms['revokeForm-${authorization.resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-primary">${msg("doRevoke")}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</#list>
|
||||
<#else>
|
||||
<tr>
|
||||
<td colspan="4">${msg("resourceIsNotBeingShared")}</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h3>
|
||||
${msg("resourceManagedPolicies")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${msg("description")}</th>
|
||||
<th>${msg("permission")}</th>
|
||||
<th>${msg("action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#if authorization.resource.policies?size != 0>
|
||||
<#list authorization.resource.policies as permission>
|
||||
<form action="${url.getResourceGrant(authorization.resource.id)}" name="revokePolicyForm-${authorization.resource.id}-${permission.id}" method="post">
|
||||
<input type="hidden" name="action" value="revokePolicy">
|
||||
<input type="hidden" name="permission_id" value="${permission.id}"/>
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<tr>
|
||||
<td>
|
||||
<#if permission.description??>
|
||||
${permission.description}
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if permission.scopes?size != 0>
|
||||
<#list permission.scopes as scope>
|
||||
<div class="search-box">
|
||||
<#if scope.displayName??>
|
||||
${scope.displayName}
|
||||
<#else>
|
||||
${scope.name}
|
||||
</#if>
|
||||
<button class="close-icon" type="button" name="removePolicyScope-${authorization.resource.id}-${permission.id}-${scope.id}" onclick="removeScopeElm(this.parentNode);document.forms['revokePolicyForm-${authorization.resource.id}-${permission.id}'].submit();"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
<input type="hidden" name="permission_id" value="${permission.id}:${scope.id}"/>
|
||||
</div>
|
||||
</#list>
|
||||
<#else>
|
||||
${msg("anyAction")}
|
||||
</#if>
|
||||
</td>
|
||||
<td width="20%" align="middle" style="vertical-align: middle">
|
||||
<a href="#" id="revokePolicy-${authorization.resource.name}-${permission.id}" onclick="document.forms['revokePolicyForm-${authorization.resource.id}-${permission.id}']['action'].value = 'revokePolicyAll';document.forms['revokePolicyForm-${authorization.resource.id}-${permission.id}'].submit();" type="submit" class="btn btn-primary">${msg("doRevoke")}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</#list>
|
||||
<#else>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
${msg("resourceNoPermissionsGrantingAccess")}
|
||||
</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h3>
|
||||
${msg("shareWithOthers")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<form action="${url.getResourceShare(authorization.resource.id)}" name="shareForm" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<div class="col-sm-3 col-md-3">
|
||||
<label for="password" class="control-label">${msg("username")} or ${msg("email")} </label> <span class="required">*</span>
|
||||
</div>
|
||||
<div class="col-sm-8 col-md-8">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="text" class="form-control" id="user_id" name="user_id" autofocus autocomplete="off">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
<#list authorization.resource.scopes as scope>
|
||||
<div id="scope" class="search-box">
|
||||
<#if scope.displayName??>
|
||||
${scope.displayName}
|
||||
<#else>
|
||||
${scope.name}
|
||||
</#if>
|
||||
<button class="close-icon" id="share-remove-scope-${authorization.resource.name}-${scope.name}" type="button" onclick="if (getChildren(this.parentNode.parentNode, 'scope').length > 1) {removeScopeElm(this.parentNode)}"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
<input type="hidden" name="scope_id" value="${scope.id}"/>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
<a href="#" onclick="document.forms['shareForm'].submit()" type="submit" id="share-button" class="btn btn-primary">${msg("share")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
</@layout.mainLayout>
|
|
@ -1,403 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='authorization' bodyClass='authorization'; section>
|
||||
<style>
|
||||
.search-box,.close-icon,.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.search-wrapper {
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.search-box {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border: 1px solid #006e9c;
|
||||
outline: 0;
|
||||
border-radius: 15px;
|
||||
background-color: #0085cf;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.search-box:focus {
|
||||
box-shadow: 0 0 15px 5px #b0e0ee;
|
||||
border: 2px solid #bebede;
|
||||
}
|
||||
.close-icon {
|
||||
border:1px solid transparent;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-icon:after {
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #FA9595;
|
||||
z-index:1;
|
||||
right: 35px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 0 2px #E50F0F;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-box:not(:valid) ~ .close-icon {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function showHideActions(elm) {
|
||||
if (elm.style.display == 'none') {
|
||||
elm.style.display = '';
|
||||
} else {
|
||||
elm.style.display = 'none';
|
||||
}
|
||||
}
|
||||
function removeScopeElm(elm) {
|
||||
var td = elm.parentNode;
|
||||
var tr = td.parentNode;
|
||||
var tbody = tr.parentNode;
|
||||
|
||||
td.removeChild(elm);
|
||||
|
||||
var childCount = td.childNodes.length - 1;
|
||||
|
||||
for (i = 0; i < td.childNodes.length; i++) {
|
||||
if (!td.childNodes[i].tagName || td.childNodes[i].tagName.toUpperCase() != 'DIV') {
|
||||
td.removeChild(td.childNodes[i]);
|
||||
childCount--;
|
||||
}
|
||||
}
|
||||
|
||||
if (childCount <= 0) {
|
||||
tbody.removeChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllScopes(id) {
|
||||
var scopesElm = document.getElementsByName('removeScope-' + id);
|
||||
|
||||
for (i = 0; i < scopesElm.length; i++) {
|
||||
var td = scopesElm[i].parentNode.parentNode;
|
||||
var tr = td.parentNode;
|
||||
var tbody = tr.parentNode;
|
||||
tbody.removeChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllCheckBoxes(formName, elm, name) {
|
||||
var shares = document.forms[formName].getElementsByTagName('input');
|
||||
|
||||
for (i = 0; i < shares.length; i++) {
|
||||
if (shares[i].name == name) {
|
||||
shares[i].checked = elm.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>
|
||||
${msg("myResources")}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#if authorization.resourcesWaitingApproval?size != 0>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>
|
||||
${msg("needMyApproval")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${msg("resource")}</th>
|
||||
<th>${msg("requestor")}</th>
|
||||
<th>${msg("permissionRequestion")}</th>
|
||||
<th>${msg("action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list authorization.resourcesWaitingApproval as resource>
|
||||
<#list resource.permissions as permission>
|
||||
<form action="${url.getResourceGrant(resource.id)}" name="approveForm-${resource.id}-${permission.requester.username}" method="post">
|
||||
<input type="hidden" name="action" value="grant">
|
||||
<input type="hidden" name="requester" value="${permission.requester.username}">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<tr>
|
||||
<td>
|
||||
<#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username}</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#list permission.scopes as scope>
|
||||
<#if scope.scope??>
|
||||
<div class="search-box">
|
||||
<#if scope.scope.displayName??>
|
||||
${scope.scope.displayName}
|
||||
<#else>
|
||||
${scope.scope.name}
|
||||
</#if>
|
||||
<button class="close-icon" type="button" id="grant-remove-scope-${resource.name}-${permission.requester.username}-${scope.scope.name}" name="removeScope-${resource.id}-${permission.requester.username}" onclick="removeScopeElm(this.parentNode);document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'deny';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
<input type="hidden" name="permission_id" value="${scope.id}"/>
|
||||
</div>
|
||||
<#else>
|
||||
${msg("anyPermission")}
|
||||
</#if>
|
||||
</#list>
|
||||
</td>
|
||||
<td width="20%" align="middle" style="vertical-align: middle">
|
||||
<a href="#" id="grant-${resource.name}-${permission.requester.username}" onclick="document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'grant';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-primary">${msg("doApprove")}</a>
|
||||
<a href="#" id="deny-${resource.name}-${permission.requester.username}" onclick="removeAllScopes('${resource.id}-${permission.requester.username}');document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'deny';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-danger">${msg("doDeny")}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</#list>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>
|
||||
${msg("myResourcesSub")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${msg("resource")}</th>
|
||||
<th>${msg("application")}</th>
|
||||
<th>${msg("peopleSharingThisResource")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<#if authorization.resources?size != 0>
|
||||
<#list authorization.resources as resource>
|
||||
<tr>
|
||||
<td>
|
||||
<a id="detail-${resource.name}" href="${url.getResourceDetailUrl(resource.id)}">
|
||||
<#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.resourceServer.baseUri??>
|
||||
<a href="${resource.resourceServer.baseUri}">${resource.resourceServer.name}</a>
|
||||
<#else>
|
||||
${resource.resourceServer.name}
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.shares?size != 0>
|
||||
<a href="${url.getResourceDetailUrl(resource.id)}">${resource.shares?size} <i class="fa fa-users"></i></a>
|
||||
<#else>
|
||||
${msg("notBeingShared")}
|
||||
</#if>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
<#else>
|
||||
<tr>
|
||||
<td colspan="4">${msg("notHaveAnyResource")}</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>
|
||||
${msg("resourcesSharedWithMe")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form action="${url.resourceUrl}" name="shareForm" method="post">
|
||||
<input type="hidden" name="action" value="cancel"/>
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%"><input type="checkbox" onclick="selectAllCheckBoxes('shareForm', this, 'resource_id');" <#if authorization.sharedResources?size == 0>disabled="true"</#if></td>
|
||||
<th>${msg("resource")}</th>
|
||||
<th>${msg("owner")}</th>
|
||||
<th>${msg("application")}</th>
|
||||
<th>${msg("permission")}</th>
|
||||
<th>${msg("date")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#if authorization.sharedResources?size != 0>
|
||||
<#list authorization.sharedResources as resource>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="resource_id" value="${resource.id}"/>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
|
||||
</td>
|
||||
<td>
|
||||
${resource.ownerName}
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.resourceServer.baseUri??>
|
||||
<a href="${resource.resourceServer.baseUri}">${resource.resourceServer.name}</a>
|
||||
<#else>
|
||||
${resource.resourceServer.name}
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.permissions?size != 0>
|
||||
<ul>
|
||||
<#list resource.permissions as permission>
|
||||
<#list permission.scopes as scope>
|
||||
<#if scope.granted && scope.scope??>
|
||||
<li>
|
||||
<#if scope.scope.displayName??>
|
||||
${scope.scope.displayName}
|
||||
<#else>
|
||||
${scope.scope.name}
|
||||
</#if>
|
||||
</li>
|
||||
<#else>
|
||||
${msg("anyPermission")}
|
||||
</#if>
|
||||
</#list>
|
||||
</#list>
|
||||
</ul>
|
||||
<#else>
|
||||
Any action
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
${resource.permissions[0].grantedDate?datetime}
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
<#else>
|
||||
<tr>
|
||||
<td colspan="6">${msg("noResourcesSharedWithYou")}</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<#if authorization.sharedResources?size != 0>
|
||||
<div class="col-md-12">
|
||||
<a href="#" onclick="document.forms['shareForm'].submit();" type="submit" class="btn btn-danger">${msg("doRemoveSharing")}</a>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<#if authorization.resourcesWaitingOthersApproval?size != 0>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>
|
||||
${msg("requestsWaitingApproval")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<i class="pficon pficon-info"></i> ${msg("havePermissionRequestsWaitingForApproval",authorization.resourcesWaitingOthersApproval?size)}
|
||||
<a href="#" onclick="document.getElementById('waitingApproval').style.display=''">${msg("clickHereForDetails")}</a>
|
||||
<div class="row">
|
||||
<div class="col-md-12"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12"></div>
|
||||
</div>
|
||||
<div class="row" id="waitingApproval" style="display:none">
|
||||
<div class="col-md-12">
|
||||
<form action="${url.resourceUrl}" name="waitingApprovalForm" method="post">
|
||||
<input type="hidden" name="action" value="cancelRequest"/>
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%"><input type="checkbox" onclick="selectAllCheckBoxes('waitingApprovalForm', this, 'resource_id');" <#if authorization.resourcesWaitingOthersApproval?size == 0>disabled="true"</#if></th>
|
||||
<th>${msg("resource")}</th>
|
||||
<th>${msg("owner")}</th>
|
||||
<th>${msg("action")}</th>
|
||||
<th>${msg("date")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list authorization.resourcesWaitingOthersApproval as resource>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="resource_id" value="${resource.id}"/>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
|
||||
</td>
|
||||
<td>
|
||||
${resource.ownerName}
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<#list resource.permissions as permission>
|
||||
<#list permission.scopes as scope>
|
||||
<li>
|
||||
<#if scope.scope??>
|
||||
<#if scope.scope.displayName??>
|
||||
${scope.scope.displayName}
|
||||
<#else>
|
||||
${scope.scope.name}
|
||||
</#if>
|
||||
<#else>
|
||||
${msg("anyPermission")}
|
||||
</#if>
|
||||
</li>
|
||||
</#list>
|
||||
</#list>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
${resource.permissions[0].createdDate?datetime}
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<a href="#" onclick="document.forms['waitingApprovalForm'].submit();" type="submit" class="btn btn-danger">${msg("doRemoveRequest")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,44 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='sessions' bodyClass='sessions'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("sessionsHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>${msg("ip")}</td>
|
||||
<td>${msg("started")}</td>
|
||||
<td>${msg("lastAccess")}</td>
|
||||
<td>${msg("expires")}</td>
|
||||
<td>${msg("clients")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<#list sessions.sessions as session>
|
||||
<tr>
|
||||
<td>${session.ipAddress}</td>
|
||||
<td>${session.started?datetime}</td>
|
||||
<td>${session.lastAccess?datetime}</td>
|
||||
<td>${session.expires?datetime}</td>
|
||||
<td>
|
||||
<#list session.clients as client>
|
||||
${client}<br/>
|
||||
</#list>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<form action="${url.sessionsUrl}" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<button id="logout-all-sessions" class="btn btn-default">${msg("doLogOutAllSessions")}</button>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,88 +0,0 @@
|
|||
<#macro mainLayout active bodyClass>
|
||||
<!doctype html>
|
||||
<html<#if realm.internationalizationEnabled> lang="${locale.currentLanguageTag}"</#if>>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<title>${msg("accountManagementTitle")}</title>
|
||||
<link rel="icon" href="${url.resourcesPath}/img/favicon.ico">
|
||||
<#if properties.stylesCommon?has_content>
|
||||
<#list properties.stylesCommon?split(' ') as style>
|
||||
<link href="${url.resourcesCommonPath}/${style}" rel="stylesheet" />
|
||||
</#list>
|
||||
</#if>
|
||||
<#if properties.styles?has_content>
|
||||
<#list properties.styles?split(' ') as style>
|
||||
<link href="${url.resourcesPath}/${style}" rel="stylesheet" />
|
||||
</#list>
|
||||
</#if>
|
||||
<#if properties.scripts?has_content>
|
||||
<#list properties.scripts?split(' ') as script>
|
||||
<script type="text/javascript" src="${url.resourcesPath}/${script}"></script>
|
||||
</#list>
|
||||
</#if>
|
||||
</head>
|
||||
<body class="admin-console user ${bodyClass}">
|
||||
|
||||
<header class="navbar navbar-default navbar-pf navbar-main header">
|
||||
<nav class="navbar" role="navigation">
|
||||
<div class="navbar-header">
|
||||
<div class="container">
|
||||
<h1 class="navbar-title">Keycloak</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-collapse navbar-collapse-1">
|
||||
<div class="container">
|
||||
<ul class="nav navbar-nav navbar-utility">
|
||||
<#if realm.internationalizationEnabled>
|
||||
<li>
|
||||
<div class="kc-dropdown" id="kc-locale-dropdown">
|
||||
<a href="#" id="kc-current-locale-link">${locale.current}</a>
|
||||
<ul>
|
||||
<#list locale.supported as l>
|
||||
<li class="kc-dropdown-item"><a href="${l.url}">${l.label}</a></li>
|
||||
</#list>
|
||||
</ul>
|
||||
</div>
|
||||
<li>
|
||||
</#if>
|
||||
<#if referrer?has_content && referrer.url?has_content><li><a href="${referrer.url}" id="referrer">${msg("backTo",referrer.name)}</a></li></#if>
|
||||
<li><a href="${url.getLogoutUrl()}">${msg("doSignOut")}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="bs-sidebar col-sm-3">
|
||||
<ul>
|
||||
<li class="<#if active=='account'>active</#if>"><a href="${url.accountUrl}">${msg("account")}</a></li>
|
||||
<#if features.passwordUpdateSupported><li class="<#if active=='password'>active</#if>"><a href="${url.passwordUrl}">${msg("password")}</a></li></#if>
|
||||
<li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">${msg("authenticator")}</a></li>
|
||||
<#if features.identityFederation><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">${msg("federatedIdentity")}</a></li></#if>
|
||||
<li class="<#if active=='sessions'>active</#if>"><a href="${url.sessionsUrl}">${msg("sessions")}</a></li>
|
||||
<li class="<#if active=='applications'>active</#if>"><a href="${url.applicationsUrl}">${msg("applications")}</a></li>
|
||||
<#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">${msg("log")}</a></li></#if>
|
||||
<#if realm.userManagedAccessAllowed && features.authorization><li class="<#if active=='authorization'>active</#if>"><a href="${url.resourceUrl}">${msg("myResources")}</a></li></#if>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-9 content-area">
|
||||
<#if message?has_content>
|
||||
<div class="alert alert-${message.type}">
|
||||
<#if message.type=='success' ><span class="pficon pficon-ok"></span></#if>
|
||||
<#if message.type=='error' ><span class="pficon pficon-error-circle-o"></span></#if>
|
||||
<span class="kc-feedback-text">${kcSanitize(message.summary)?no_esc}</span>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<#nested "content">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</#macro>
|
|
@ -1,141 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='totp' bodyClass='totp'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("authenticatorTitle")}</h2>
|
||||
</div>
|
||||
<#if totp.otpCredentials?size == 0>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<#if totp.enabled>
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<#if totp.otpCredentials?size gt 1>
|
||||
<tr>
|
||||
<th colspan="4">${msg("configureAuthenticators")}</th>
|
||||
</tr>
|
||||
<#else>
|
||||
<tr>
|
||||
<th colspan="3">${msg("configureAuthenticators")}</th>
|
||||
</tr>
|
||||
</#if>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list totp.otpCredentials as credential>
|
||||
<tr>
|
||||
<td class="provider">${msg("mobile")}</td>
|
||||
<#if totp.otpCredentials?size gt 1>
|
||||
<td class="provider">${credential.id}</td>
|
||||
</#if>
|
||||
<td class="provider">${credential.userLabel!}</td>
|
||||
<td class="action">
|
||||
<form action="${url.totpUrl}" method="post" class="form-inline">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<input type="hidden" id="submitAction" name="submitAction" value="Delete">
|
||||
<input type="hidden" id="credentialId" name="credentialId" value="${credential.id}">
|
||||
<button id="remove-mobile" class="btn btn-default">
|
||||
<i class="pficon pficon-delete"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
<#else>
|
||||
|
||||
<hr/>
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
<p>${msg("totpStep1")}</p>
|
||||
|
||||
<ul>
|
||||
<#list totp.supportedApplications as app>
|
||||
<li>${msg(app)}</li>
|
||||
</#list>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<#if mode?? && mode = "manual">
|
||||
<li>
|
||||
<p>${msg("totpManualStep2")}</p>
|
||||
<p><span id="kc-totp-secret-key">${totp.totpSecretEncoded}</span></p>
|
||||
<p><a href="${totp.qrUrl}" id="mode-barcode">${msg("totpScanBarcode")}</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>${msg("totpManualStep3")}</p>
|
||||
<ul>
|
||||
<li id="kc-totp-type">${msg("totpType")}: ${msg("totp." + totp.policy.type)}</li>
|
||||
<li id="kc-totp-algorithm">${msg("totpAlgorithm")}: ${totp.policy.getAlgorithmKey()}</li>
|
||||
<li id="kc-totp-digits">${msg("totpDigits")}: ${totp.policy.digits}</li>
|
||||
<#if totp.policy.type = "totp">
|
||||
<li id="kc-totp-period">${msg("totpInterval")}: ${totp.policy.period}</li>
|
||||
<#elseif totp.policy.type = "hotp">
|
||||
<li id="kc-totp-counter">${msg("totpCounter")}: ${totp.policy.initialCounter}</li>
|
||||
</#if>
|
||||
</ul>
|
||||
</li>
|
||||
<#else>
|
||||
<li>
|
||||
<p>${msg("totpStep2")}</p>
|
||||
<p><img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"></p>
|
||||
<p><a href="${totp.manualUrl}" id="mode-manual">${msg("totpUnableToScan")}</a></p>
|
||||
</li>
|
||||
</#if>
|
||||
<li>
|
||||
<p>${msg("totpStep3")}</p>
|
||||
<p>${msg("totpStep3DeviceName")}</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<hr/>
|
||||
|
||||
<form action="${url.totpUrl}" class="form-horizontal" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="totp" class="control-label">${msg("authenticatorCode")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="totp" name="totp" autocomplete="off" autofocus>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value="${totp.totpSecret}"/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group" ${messagesPerField.printIfExists('userLabel',properties.kcFormGroupErrorClass!)}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="userLabel" class="control-label">${msg("totpDeviceName")}</label> <#if totp.otpCredentials?size gte 1><span class="required">*</span></#if>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="userLabel" name="userLabel" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="saveTOTPBtn" name="submitAction" value="Save">${msg("doSave")}
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="cancelTOTPBtn" name="submitAction" value="Cancel">${msg("doCancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</#if>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,277 +0,0 @@
|
|||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F9F9F9;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
header .navbar {
|
||||
margin-bottom: 0;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.header .container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
background-image: url('../img/logo.png');
|
||||
height: 25px;
|
||||
background-repeat: no-repeat;
|
||||
width: 123px;
|
||||
margin: 3px 10px 5px;
|
||||
text-indent: -99999px;
|
||||
}
|
||||
|
||||
.navbar-pf .navbar-utility {
|
||||
right: 20px;
|
||||
top: -34px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.navbar-pf .navbar-utility > li > a {
|
||||
color: #fff !important;
|
||||
padding-bottom: 12px;
|
||||
padding-top: 11px;
|
||||
border-left: medium none;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
background-color: #fff;
|
||||
border-color: #CECECE;
|
||||
border-style: solid;
|
||||
border-width: 0 1px;
|
||||
height: 100%;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.margin-bottom {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
|
||||
.bs-sidebar {
|
||||
background-color: #f9f9f9;
|
||||
padding-top: 44px;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
.bs-sidebar ul {
|
||||
list-style: none;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.bs-sidebar ul li {
|
||||
margin-bottom: 0.5em;
|
||||
margin-left: -1em;
|
||||
}
|
||||
.bs-sidebar ul li a {
|
||||
font-size: 14px;
|
||||
padding-left: 25px;
|
||||
color: #4d5258;
|
||||
line-height: 28px;
|
||||
display: block;
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
.bs-sidebar ul li a:hover,
|
||||
.bs-sidebar ul li a:focus {
|
||||
text-decoration: none;
|
||||
color: #777777;
|
||||
border-right: 2px solid #aaa;
|
||||
}
|
||||
.bs-sidebar ul li.active a {
|
||||
background-color: #c7e5f0;
|
||||
border-color: #56bae0;
|
||||
font-weight: bold;
|
||||
background-image: url(../img/icon-sidebar-active.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
}
|
||||
|
||||
.bs-sidebar ul li.active a:hover {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
|
||||
.content-area h2 {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: 100;
|
||||
font-size: 24px;
|
||||
margin-bottom: 25px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: right;
|
||||
margin-top: 30px;
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #CB2915;
|
||||
}
|
||||
|
||||
|
||||
.alert {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.feedback-aligner .alert {
|
||||
background-position: 1.27273em center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 2px;
|
||||
border-width: 1px;
|
||||
color: #4D5258;
|
||||
display: inline-block;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.4em;
|
||||
margin: 0;
|
||||
padding: 0.909091em 3.63636em;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
.alert.alert-success {
|
||||
background-color: #E4F1E1;
|
||||
border-color: #4B9E39;
|
||||
}
|
||||
.alert.alert-error {
|
||||
background-color: #F8E7E7;
|
||||
border-color: #B91415;
|
||||
}
|
||||
.alert.alert-warning {
|
||||
background-color: #FEF1E9;
|
||||
border-color: #F17528;
|
||||
}
|
||||
.alert.alert-info {
|
||||
background-color: #E4F3FA;
|
||||
border-color: #5994B2;
|
||||
}
|
||||
|
||||
.form-horizontal {
|
||||
border-top: 1px solid #E9E8E8;
|
||||
padding-top: 23px;
|
||||
}
|
||||
|
||||
.form-horizontal .control-label {
|
||||
color: #909090;
|
||||
line-height: 1.4em;
|
||||
padding-top: 5px;
|
||||
position: relative;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.control-label + .required {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#kc-form-buttons {
|
||||
text-align: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#kc-form-buttons .btn-primary {
|
||||
float: right;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Authenticator page */
|
||||
|
||||
ol {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
ol li {
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ol li img {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
hr + .form-horizontal {
|
||||
border: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.kc-dropdown{
|
||||
position: relative;
|
||||
}
|
||||
.kc-dropdown > a{
|
||||
display:block;
|
||||
padding: 11px 10px 12px;
|
||||
line-height: 12px;
|
||||
font-size: 12px;
|
||||
color: #fff !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.kc-dropdown > a::after{
|
||||
content: "\2c5";
|
||||
margin-left: 4px;
|
||||
}
|
||||
.kc-dropdown:hover > a{
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
.kc-dropdown ul li a{
|
||||
padding: 1px 11px;
|
||||
font-size: 12px;
|
||||
color: #000 !important;
|
||||
border: 1px solid #fff;
|
||||
text-decoration: none;
|
||||
display:block;
|
||||
line-height: 20px;
|
||||
}
|
||||
.kc-dropdown ul li a:hover{
|
||||
color: #4d5258;
|
||||
background-color: #d4edfa;
|
||||
border-color: #b3d3e7;
|
||||
}
|
||||
.kc-dropdown ul{
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
list-style:none;
|
||||
display:none;
|
||||
padding: 5px 0px;
|
||||
margin: 0px;
|
||||
background-color: #fff !important;
|
||||
border: 1px solid #b6b6b6;
|
||||
border-radius: 1px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-clip: padding-box;
|
||||
min-width: 100px;
|
||||
}
|
||||
.kc-dropdown:hover ul{
|
||||
display:block;
|
||||
}
|
||||
|
||||
|
||||
#kc-totp-secret-key {
|
||||
border: 1px solid #eee;
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
margin: 50px 0;
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 202 B |
Binary file not shown.
Before Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.1 KiB |
|
@ -1,14 +0,0 @@
|
|||
parent=base
|
||||
import=common/keycloak
|
||||
|
||||
styles=css/account.css
|
||||
stylesCommon=node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css
|
||||
|
||||
##### css classes for form buttons
|
||||
# main class used for all buttons
|
||||
kcButtonClass=btn
|
||||
# classes defining priority of the button - primary or default (there is typically only one priority button for the form)
|
||||
kcButtonPrimaryClass=btn-primary
|
||||
kcButtonDefaultClass=btn-default
|
||||
# classes defining size of the button
|
||||
kcButtonLargeClass=btn-lg
|
Loading…
Reference in a new issue