Skip to content

Commit 9ed84b8

Browse files
committed
Merge pull request 'Allow to use a SPeL Expression for access control and a list of users' (#56) from feature/26246_spel_auth into develop
lgtm
2 parents 89e14e8 + 97abb6b commit 9ed84b8

7 files changed

Lines changed: 555 additions & 94 deletions

File tree

Jenkinsfile

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ pipeline {
99
options {
1010
buildDiscarder(logRotator(numToKeepStr: '3'))
1111
}
12-
12+
1313
stages {
14-
14+
1515
stage('build and deploy to nexus'){
16-
16+
1717
steps {
18-
18+
1919
container('containerproxy-build') {
20-
20+
2121
configFileProvider([configFile(fileId: 'maven-settings-rsb', variable: 'MAVEN_SETTINGS_RSB')]) {
22-
23-
sh 'mvn -s $MAVEN_SETTINGS_RSB -U clean install deploy -DskipTests=true'
24-
22+
23+
sh 'mvn -B -s $MAVEN_SETTINGS_RSB -U clean install deploy -DskipTests=true'
24+
2525
}
2626
}
2727
}

src/main/java/eu/openanalytics/containerproxy/model/spec/ProxyAccessControl.java

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,55 @@
2525
public class ProxyAccessControl {
2626

2727
private String[] groups;
28+
private String[] users;
29+
private String expression;
2830

2931
public String[] getGroups() {
3032
return groups;
3133
}
3234

35+
public String[] getUsers() {
36+
return users;
37+
}
38+
39+
public String getExpression() {
40+
return expression;
41+
}
42+
3343
public void setGroups(String[] groups) {
3444
this.groups = groups;
3545
}
36-
46+
47+
public void setUsers(String[] users) {
48+
this.users = users;
49+
}
50+
51+
public void setExpression(String expression) {
52+
this.expression = expression;
53+
}
54+
3755
public void copy(ProxyAccessControl target) {
3856
if (groups != null) {
3957
target.setGroups(Arrays.copyOf(groups, groups.length));
4058
}
59+
if (users != null) {
60+
target.setUsers(Arrays.copyOf(users, users.length));
61+
}
62+
if (expression != null) {
63+
target.setExpression(expression);
64+
}
4165
}
42-
66+
67+
public boolean hasGroupAccess() {
68+
return groups != null && groups.length > 0;
69+
}
70+
71+
public boolean hasUserAccess() {
72+
return users != null && users.length > 0;
73+
}
74+
75+
public boolean hasExpressionAccess() {
76+
return expression != null && expression.length() > 0;
77+
}
78+
4379
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2021 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.service;
22+
23+
import eu.openanalytics.containerproxy.auth.IAuthenticationBackend;
24+
import eu.openanalytics.containerproxy.model.spec.ProxyAccessControl;
25+
import eu.openanalytics.containerproxy.model.spec.ProxySpec;
26+
import eu.openanalytics.containerproxy.spec.IProxySpecProvider;
27+
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionContext;
28+
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionResolver;
29+
import org.apache.commons.lang3.tuple.Pair;
30+
import org.springframework.context.annotation.Lazy;
31+
import org.springframework.context.event.EventListener;
32+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
33+
import org.springframework.security.core.Authentication;
34+
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
35+
import org.springframework.stereotype.Service;
36+
import org.springframework.web.context.request.RequestAttributes;
37+
import org.springframework.web.context.request.RequestContextHolder;
38+
39+
import java.util.Map;
40+
import java.util.Optional;
41+
import java.util.concurrent.ConcurrentHashMap;
42+
43+
@Service
44+
public class AccessControlService {
45+
46+
private final IAuthenticationBackend authBackend;
47+
private final UserService userService;
48+
private final IProxySpecProvider specProvider;
49+
private final SpecExpressionResolver specExpressionResolver;
50+
51+
/* This map is used to cache whether a user has access to an app or not.
52+
* The reason is two-fold:
53+
* - for every request made (including static files of apps etc) the access control is checked
54+
* - when using the `access-expression` feature, checking the access control means evaluating a SpEL expression
55+
* I.e. the check can be complex and is performed a lot.
56+
* This cache uses the SessionId of the user and not the userId for two reasons:
57+
* - this ensures that the key is unique
58+
* - the roles/properties of a user change when they re-login
59+
*/
60+
private final Map<Pair<String, String>, Boolean> authorizationCache = new ConcurrentHashMap<>();
61+
62+
public AccessControlService(@Lazy IAuthenticationBackend authBackend, UserService userService, IProxySpecProvider specProvider, SpecExpressionResolver specExpressionResolver) {
63+
this.authBackend = authBackend;
64+
this.userService = userService;
65+
this.specProvider = specProvider;
66+
this.specExpressionResolver = specExpressionResolver;
67+
}
68+
69+
public boolean canAccess(Authentication auth, String specId) {
70+
return canAccess(auth, specProvider.getSpec(specId));
71+
}
72+
73+
public boolean canAccess(Authentication auth, ProxySpec spec) {
74+
Optional<String> sessionId = getSessionId();
75+
if (!sessionId.isPresent()) {
76+
return checkAccess(auth, spec);
77+
}
78+
// we got a sessionId -> use the cache
79+
return authorizationCache.computeIfAbsent(
80+
Pair.of(sessionId.get(), spec.getId()),
81+
(k) -> checkAccess(auth, spec));
82+
}
83+
84+
/**
85+
* @return the sessionId if the RequestContext is present
86+
*/
87+
private Optional<String> getSessionId() {
88+
return Optional
89+
.ofNullable(RequestContextHolder.getRequestAttributes())
90+
.map(RequestAttributes::getSessionId);
91+
}
92+
93+
private boolean checkAccess(Authentication auth, ProxySpec spec) {
94+
if (auth == null || spec == null) {
95+
return false;
96+
}
97+
98+
if (auth instanceof AnonymousAuthenticationToken) {
99+
// if anonymous -> only allow access if we the backend has no authorization enabled
100+
return !authBackend.hasAuthorization();
101+
}
102+
103+
if (specHasNoAccessControl(spec.getAccessControl())) {
104+
return true;
105+
}
106+
107+
if (allowedByGroups(auth, spec)) {
108+
return true;
109+
}
110+
111+
if (allowedByUsers(auth, spec)) {
112+
return true;
113+
}
114+
115+
if (allowedByExpression(auth, spec)) {
116+
return true;
117+
}
118+
119+
return false;
120+
}
121+
122+
public boolean specHasNoAccessControl(ProxyAccessControl accessControl) {
123+
if (accessControl == null) {
124+
return true;
125+
}
126+
127+
return !accessControl.hasGroupAccess()
128+
&& !accessControl.hasUserAccess()
129+
&& !accessControl.hasExpressionAccess();
130+
}
131+
132+
public boolean allowedByGroups(Authentication auth, ProxySpec spec) {
133+
if (!spec.getAccessControl().hasGroupAccess()) {
134+
// no groups defined -> this user has no access based on the groups
135+
return false;
136+
}
137+
for (String group : spec.getAccessControl().getGroups()) {
138+
if (userService.isMember(auth, group)) {
139+
return true;
140+
}
141+
}
142+
return false;
143+
}
144+
145+
public boolean allowedByUsers(Authentication auth, ProxySpec spec) {
146+
if (!spec.getAccessControl().hasUserAccess()) {
147+
// no users defined -> this user has no access based on the users
148+
return false;
149+
}
150+
for (String user : spec.getAccessControl().getUsers()) {
151+
if (auth.getName().equals(user)) {
152+
return true;
153+
}
154+
}
155+
return false;
156+
}
157+
158+
public boolean allowedByExpression(Authentication auth, ProxySpec spec) {
159+
if (!spec.getAccessControl().hasExpressionAccess()) {
160+
// no expression defined -> this user has no access based on the expression
161+
return false;
162+
}
163+
SpecExpressionContext context = SpecExpressionContext.create(auth, auth.getPrincipal(), auth.getCredentials(), spec);
164+
return specExpressionResolver.evaluateToBoolean(spec.getAccessControl().getExpression(), context);
165+
}
166+
167+
@EventListener
168+
public void onSessionDestroyedEvent(HttpSessionDestroyedEvent event) {
169+
// remove all entries in cache for this sessionId
170+
authorizationCache.keySet().removeIf(it -> it.getLeft().equals(event.getId()));
171+
}
172+
173+
}

src/main/java/eu/openanalytics/containerproxy/service/UserService.java

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ public class UserService {
7575
@Inject
7676
private ApplicationEventPublisher applicationEventPublisher;
7777

78+
@Inject
79+
private AccessControlService accessControlService;
80+
7881
public Authentication getCurrentAuth() {
7982
return SecurityContextHolder.getContext().getAuthentication();
8083
}
@@ -103,7 +106,7 @@ public String[] getGroups() {
103106
return getGroups(getCurrentAuth());
104107
}
105108

106-
public String[] getGroups(Authentication auth) {
109+
public static String[] getGroups(Authentication auth) {
107110
List<String> groups = new ArrayList<>();
108111
if (auth != null) {
109112
for (GrantedAuthority grantedAuth: auth.getAuthorities()) {
@@ -127,23 +130,9 @@ public boolean isAdmin(Authentication auth) {
127130
}
128131

129132
public boolean canAccess(ProxySpec spec) {
130-
return canAccess(getCurrentAuth(), spec);
133+
return accessControlService.canAccess(getCurrentAuth(), spec);
131134
}
132-
133-
public boolean canAccess(Authentication auth, ProxySpec spec) {
134-
if (auth == null || spec == null) return false;
135-
if (auth instanceof AnonymousAuthenticationToken) return !authBackend.hasAuthorization();
136135

137-
if (spec.getAccessControl() == null) return true;
138-
139-
String[] groups = spec.getAccessControl().getGroups();
140-
if (groups == null || groups.length == 0) return true;
141-
for (String group: groups) {
142-
if (isMember(auth, group)) return true;
143-
}
144-
return false;
145-
}
146-
147136
public boolean isOwner(Proxy proxy) {
148137
return isOwner(getCurrentAuth(), proxy);
149138
}
@@ -153,7 +142,7 @@ public boolean isOwner(Authentication auth, Proxy proxy) {
153142
return proxy.getUserId().equals(getUserId(auth));
154143
}
155144

156-
private boolean isMember(Authentication auth, String groupName) {
145+
public boolean isMember(Authentication auth, String groupName) {
157146
if (auth == null || auth instanceof AnonymousAuthenticationToken || groupName == null) return false;
158147
for (String group: getGroups(auth)) {
159148
if (group.equalsIgnoreCase(groupName)) return true;

0 commit comments

Comments
 (0)