This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit e1ae434403a20d977a4f1e46968975c6323fde14 Author: Rene Cordier <rcord...@linagora.com> AuthorDate: Tue Jul 21 16:56:03 2020 +0700 JAMES-3093 Port UserProvisioner from draft to jmap-rfc-8621 --- .../apache/james/jmap/http/UserProvisioning.scala | 64 +++++++++++++ .../james/jmap/http/UserProvisioningTest.scala | 103 +++++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala new file mode 100644 index 0000000..95895e5 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala @@ -0,0 +1,64 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.http + +import java.util.UUID + +import javax.inject.Inject +import org.apache.james.core.Username +import org.apache.james.mailbox.MailboxSession +import org.apache.james.metrics.api.MetricFactory +import org.apache.james.metrics.api.TimeMetric.ExecutionResult.DEFAULT_100_MS_THRESHOLD +import org.apache.james.user.api.{AlreadyExistInUsersRepositoryException, UsersRepository, UsersRepositoryException} +import reactor.core.scala.publisher.SMono + +class UserProvisioning @Inject() (usersRepository: UsersRepository, metricFactory: MetricFactory) { + + def provisionUser(session: MailboxSession): SMono[Unit] = + if (session != null && !usersRepository.isReadOnly) { + SMono.fromCallable(() => createAccountIfNeeded(session)) + .`then` + } else { + SMono.empty + } + + private def createAccountIfNeeded(session: MailboxSession): Unit = { + val timeMetric = metricFactory.timer("JMAP-RFC-8621-user-provisioning") + try { + val username = session.getUser + if (needsAccountCreation(username)) { + createAccount(username) + } + } catch { + case exception: AlreadyExistInUsersRepositoryException => // Ignore + case exception: UsersRepositoryException => throw new RuntimeException(exception) + } finally { + timeMetric.stopAndPublish.logWhenExceedP99(DEFAULT_100_MS_THRESHOLD) + } + } + + @throws[UsersRepositoryException] + private def createAccount(username: Username): Unit = usersRepository.addUser(username, generatePassword) + + @throws[UsersRepositoryException] + private def needsAccountCreation(username: Username): Boolean = !usersRepository.contains(username) + + private def generatePassword: String = UUID.randomUUID.toString +} diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserProvisioningTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserProvisioningTest.scala new file mode 100644 index 0000000..ff799a0 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserProvisioningTest.scala @@ -0,0 +1,103 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.http + +import java.time.Duration + +import org.apache.james.core.Username +import org.apache.james.domainlist.api.DomainList +import org.apache.james.mailbox.{MailboxSession, MailboxSessionUtil} +import org.apache.james.metrics.tests.RecordingMetricFactory +import org.apache.james.user.api.{UsersRepository, UsersRepositoryException} +import org.apache.james.user.memory.MemoryUsersRepository +import org.apache.james.util.concurrency.ConcurrentTestRunner +import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy} +import org.junit.jupiter.api.{BeforeEach, Test} +import org.mockito.Mockito.{mock, verify, verifyNoMoreInteractions, when} + +object UserProvisioningTest { + private val USERNAME: Username = Username.of("username") + private val USERNAME_WITH_DOMAIN: Username = Username.of("usern...@james.org") + private val NO_DOMAIN_LIST: DomainList = null +} + +class UserProvisioningTest { + import UserProvisioningTest._ + + var testee: UserProvisioning = _ + var usersRepository: MemoryUsersRepository = _ + + @BeforeEach + def setup(): Unit = { + usersRepository = MemoryUsersRepository.withoutVirtualHosting(NO_DOMAIN_LIST) + testee = new UserProvisioning(usersRepository, new RecordingMetricFactory) + } + + @Test + def filterShouldDoNothingOnNullSession(): Unit = { + testee.provisionUser(null).block() + + assertThat(usersRepository.list).toIterable + .isEmpty + } + + @Test + def filterShouldAddUsernameWhenNoVirtualHostingAndMailboxSessionContainsUsername(): Unit = { + usersRepository.setEnableVirtualHosting(false) + val mailboxSession: MailboxSession = MailboxSessionUtil.create(USERNAME) + + testee.provisionUser(mailboxSession).block() + + assertThat(usersRepository.list).toIterable + .contains(USERNAME) + } + + @Test + def filterShouldFailOnInvalidVirtualHosting(): Unit = { + usersRepository.setEnableVirtualHosting(false) + val mailboxSession: MailboxSession = MailboxSessionUtil.create(USERNAME_WITH_DOMAIN) + + assertThatThrownBy(() => testee.provisionUser(mailboxSession).block()) + .hasCauseInstanceOf(classOf[UsersRepositoryException]) + } + + @Test + def filterShouldNotTryToAddUserWhenReadOnlyUsersRepository(): Unit = { + val usersRepository: UsersRepository = mock(classOf[UsersRepository]) + when(usersRepository.isReadOnly).thenReturn(true) + testee = new UserProvisioning(usersRepository, new RecordingMetricFactory) + + val mailboxSession: MailboxSession = MailboxSessionUtil.create(USERNAME_WITH_DOMAIN) + testee.provisionUser(mailboxSession).block() + + verify(usersRepository).isReadOnly + verifyNoMoreInteractions(usersRepository) + } + + @Test + def testConcurrentAccessToFilterShouldNotThrow(): Unit = { + val session: MailboxSession = MailboxSessionUtil.create(USERNAME) + + ConcurrentTestRunner.builder + .operation((threadNumber: Int, step: Int) => testee.provisionUser(session)) + .threadCount(2) + .runSuccessfullyWithin(Duration.ofMinutes(1)) + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org