Skip to content

Commit c66667a

Browse files
authored
Merge pull request #753 from apache/master
[pull] master from apache:master
2 parents 8b812a0 + 218f30f commit c66667a

15 files changed

Lines changed: 204 additions & 31 deletions

File tree

docs/modules/servers/partials/configure/imap.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ Whether to enable Authentication PLAIN if the connection is not encrypted via SS
6262
| auth.oidc.scope
6363
| An OAuth scope that is valid to access the service (RF: RFC7628). Only configure this when you want to authenticate IMAP server using a OIDC provider.
6464

65+
| auth.adminUsers.adminUser
66+
| XML list of administrators able to impersonate any IMAP users. This is typically enable on non publicly exposed IMAP servers eg port 994 for admin support or migration.
67+
6568
| timeout
6669
| Default to 30 minutes. After this time, inactive channels that have not performed read, write, or both operation for a while
6770
will be closed. Negative value disable this behaviour.

docs/modules/servers/partials/configure/usersrepository.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ acting on the behalf of any user.
3636
| administratorIds
3737
| List of usernames. Allows multiple administrators to access the https://tools.ietf.org/html/rfc4616#section-2[impersonation command],
3838
acting on behalf of any user by specifying multiple `<administratorId>` entries inside the `<administratorIds>` block.
39+
Prefer ref:imap.adoc[imapserver.xml] `auth.adminUsers` property to not
40+
enable admin impersonation onto publicly exposed interfaces.
3941

4042
Notes: Only one of the above `<administratorId>` property or this `<administratorIds>` block should be used to specify the administrator(s).
4143

