Skip to content

Commit c75eb15

Browse files
[IMPROVEMENT] CapabilityFactory should take Username into account + be reactive (#2781)
It is a common use case that capability could be varied per User, e.g. in a SaaS deployment where users can have different plan capabilities. Also, allow evaluating capability to be reactive.
1 parent 57bc76a commit c75eb15

File tree

5 files changed

+36
-24
lines changed

5 files changed

+36
-24
lines changed

server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import jakarta.inject.{Inject, Named}
3535
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
3636
import org.apache.http.HttpStatus.SC_OK
3737
import org.apache.james.GuiceJamesServer
38+
import org.apache.james.core.Username
3839
import org.apache.james.events.Event.EventId
3940
import org.apache.james.events.EventBus
4041
import org.apache.james.jmap.api.model.Size.Size
@@ -184,7 +185,7 @@ case class CustomCapabilityProperties() extends CapabilityProperties {
184185
case class CustomCapability(properties: CustomCapabilityProperties = CustomCapabilityProperties(), identifier: CapabilityIdentifier = CUSTOM) extends Capability
185186

186187
case object CustomCapabilityFactory extends CapabilityFactory {
187-
override def create(urlPrefixes: UrlPrefixes): Capability = CustomCapability()
188+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = CustomCapability()
188189

189190
override def id(): CapabilityIdentifier = CUSTOM
190191
}

server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@ import eu.timepit.refined.api.Refined
2828
import eu.timepit.refined.auto._
2929
import eu.timepit.refined.collection.NonEmpty
3030
import eu.timepit.refined.string.Uri
31+
import org.apache.james.core.Username
3132
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, EMAIL_SUBMISSION, JAMES_DELEGATION, JAMES_IDENTITY_SORTORDER, JAMES_QUOTA, JAMES_SHARES, JMAP_CORE, JMAP_MAIL, JMAP_MDN, JMAP_QUOTA, JMAP_VACATION_RESPONSE, JMAP_WEBSOCKET}
3233
import org.apache.james.jmap.core.CoreCapabilityProperties.CollationAlgorithm
3334
import org.apache.james.jmap.core.MailCapability.EmailQuerySortOption
3435
import org.apache.james.jmap.core.SubmissionCapabilityFactory.maximumDelays
3536
import org.apache.james.jmap.core.UnsignedInt.{UnsignedInt, UnsignedIntConstraint}
3637
import org.apache.james.jmap.json.ResponseSerializer
3738
import org.apache.james.util.Size
39+
import org.reactivestreams.Publisher
3840
import play.api.libs.json.{JsObject, Json}
41+
import reactor.core.scala.publisher.SMono
3942
import reactor.netty.http.server.HttpServerRequest
4043

4144
import scala.util.{Failure, Success, Try}
@@ -98,7 +101,10 @@ object UrlPrefixes {
98101
final case class UrlPrefixes(httpUrlPrefix: URI, webSocketURLPrefix: URI)
99102

100103
trait CapabilityFactory {
101-
def create(urlPrefixes: UrlPrefixes): Capability
104+
def create(urlPrefixes: UrlPrefixes, username: Username): Capability
105+
106+
def createReactive(urlPrefixes: UrlPrefixes, username: Username): Publisher[Capability] =
107+
SMono.fromCallable(() => create(urlPrefixes, username))
102108

103109
def id(): CapabilityIdentifier
104110
}
@@ -109,7 +115,7 @@ final case class CoreCapability(properties: CoreCapabilityProperties,
109115
final case class CoreCapabilityFactory(configration: JmapRfc8621Configuration) extends CapabilityFactory {
110116
override def id(): CapabilityIdentifier = JMAP_CORE
111117

112-
override def create(urlPrefixes: UrlPrefixes): Capability = CoreCapability(CoreCapabilityProperties(
118+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = CoreCapability(CoreCapabilityProperties(
113119
configration.maxUploadSize,
114120
MaxConcurrentUpload(4L),
115121
MaxSizeRequest(10_000_000L), // See MaxSizeRequest.DEFAULT compile-time refinement only works with literals
@@ -125,7 +131,7 @@ case class WebSocketCapability(properties: WebSocketCapabilityProperties, identi
125131
case object WebSocketCapabilityFactory extends CapabilityFactory {
126132
override def id(): CapabilityIdentifier = JMAP_WEBSOCKET
127133

128-
override def create(urlPrefixes: UrlPrefixes): Capability = WebSocketCapability(
134+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = WebSocketCapability(
129135
WebSocketCapabilityProperties(SupportsPush(true), new URI(urlPrefixes.webSocketURLPrefix.toString + "/jmap/ws")))
130136
}
131137

@@ -188,7 +194,7 @@ case object SubmissionCapabilityFactory {
188194
final case class SubmissionCapabilityFactory(clock: Clock, supportsDelaySends: Boolean) extends CapabilityFactory {
189195
override def id(): CapabilityIdentifier = EMAIL_SUBMISSION
190196

191-
override def create(urlPrefixes: UrlPrefixes): Capability =
197+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability =
192198
if (supportsDelaySends) {
193199
advertiseDelaySendSupport
194200
} else {
@@ -227,7 +233,7 @@ final case class MailCapability(properties: MailCapabilityProperties,
227233
case class MailCapabilityFactory(configuration: JmapRfc8621Configuration) extends CapabilityFactory {
228234
override def id(): CapabilityIdentifier = JMAP_MAIL
229235

230-
override def create(urlPrefixes: UrlPrefixes): Capability = MailCapability(MailCapabilityProperties(
236+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = MailCapability(MailCapabilityProperties(
231237
MaxMailboxesPerEmail(Some(10_000_000L)),
232238
MaxMailboxDepth(None),
233239
MaxSizeMailboxName(200L),
@@ -287,7 +293,7 @@ final case class QuotaCapability(properties: QuotaCapabilityProperties = QuotaCa
287293
case object QuotaCapabilityFactory extends CapabilityFactory {
288294
override def id(): CapabilityIdentifier = JAMES_QUOTA
289295

290-
override def create(urlPrefixes: UrlPrefixes): Capability = QuotaCapability()
296+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = QuotaCapability()
291297
}
292298

293299
final case class IdentitySortOrderCapabilityProperties() extends CapabilityProperties {
@@ -300,7 +306,7 @@ final case class IdentitySortOrderCapability(properties: IdentitySortOrderCapabi
300306
case object IdentitySortOrderCapabilityFactory extends CapabilityFactory {
301307
override def id(): CapabilityIdentifier = JAMES_IDENTITY_SORTORDER
302308

303-
override def create(urlPrefixes: UrlPrefixes): Capability = IdentitySortOrderCapability()
309+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = IdentitySortOrderCapability()
304310
}
305311

306312
final case class DelegationCapabilityProperties() extends CapabilityProperties {
@@ -313,7 +319,7 @@ final case class DelegationCapability(properties: DelegationCapabilityProperties
313319
case object DelegationCapabilityFactory extends CapabilityFactory {
314320
override def id(): CapabilityIdentifier = JAMES_DELEGATION
315321

316-
override def create(urlPrefixes: UrlPrefixes): Capability = DelegationCapability()
322+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = DelegationCapability()
317323
}
318324

319325
final case class SharesCapabilityProperties() extends CapabilityProperties {
@@ -323,7 +329,7 @@ final case class SharesCapabilityProperties() extends CapabilityProperties {
323329
case object SharesCapabilityFactory extends CapabilityFactory {
324330
override def id(): CapabilityIdentifier = JAMES_SHARES
325331

326-
override def create(urlPrefixes: UrlPrefixes): Capability = SharesCapability()
332+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = SharesCapability()
327333
}
328334

329335
final case class SharesCapability(properties: SharesCapabilityProperties = SharesCapabilityProperties(),
@@ -336,7 +342,7 @@ final case class MDNCapabilityProperties() extends CapabilityProperties {
336342
case object MDNCapabilityFactory extends CapabilityFactory {
337343
override def id(): CapabilityIdentifier = JMAP_MDN
338344

339-
override def create(urlPrefixes: UrlPrefixes): Capability = MDNCapability()
345+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = MDNCapability()
340346
}
341347

342348
final case class MDNCapability(properties: MDNCapabilityProperties = MDNCapabilityProperties(),
@@ -349,7 +355,7 @@ final case class VacationResponseCapabilityProperties() extends CapabilityProper
349355
case object VacationResponseCapabilityFactory extends CapabilityFactory {
350356
override def id(): CapabilityIdentifier = JMAP_VACATION_RESPONSE
351357

352-
override def create(urlPrefixes: UrlPrefixes): Capability = VacationResponseCapability()
358+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = VacationResponseCapability()
353359
}
354360

355361
final case class VacationResponseCapability(properties: VacationResponseCapabilityProperties = VacationResponseCapabilityProperties(),
@@ -365,5 +371,5 @@ final case class JmapQuotaCapabilityProperties() extends CapabilityProperties {
365371
case object JmapQuotaCapabilityFactory extends CapabilityFactory {
366372
override def id(): CapabilityIdentifier = JMAP_QUOTA
367373

368-
override def create(urlPrefixes: UrlPrefixes): Capability = JmapQuotaCapability()
374+
override def create(urlPrefixes: UrlPrefixes, username: Username): Capability = JmapQuotaCapability()
369375
}

server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,11 @@ class SessionRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
6666
.flatMap(mailboxSession => getDelegatedUsers(mailboxSession)
6767
.collectSeq()
6868
.map(seq => Pair.of(mailboxSession.getUser, seq)))
69-
.handle[Session] {
70-
case (baseUserAndDelegatedUsers, sink) => sessionSupplier.generate(
69+
.flatMap { baseUserAndDelegatedUsers =>
70+
sessionSupplier.generate(
7171
username = baseUserAndDelegatedUsers.getLeft,
7272
delegatedUsers = baseUserAndDelegatedUsers.getRight.toSet,
7373
urlPrefixes = UrlPrefixes.from(jmapRfc8621Configuration, request))
74-
.fold(sink.error, session => sink.next(session))
7574
}
7675
.flatMap(session => sendRespond(session, response))
7776
.onErrorResume(throwable => SMono.fromPublisher(errorHandling(throwable, response)))

server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import jakarta.inject.Inject
2626
import org.apache.james.core.Username
2727
import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
2828
import org.apache.james.jmap.core.{Account, AccountId, Capabilities, Capability, CapabilityFactory, IsPersonal, IsReadOnly, JmapRfc8621Configuration, Session, URL, UrlPrefixes}
29+
import reactor.core.scala.publisher.{SFlux, SMono}
2930

3031
import scala.jdk.CollectionConverters._
3132

@@ -40,15 +41,13 @@ class SessionSupplier(capabilityFactories: Set[CapabilityFactory], configuration
4041
.toOption
4142
.getOrElse(false)
4243

43-
def generate(username: Username, delegatedUsers: Set[Username], urlPrefixes: UrlPrefixes): Either[IllegalArgumentException, Session] = {
44+
def generate(username: Username, delegatedUsers: Set[Username], urlPrefixes: UrlPrefixes): SMono[Session] = {
4445
val urlEndpointResolver: JmapUrlEndpointResolver = new JmapUrlEndpointResolver(urlPrefixes)
45-
val capabilities: Set[Capability] = capabilityFactories
46-
.map(cf => cf.create(urlPrefixes))
47-
.filter(capability => !configuration.disabledCapabilities.contains(capability.identifier()))
4846

4947
for {
50-
account <- accounts(username, capabilities)
51-
delegatedAccounts <- delegatedAccounts(delegatedUsers, capabilities)
48+
capabilities <- evaluateCapabilities(username, urlPrefixes)
49+
account <- SMono.fromTry(accounts(username, capabilities).toTry)
50+
delegatedAccounts <- SMono.fromTry(delegatedAccounts(delegatedUsers, capabilities).toTry)
5251
} yield {
5352
Session(
5453
Capabilities(capabilities),
@@ -62,6 +61,13 @@ class SessionSupplier(capabilityFactories: Set[CapabilityFactory], configuration
6261
}
6362
}
6463

64+
private def evaluateCapabilities(username: Username, urlPrefixes: UrlPrefixes): SMono[Set[Capability]] =
65+
SFlux.fromIterable(capabilityFactories)
66+
.flatMap(capabilityFactory => SMono.fromPublisher(capabilityFactory.createReactive(urlPrefixes, username)))
67+
.filter(capability => !configuration.disabledCapabilities.contains(capability.identifier()))
68+
.collectSeq()
69+
.map(_.toSet)
70+
6571
private def accounts(username: Username, capabilities: Set[Capability]): Either[IllegalArgumentException, Account] =
6672
Account.from(username, IsPersonal(true), IsReadOnly(false), capabilities)
6773

server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ class SessionSupplierTest extends AnyWordSpec with Matchers {
3434
"generate" should {
3535
"return correct username" in {
3636
new SessionSupplier(DefaultCapabilities.supported(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION), JmapRfc8621Configuration.LOCALHOST_CONFIGURATION)
37-
.generate(USERNAME, Set(), JmapRfc8621Configuration.LOCALHOST_CONFIGURATION.urlPrefixes()).toOption.get.username should equal(USERNAME)
37+
.generate(USERNAME, Set(), JmapRfc8621Configuration.LOCALHOST_CONFIGURATION.urlPrefixes()).blockOption().get.username should equal(USERNAME)
3838
}
3939

4040
"return correct account" which {
4141
val accounts = new SessionSupplier(DefaultCapabilities.supported(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION), JmapRfc8621Configuration.LOCALHOST_CONFIGURATION)
42-
.generate(USERNAME, Set(), JmapRfc8621Configuration.LOCALHOST_CONFIGURATION.urlPrefixes()).toOption.get.accounts
42+
.generate(USERNAME, Set(), JmapRfc8621Configuration.LOCALHOST_CONFIGURATION.urlPrefixes()).blockOption().get.accounts
4343

4444
"has size" in {
4545
accounts should have size 1

0 commit comments

Comments
 (0)