Skip to content
This repository was archived by the owner on May 27, 2024. It is now read-only.

Commit 8e2fe07

Browse files
committed
adding a filter to check if SSO headers while session is active
1 parent 6666c7d commit 8e2fe07

File tree

8 files changed

+550
-66
lines changed

8 files changed

+550
-66
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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.graylog2.database.NotFoundException;
20+
import org.graylog2.shared.users.Role;
21+
import org.graylog2.users.RoleService;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
import javax.annotation.Nullable;
26+
import javax.validation.constraints.NotNull;
27+
import javax.ws.rs.core.MultivaluedMap;
28+
import java.util.HashSet;
29+
import java.util.List;
30+
import java.util.Optional;
31+
import java.util.Set;
32+
import java.util.stream.Collectors;
33+
34+
/**
35+
* Encapsulating common header and role parsing.
36+
*/
37+
abstract class HeaderRoleUtil {
38+
private static final Logger LOG = LoggerFactory.getLogger(HeaderRoleUtil.class);
39+
40+
static Optional<String> headerValue(MultivaluedMap<String, String> headers, @Nullable String headerName) {
41+
if (headerName == null) {
42+
return Optional.empty();
43+
}
44+
return Optional.ofNullable(headers.getFirst(headerName.toLowerCase()));
45+
}
46+
47+
static Optional<List<String>> headerValues(MultivaluedMap<String, String> headers,
48+
@Nullable String headerNamePrefix) {
49+
if (headerNamePrefix == null) {
50+
return Optional.empty();
51+
}
52+
Set<String> keys = headers.keySet();
53+
List<String> headerValues = keys.stream().filter(key -> key.startsWith(headerNamePrefix.toLowerCase()))
54+
.map(key -> headers.getFirst(key)).collect(Collectors.toList());
55+
56+
return Optional.ofNullable(headerValues);
57+
}
58+
59+
static @NotNull Set<String> csv(@NotNull List<String> values) {
60+
Set<String> uniqValues = new HashSet<>();
61+
for (String csString : values) {
62+
String[] valueArr = csString.split(",");
63+
64+
for (String value : valueArr) {
65+
uniqValues.add(value.trim());
66+
}
67+
}
68+
return uniqValues;
69+
}
70+
71+
static @NotNull Set<String> getRoleIds(RoleService roleService, @NotNull Set<String> roleNames) {
72+
Set<String> roleIds = new HashSet<>();
73+
for (String roleName : roleNames) {
74+
if (roleService.exists(roleName)) {
75+
try {
76+
Role r = roleService.load(roleName);
77+
roleIds.add(r.getId());
78+
} catch (NotFoundException e) {
79+
LOG.error("Role {} not found, but it existed before", roleName);
80+
}
81+
}
82+
}
83+
return roleIds;
84+
}
85+
86+
}

src/main/java/org/graylog/plugins/auth/sso/SsoAuthModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
package org.graylog.plugins.auth.sso;
1818

1919
import com.google.inject.Scopes;
20+
import com.google.inject.multibindings.Multibinder;
2021
import org.graylog.plugins.auth.sso.audit.SsoAuthAuditEventTypes;
2122
import org.graylog2.plugin.PluginModule;
2223

24+
import javax.ws.rs.container.DynamicFeature;
25+
2326
/**
2427
* Extend the PluginModule abstract class here to add you plugin to the system.
2528
*/
@@ -31,5 +34,8 @@ protected void configure() {
3134
addRestResource(SsoConfigResource.class);
3235
addPermissions(SsoAuthPermissions.class);
3336
addAuditEventTypes(SsoAuthAuditEventTypes.class);
37+
38+
Multibinder<Class<? extends DynamicFeature>> setBinder = jerseyDynamicFeatureBinder();
39+
setBinder.addBinding().toInstance(SsoSecurityBinding.class);
3440
}
3541
}

src/main/java/org/graylog/plugins/auth/sso/SsoAuthRealm.java

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,16 @@
3838
import org.slf4j.Logger;
3939
import org.slf4j.LoggerFactory;
4040

41-
import javax.annotation.Nullable;
4241
import javax.inject.Inject;
4342
import javax.inject.Named;
4443
import javax.ws.rs.core.MultivaluedMap;
4544
import java.net.UnknownHostException;
4645
import java.util.Collections;
47-
import java.util.HashSet;
4846
import java.util.List;
4947
import java.util.Optional;
5048
import java.util.Set;
51-
import java.util.stream.Collectors;
49+
50+
import static org.graylog.plugins.auth.sso.HeaderRoleUtil.*;
5251