mailbox/api/src/main/java/org/apache/james/mailbox/Authorizator.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,17 @@ interface FluentAuthorizator {
3838
enum AuthorizationState {
3939
ALLOWED,
4040
FORBIDDEN,
41-
UNKNOWN_USER
41+
UNKNOWN_USER;
42+
43+
static AuthorizationState combine(AuthorizationState a, AuthorizationState b) {
44+
if (a == AuthorizationState.FORBIDDEN) {
45+
return b;
46+
}
47+
if (a == UNKNOWN_USER && b == ALLOWED) {
48+
return b;
49+
}
50+
return a;
51+
}
4252
}
4353

4454
AuthorizationState canLoginAsOtherUser(Username userId, Username otherUserId) throws MailboxException;
@@ -50,5 +60,11 @@ default FluentAuthorizator user(Username userId) {
5060
default Collection<Username> delegatedUsers(Username username) {
5161
return ImmutableList.of();
5262
}
63+
64+
static Authorizator combine(Authorizator a, Authorizator b) {
65+
return (userA, userB) -> AuthorizationState.combine(
66+
a.canLoginAsOtherUser(userA, userB),
67+
b.canLoginAsOtherUser(userA, userB));
68+
}
5369
}
5470

mailbox/api/src/main/java/org/apache/james/mailbox/SessionProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,6 @@ interface AuthorizationStep {
7575
* when the creation fails for other reasons
7676
*/
7777
AuthorizationStep authenticate(Username givenUserid);
78+
79+
SessionProvider withExtraAuthorizator(Authorizator authorizator);
7880
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/******************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one *
3+
* or more contributor license agreements. See the NOTICE file *
4+
* distributed with this work for additional information *
5+
* regarding copyright ownership. The ASF licenses this file *
6+
* to you under the Apache License, Version 2.0 (the *
7+
* "License"); you may not use this file except in compliance *
8+
* with the License. You may obtain a copy of the License at *
9+
* *
10+
* http://www.apache.org/licenses/LICENSE-2.0 *
11+
* *
12+
* Unless required by applicable law or agreed to in writing, *
13+
* software distributed under the License is distributed on an *
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15+
* KIND, either express or implied. See the License for the *
16+
* specific language governing permissions and limitations *
17+
* under the License. *
18+
******************************************************************/
19+
20+
package org.apache.james.mailbox;
21+
22+
import static org.apache.james.mailbox.Authorizator.AuthorizationState.ALLOWED;
23+
import static org.apache.james.mailbox.Authorizator.AuthorizationState.FORBIDDEN;
24+
import static org.apache.james.mailbox.Authorizator.AuthorizationState.UNKNOWN_USER;
25+
26+
import org.apache.james.mailbox.Authorizator.AuthorizationState;
27+
import org.assertj.core.api.SoftAssertions;
28+
import org.junit.jupiter.api.Test;
29+
30+
class AuthorizatorTest {
31+
@Test
32+
void combineShouldAggregateResults() {
33+
SoftAssertions.assertSoftly(softly -> {
34+
softly.assertThat(AuthorizationState.combine(ALLOWED, ALLOWED)).isEqualTo(ALLOWED);
35+
softly.assertThat(AuthorizationState.combine(ALLOWED, FORBIDDEN)).isEqualTo(ALLOWED);
36+
softly.assertThat(AuthorizationState.combine(FORBIDDEN, ALLOWED)).isEqualTo(ALLOWED);
37+
softly.assertThat(AuthorizationState.combine(FORBIDDEN, FORBIDDEN)).isEqualTo(FORBIDDEN);
38+
softly.assertThat(AuthorizationState.combine(UNKNOWN_USER, FORBIDDEN)).isEqualTo(UNKNOWN_USER);
39+
softly.assertThat(AuthorizationState.combine(FORBIDDEN, UNKNOWN_USER)).isEqualTo(UNKNOWN_USER);
40+
softly.assertThat(AuthorizationState.combine(UNKNOWN_USER, UNKNOWN_USER)).isEqualTo(UNKNOWN_USER);
41+
softly.assertThat(AuthorizationState.combine(UNKNOWN_USER, ALLOWED)).isEqualTo(ALLOWED);
42+
softly.assertThat(AuthorizationState.combine(ALLOWED, UNKNOWN_USER)).isEqualTo(ALLOWED);
43+
});
44+
}
45+
}

mailbox/store/src/main/java/org/apache/james/mailbox/store/SessionProviderImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,9 @@ private long randomId() {
148148
private Optional<Username> isValidLogin(Username userid, String passwd) throws MailboxException {
149149
return authenticator.isAuthentic(userid, passwd);
150150
}
151+
152+
@Override
153+
public SessionProvider withExtraAuthorizator(Authorizator authorizator) {
154+
return new SessionProviderImpl(authenticator, Authorizator.combine(this.authorizator, authorizator));
155+
}
151156
}

mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.james.core.quota.QuotaCountUsage;
3838
import org.apache.james.core.quota.QuotaSizeUsage;
3939
import org.apache.james.events.EventBus;
40+
import org.apache.james.mailbox.Authorizator;
4041
import org.apache.james.mailbox.DefaultMailboxes;
4142
import org.apache.james.mailbox.MailboxAnnotationManager;
4243
import org.apache.james.mailbox.MailboxManager;
@@ -250,6 +251,11 @@ public AuthorizationStep authenticate(Username givenUserid, String passwd) {
250251
return sessionProvider.authenticate(givenUserid, passwd);
251252
}
252253

254+
@Override
255+
public SessionProvider withExtraAuthorizator(Authorizator authorizator) {
256+
return sessionProvider.withExtraAuthorizator(authorizator);
257+
}
258+
253259
@Override
254260
public AuthorizationStep authenticate(Username givenUserid) {
255261
return sessionProvider.authenticate(givenUserid);

protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.apache.james.imap.api;
2121

2222
import java.time.Duration;
23+
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Optional;
2526
import java.util.Properties;
@@ -31,6 +32,7 @@
3132
import com.google.common.base.MoreObjects;
3233
import com.google.common.base.Objects;
3334
import com.google.common.base.Preconditions;
35+
import com.google.common.collect.ImmutableList;
3436
import com.google.common.collect.ImmutableMap;
3537
import com.google.common.collect.ImmutableSet;
3638

@@ -65,6 +67,7 @@ private static boolean noBlankString(String disableCap) {
6567
private Optional<Boolean> provisionDefaultMailboxes;
6668
private Optional<Properties> customProperties;
6769
private ImmutableSet<String> additionalConnectionChecks;
70+
private ImmutableList<String> adminUsers;
6871
private ImmutableMap<String, String> idFieldsResponse;
6972

7073
private Builder() {
@@ -80,6 +83,7 @@ private Builder() {
8083
this.customProperties = Optional.empty();
8184
this.additionalConnectionChecks = ImmutableSet.of();
8285
this.idFieldsResponse = ImmutableMap.of();
86+
this.adminUsers = ImmutableList.of();
8387
}
8488

8589
public Builder idleTimeInterval(long idleTimeInterval) {
@@ -114,6 +118,11 @@ public Builder disabledCaps(ImmutableSet<String> disabledCaps) {
114118
return this;
115119
}
116120

121+
public Builder adminUsers(ImmutableList<String> adminUsers) {
122+
this.adminUsers = adminUsers;
123+
return this;
124+
}
125+
117126
public Builder disabledCaps(String... disableCaps) {
118127
this.disabledCaps = ImmutableSet.copyOf(disableCaps);
119128
return this;
@@ -166,23 +175,25 @@ public Builder idFieldsResponse(ImmutableMap<String, String> idFieldsResponse) {
166175

167176
public ImapConfiguration build() {
168177
ImmutableSet<Capability> normalizeDisableCaps = disabledCaps.stream()
169-
.filter(Builder::noBlankString)
170-
.map(StringUtils::normalizeSpace)
171-
.map(Capability::of)
172-
.collect(ImmutableSet.toImmutableSet());
178+
.filter(Builder::noBlankString)
179+
.map(StringUtils::normalizeSpace)
180+
.map(Capability::of)
181+
.collect(ImmutableSet.toImmutableSet());
182+
173183
return new ImapConfiguration(
174-
appendLimit,
175-
enableIdle.orElse(DEFAULT_ENABLE_IDLE),
176-
idleTimeInterval.orElse(DEFAULT_HEARTBEAT_INTERVAL_IN_SECONDS),
177-
concurrentRequests.orElse(DEFAULT_CONCURRENT_REQUESTS),
178-
maxQueueSize.orElse(DEFAULT_QUEUE_SIZE),
179-
idleTimeIntervalUnit.orElse(DEFAULT_HEARTBEAT_INTERVAL_UNIT),
180-
normalizeDisableCaps,
181-
isCondstoreEnable.orElse(DEFAULT_CONDSTORE_DISABLE),
182-
provisionDefaultMailboxes.orElse(DEFAULT_PROVISION_DEFAULT_MAILBOXES),
183-
customProperties.orElseGet(Properties::new),
184-
additionalConnectionChecks,
185-
idFieldsResponse);
184+
appendLimit,
185+
enableIdle.orElse(DEFAULT_ENABLE_IDLE),
186+
idleTimeInterval.orElse(DEFAULT_HEARTBEAT_INTERVAL_IN_SECONDS),
187+
concurrentRequests.orElse(DEFAULT_CONCURRENT_REQUESTS),
188+
maxQueueSize.orElse(DEFAULT_QUEUE_SIZE),
189+
idleTimeIntervalUnit.orElse(DEFAULT_HEARTBEAT_INTERVAL_UNIT),
190+
normalizeDisableCaps,
191+
isCondstoreEnable.orElse(DEFAULT_CONDSTORE_DISABLE),
192+
provisionDefaultMailboxes.orElse(DEFAULT_PROVISION_DEFAULT_MAILBOXES),
193+
customProperties.orElseGet(Properties::new),
194+
additionalConnectionChecks,
195+
adminUsers,
196+
idFieldsResponse);
186197
}
187198
}
188199

@@ -197,6 +208,7 @@ public ImapConfiguration build() {
197208
private final boolean provisionDefaultMailboxes;
198209
private final Properties customProperties;
199210
private final ImmutableSet<String> additionalConnectionChecks;
211+
private final List<String> adminUsers;
200212
private final ImmutableMap<String, String> idFieldsResponse;
201213

202214
private ImapConfiguration(Optional<Long> appendLimit,
@@ -210,6 +222,7 @@ private ImapConfiguration(Optional<Long> appendLimit,
210222
boolean provisionDefaultMailboxes,
211223
Properties customProperties,
212224
ImmutableSet<String> additionalConnectionChecks,
225+
List<String> adminUsers,
213226
ImmutableMap<String, String> idFieldsResponse) {
214227
this.appendLimit = appendLimit;
215228
this.enableIdle = enableIdle;
@@ -222,6 +235,7 @@ private ImapConfiguration(Optional<Long> appendLimit,
222235
this.provisionDefaultMailboxes = provisionDefaultMailboxes;
223236
this.customProperties = customProperties;
224237
this.additionalConnectionChecks = additionalConnectionChecks;
238+
this.adminUsers = adminUsers;
225239
this.idFieldsResponse = idFieldsResponse;
226240
}
227241

@@ -277,6 +291,10 @@ public ImmutableMap<String, String> getIdFieldsResponse() {
277291
return idFieldsResponse;
278292
}
279293

294+
public List<String> getAdminUsers() {
295+
return adminUsers;
296+
}
297+
280298
@Override
281299
public final boolean equals(Object obj) {
282300
if (obj instanceof ImapConfiguration that) {
@@ -291,7 +309,8 @@ public final boolean equals(Object obj) {
291309
&& Objects.equal(that.getCustomProperties(), customProperties)
292310
&& Objects.equal(that.isCondstoreEnable(), isCondstoreEnable)
293311
&& Objects.equal(that.getAdditionalConnectionChecks(), additionalConnectionChecks)
294-
&& Objects.equal(that.getIdFieldsResponse(), idFieldsResponse);
312+
&& Objects.equal(that.getIdFieldsResponse(), idFieldsResponse)
313+
&& Objects.equal(that.getAdminUsers(), adminUsers);
295314
}
296315
return false;
297316
}
@@ -300,7 +319,7 @@ public final boolean equals(Object obj) {
300319
public final int hashCode() {
301320
return Objects.hashCode(enableIdle, idleTimeInterval, idleTimeIntervalUnit, disabledCaps, isCondstoreEnable,
302321
concurrentRequests, maxQueueSize, appendLimit, provisionDefaultMailboxes, customProperties, additionalConnectionChecks,
303-
idFieldsResponse);
322+
idFieldsResponse, adminUsers);
304323
}
305324

306325
@Override
@@ -318,6 +337,7 @@ public String toString() {
318337
.add("customProperties", customProperties)
319338
.add("additionalConnectionChecks", additionalConnectionChecks)
320339
.add("idFieldsResponse", idFieldsResponse)
340+
.add("adminUsers", adminUsers)
321341
.toString();
322342
}
323343
}

protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.james.imap.api.message.response.StatusResponseFactory;
2828
import org.apache.james.imap.api.process.ImapSession;
2929
import org.apache.james.imap.main.PathConverter;
30+
import org.apache.james.mailbox.Authorizator;
3031
import org.apache.james.mailbox.DefaultMailboxes;
3132
import org.apache.james.mailbox.MailboxManager;
3233
import org.apache.james.mailbox.MailboxSession;
@@ -126,13 +127,23 @@ protected void doAuthWithDelegation(AuthenticationAttempt authenticationAttempt,
126127
}
127128
Username otherUser = authenticationAttempt.getDelegateUserName().orElseThrow();
128129
doAuthWithDelegation(() -> getMailboxManager()
130+
.withExtraAuthorizator(withAdminUsers())
129131
.authenticate(givenUser, authenticationAttempt.getPassword())
130132
.as(otherUser),
131133
session,
132134
request, responder,
133135
givenUser, otherUser);
134136
}
135137

138+
protected Authorizator withAdminUsers() {
139+
return (userId, otherUserId) -> {
140+
if (imapConfiguration.getAdminUsers().contains(userId.asString())) {
141+
return Authorizator.AuthorizationState.ALLOWED;
142+
}
143+
return Authorizator.AuthorizationState.FORBIDDEN;
144+
};
145+
}
146+
136147
protected void doAuthWithDelegation(MailboxSessionAuthWithDelegationSupplier mailboxSessionSupplier,
137148
ImapSession session, ImapRequest request, Responder responder,
138149
Username authenticateUser, Username delegatorUser) {

protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ private void doOAuth(OIDCSASLParser.OIDCInitialResponse oidcInitialResponse, Oid
215215
Username associatedUser = Username.of(oidcInitialResponse.getAssociatedUser());
216216
if (!associatedUser.equals(authenticatedUser)) {
217217
doAuthWithDelegation(() -> getMailboxManager()
218+
.withExtraAuthorizator(withAdminUsers())
218219
.authenticate(authenticatedUser)
219220
.as(associatedUser),
220221
session, request, responder, authenticatedUser, associatedUser);

0 commit comments

Comments
 (0)