|
| 1 | +/** |
| 2 | + * This file is part of Graylog Archive. |
| 3 | + * |
| 4 | + * Graylog Archive is free software: you can redistribute it and/or modify |
| 5 | + * it under the terms of the GNU General Public License as published by |
| 6 | + * the Free Software Foundation, either version 3 of the License, or |
| 7 | + * (at your option) any later version. |
| 8 | + * |
| 9 | + * Graylog Archive is distributed in the hope that it will be useful, |
| 10 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | + * GNU General Public License for more details. |
| 13 | + * |
| 14 | + * You should have received a copy of the GNU General Public License |
| 15 | + * along with Graylog Archive. If not, see <http://www.gnu.org/licenses/>. |
| 16 | + */ |
| 17 | +package org.graylog.plugins.auth.sso; |
| 18 | + |
| 19 | +import org.apache.shiro.SecurityUtils; |
| 20 | +import org.apache.shiro.session.Session; |
| 21 | +import org.apache.shiro.subject.Subject; |
| 22 | +import org.graylog2.plugin.cluster.ClusterConfigService; |
| 23 | +import org.graylog2.plugin.database.users.User; |
| 24 | +import org.graylog2.security.realm.LdapUserAuthenticator; |
| 25 | +import org.graylog2.shared.users.UserService; |
| 26 | +import org.graylog2.users.RoleService; |
| 27 | +import org.slf4j.Logger; |
| 28 | +import org.slf4j.LoggerFactory; |
| 29 | + |
| 30 | +import javax.annotation.Priority; |
| 31 | +import javax.inject.Inject; |
| 32 | +import javax.ws.rs.NotAuthorizedException; |
| 33 | +import javax.ws.rs.Priorities; |
| 34 | +import javax.ws.rs.container.ContainerRequestContext; |
| 35 | +import javax.ws.rs.container.ContainerRequestFilter; |
| 36 | +import javax.ws.rs.core.Response; |
| 37 | +import java.util.List; |
| 38 | +import java.util.Optional; |
| 39 | +import java.util.Set; |
| 40 | + |
| 41 | +import static org.graylog.plugins.auth.sso.HeaderRoleUtil.*; |
| 42 | + |
| 43 | +/** |
| 44 | + * Checking the session if it still matches the user and the roles in the HTTP request SSO headers. |
| 45 | + * This is necessary as {@link SsoAuthRealm} is only called at the creation of the session. |
| 46 | + * <p> |
| 47 | + * DESIGN DECISIONS |
| 48 | + * <p> |
| 49 | + * #1 This class doesn't honor the additional trust proxies. This leads to closing more session than necessary, |
| 50 | + * but avoids duplicating the logic here with the danger of failing to call it identically. |
| 51 | + * <p> |
| 52 | + * #2 If role syncing with Graylog is activated, the first request of a session checks roles against the user database. |
| 53 | + * If this matches, the contents of the role header are cached in the session and checked again if the role headers of a request change. |
| 54 | + * If this doesn't match, the user is logged out and the session is terminated. In a SSO system this will trigger a re-login |
| 55 | + * and a re-sync of the user roles. |
| 56 | + * |
| 57 | + */ |
| 58 | +/* This needs to have a lower priority than the {@link org.graylog2.shared.security.ShiroAuthenticationFilter} |
| 59 | + * so that the {@link Subject} is already populated. */ |
| 60 | +@Priority(Priorities.AUTHENTICATION + 1) |
| 61 | +public class SsoAuthenticationFilter implements ContainerRequestFilter { |
| 62 | + private static final Logger LOG = LoggerFactory.getLogger(SsoAuthenticationFilter.class); |
| 63 | + private static final String VERIFIED_ROLES = SsoAuthenticationFilter.class.getName() + ".VERIFIED_USERS"; |
| 64 | + |
| 65 | + private final ClusterConfigService clusterConfigService; |
| 66 | + private final RoleService roleService; |
| 67 | + private final UserService userService; |
| 68 | + private final LdapUserAuthenticator ldapAuthenticator; |
| 69 | + |
| 70 | + @Inject |
| 71 | + public SsoAuthenticationFilter(ClusterConfigService clusterConfigService, RoleService roleService, UserService userService, LdapUserAuthenticator ldapAuthenticator) { |
| 72 | + this.clusterConfigService = clusterConfigService; |
| 73 | + this.roleService = roleService; |
| 74 | + this.userService = userService; |
| 75 | + this.ldapAuthenticator = ldapAuthenticator; |
| 76 | + } |
| 77 | + |
| 78 | + @Override |
| 79 | + public void filter(ContainerRequestContext containerRequestContext) { |
| 80 | + |
| 81 | + final SsoAuthConfig config = clusterConfigService.getOrDefault( |
| 82 | + SsoAuthConfig.class, |
| 83 | + SsoAuthConfig.defaultConfig("")); |
| 84 | + |
| 85 | + Subject subject = SecurityUtils.getSubject(); |
| 86 | + // the subject needs to be inspected only if there is a session. |
| 87 | + // If there is no session, Shiro has checked the headers on this request already |
| 88 | + if (subject.getSession(false) != null) { |
| 89 | + Session session = subject.getSession(); |
| 90 | + final String usernameHeader = config.usernameHeader(); |
| 91 | + final Optional<String> userNameOption = headerValue(containerRequestContext.getHeaders(), usernameHeader); |
| 92 | + // is the header with the user name is present ... |
| 93 | + if (userNameOption.isPresent()) { |
| 94 | + String username = userNameOption.get(); |
| 95 | + if (!username.equals(subject.getPrincipal())) { |
| 96 | + // terminate the session and return "unauthorized" for this request |
| 97 | + LOG.warn("terminating session of {} as new user {} appears in the header", subject.getPrincipal(), username); |
| 98 | + subject.logout(); |
| 99 | + throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); |
| 100 | + } |
| 101 | + if (config.syncRoles()) { |
| 102 | + Optional<List<String>> rolesList = headerValues(containerRequestContext.getHeaders(), config.rolesHeader()); |
| 103 | + if (rolesList.isPresent() && !rolesList.get().equals(session.getAttribute(VERIFIED_ROLES))) { |
| 104 | + User user = null; |
| 105 | + if (ldapAuthenticator.isEnabled()) { |
| 106 | + user = ldapAuthenticator.syncLdapUser(username); |
| 107 | + } |
| 108 | + if (user == null) { |
| 109 | + user = userService.load(username); |
| 110 | + } |
| 111 | + if (user == null) { |
| 112 | + LOG.error("user {} not found", |
| 113 | + subject.getPrincipal()); |
| 114 | + subject.logout(); |
| 115 | + throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); |
| 116 | + } |
| 117 | + Set<String> roleNames = csv(rolesList.get()); |
| 118 | + Set<String> existingRoles = user.getRoleIds(); |
| 119 | + |
| 120 | + Set<String> roleIds = getRoleIds(roleService, roleNames); |
| 121 | + if (!existingRoles.equals(roleIds)) { |
| 122 | + // terminate the session and return "unauthorized" for this request |
| 123 | + LOG.warn("terminating session of user {} as roles in user differ from roles in header ({})", |
| 124 | + subject.getPrincipal(), |
| 125 | + roleNames); |
| 126 | + subject.logout(); |
| 127 | + throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); |
| 128 | + } |
| 129 | + session.setAttribute(VERIFIED_ROLES, rolesList.get()); |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | +} |
0 commit comments