5352
public class SsoAuthRealm extends AuthenticatingRealm {
5453
private static final Logger LOG = LoggerFactory.getLogger(SsoAuthRealm.class);
@@ -187,34 +186,13 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
187186
return null;
188187
}
189188

190-
protected Set<String> csv(List<String> values) {
191-
Set<String> uniqValues = new HashSet<>();
192-
for (String csString : values) {
193-
String[] valueArr = csString.split(",");
194-
195-
for (String value : valueArr) {
196-
uniqValues.add(value.trim());
197-
}
198-
}
199-
return uniqValues;
200-
}
201-
202189
protected void syncUserRoles(List<String> roleCsv, User user) throws ValidationException {
203190
Set<String> roleNames = csv(roleCsv);
204191
Set<String> existingRoles = user.getRoleIds();
205192

206-
Set<String> syncedRoles = new HashSet<>();
207-
for (String roleName : roleNames) {
208-
if (roleService.exists(roleName)) {
209-
try {
210-
Role r = roleService.load(roleName);
211-
syncedRoles.add(r.getId());
212-
} catch (NotFoundException e) {
213-
LOG.error("Role {} not found, but it existed before", roleName);
214-
}
215-
}
216-
}
217-
if (existingRoles != null && !existingRoles.equals(syncedRoles)) {
193+
Set<String> syncedRoles = getRoleIds(roleService, roleNames);
194+
195+
if (!existingRoles.equals(syncedRoles)) {
218196
user.setRoleIds(syncedRoles);
219197
userService.save(user);
220198
}
@@ -234,22 +212,4 @@ boolean inTrustedSubnets(String remoteAddr) {
234212
});
235213
}
236214

237-
private Optional<String> headerValue(MultivaluedMap<String, String> headers, @Nullable String headerName) {
238-
if (headerName == null) {
239-
return Optional.empty();
240-
}
241-
return Optional.ofNullable(headers.getFirst(headerName.toLowerCase()));
242-
}
243-
244-
protected Optional<List<String>> headerValues(MultivaluedMap<String, String> headers,
245-
@Nullable String headerNamePrefix) {
246-
if (headerNamePrefix == null) {
247-
return Optional.empty();
248-
}
249-
Set<String> keys = headers.keySet();
250-
List<String> headerValues = keys.stream().filter(key -> key.startsWith(headerNamePrefix.toLowerCase()))
251-
.map(key -> headers.getFirst(key)).collect(Collectors.toList());
252-
253-
return Optional.ofNullable(headerValues);
254-
}
255215
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* This file is part of Graylog.
3+
*
4+
* Graylog 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 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. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package org.graylog.plugins.auth.sso;
18+
19+
import org.apache.shiro.authz.annotation.RequiresAuthentication;
20+
import org.apache.shiro.authz.annotation.RequiresGuest;
21+
import org.graylog2.shared.security.ShiroSecurityBinding;
22+
import org.graylog2.shared.security.ShiroSecurityContextFilter;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
import javax.ws.rs.container.DynamicFeature;
27+
import javax.ws.rs.container.ResourceInfo;
28+
import javax.ws.rs.core.FeatureContext;
29+
import java.lang.reflect.Method;
30+
31+
/**
32+
* Adding the {@link SsoAuthenticationFilter} to each resource that requires authentication.
33+
* This is mirroring the efforts in {@link ShiroSecurityBinding}
34+
*/
35+
public class SsoSecurityBinding implements DynamicFeature {
36+
private static final Logger LOG = LoggerFactory.getLogger(SsoSecurityBinding.class);
37+
38+
@Override
39+
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
40+
final Class<?> resourceClass = resourceInfo.getResourceClass();
41+
final Method resourceMethod = resourceInfo.getResourceMethod();
42+
43+
context.register(ShiroSecurityContextFilter.class);
44+
45+
if (resourceMethod.isAnnotationPresent(RequiresAuthentication.class) || resourceClass.isAnnotationPresent(RequiresAuthentication.class)) {
46+
if (resourceMethod.isAnnotationPresent(RequiresGuest.class)) {
47+
LOG.debug("Resource method {}#{} is marked as unauthenticated, skipping setting filter.", resourceClass.getCanonicalName(), resourceMethod.getName());
48+
} else {
49+
LOG.debug("Resource method {}#{} requires an authenticated user.", resourceClass.getCanonicalName(), resourceMethod.getName());
50+
context.register(SsoAuthenticationFilter.class);
51+
}
52+
}
53+
54+
}
55+
}

0 commit comments

Comments
 (0)