diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenFacade.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenFacade.kt index b45ee8035..b507352a2 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenFacade.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenFacade.kt @@ -42,23 +42,24 @@ import java.io.InputStream class MavenFacade internal constructor( private val journalist: Journalist, private val repositorySecurityProvider: RepositorySecurityProvider, - private val repositoryService: RepositoryService, - private val mavenService: MavenService, + private val repositoryProvider: RepositoryProvider, private val metadataService: MetadataService, private val latestService: LatestService, ) : Journalist, Facade { + private val repositoryService = repositoryProvider.repositoryService + fun findDetails(lookupRequest: LookupRequest): Result = - mavenService.findDetails(lookupRequest) + repositoryService.findDetails(lookupRequest) fun findFile(lookupRequest: LookupRequest): Result, ErrorResponse> = - mavenService.findFile(lookupRequest) + repositoryService.findFile(lookupRequest) fun deployFile(deployRequest: DeployRequest): Result = - mavenService.deployFile(deployRequest) + repositoryService.deployFile(deployRequest) fun deleteFile(deleteRequest: DeleteRequest): Result = - mavenService.deleteFile(deleteRequest) + repositoryService.deleteFile(deleteRequest) fun saveMetadata(saveMetadataRequest: SaveMetadataRequest): Result = metadataService.saveMetadata(saveMetadataRequest) @@ -95,10 +96,10 @@ class MavenFacade internal constructor( repositoryService.getRootDirectory(accessToken) fun getRepository(name: String) = - repositoryService.getRepository(name) + repositoryService.repositoryProvider.getRepository(name) fun getRepositories(): Collection = - repositoryService.getRepositories() + repositoryService.repositoryProvider.getRepositories() override fun getLogger(): Logger = journalist.logger diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenService.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenService.kt deleted file mode 100644 index 1395e2893..000000000 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenService.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2023 dzikoysk - * - * Licensed 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 com.reposilite.maven - -import com.reposilite.journalist.Journalist -import com.reposilite.journalist.Logger -import com.reposilite.maven.api.DeleteRequest -import com.reposilite.maven.api.DeployEvent -import com.reposilite.maven.api.DeployRequest -import com.reposilite.maven.api.Identifier -import com.reposilite.maven.api.LookupRequest -import com.reposilite.maven.api.METADATA_FILE -import com.reposilite.maven.api.PreResolveEvent -import com.reposilite.maven.api.ResolvedFileEvent -import com.reposilite.plugin.Extensions -import com.reposilite.shared.ErrorResponse -import com.reposilite.shared.badRequestError -import com.reposilite.shared.notFound -import com.reposilite.shared.notFoundError -import com.reposilite.shared.unauthorizedError -import com.reposilite.statistics.StatisticsFacade -import com.reposilite.statistics.api.IncrementResolvedRequest -import com.reposilite.storage.api.DocumentInfo -import com.reposilite.storage.api.FileDetails -import com.reposilite.storage.api.FileType.DIRECTORY -import com.reposilite.storage.api.Location -import com.reposilite.token.AccessTokenIdentifier -import panda.std.Result -import panda.std.asSuccess -import java.io.InputStream - -internal class MavenService( - private val journalist: Journalist, - private val repositoryService: RepositoryService, - private val repositorySecurityProvider: RepositorySecurityProvider, - private val mirrorService: MirrorService, - private val statisticsFacade: StatisticsFacade, - private val extensions: Extensions, -) : Journalist { - - private val ignoredExtensions = listOf( - // Checksums - ".md5", - ".sha1", - ".sha256", - ".sha512", - // Artifact descriptions - ".pom", - ".xml", - ".module", - // Artifact extensions - "-sources.jar", - "-javadoc.jar", - // Sign - ".asc" - ) - - fun findDetails(lookupRequest: LookupRequest): Result = - resolve(lookupRequest) { repository, gav -> findDetails(lookupRequest.accessToken, repository, gav) } - - fun findFile(lookupRequest: LookupRequest): Result, ErrorResponse> = - resolve(lookupRequest) { repository, gav -> findFile(lookupRequest.accessToken, repository, gav) } - - fun canAccessResource(accessToken: AccessTokenIdentifier?, repository: String, gav: Location): Result = - repositoryService.findRepository(repository) - .flatMap { repositorySecurityProvider.canAccessResource(accessToken, it, gav) } - - fun deployFile(deployRequest: DeployRequest): Result { - val (repository, path) = deployRequest - - if (repository.redeployment.not() && !path.getSimpleName().contains(METADATA_FILE) && repository.storageProvider.exists(path)) { - return badRequestError("Redeployment is not allowed") - } - - return repository.storageProvider.putFile(path, deployRequest.content).peek { - logger.info("DEPLOY | Artifact $path successfully deployed to ${repository.name} by ${deployRequest.by}") - extensions.emitEvent(DeployEvent(repository, path, deployRequest.by)) - } - } - - fun deleteFile(deleteRequest: DeleteRequest): Result { - val (accessToken, repository, path) = deleteRequest - - if (!repositorySecurityProvider.canModifyResource(accessToken, repository, path)) { - return unauthorizedError("Unauthorized access request") - } - - return repository.storageProvider.removeFile(path).peek { - logger.info("DELETE | File $path has been deleted from ${repository.name} by ${deleteRequest.by}") - } - } - - private fun findFile(accessToken: AccessTokenIdentifier?, repository: Repository, gav: Location): Result, ErrorResponse> = - findDetails(accessToken, repository, gav) - .`is`(DocumentInfo::class.java) { notFound("Requested file is a directory") } - .flatMap { details -> findInputStream(repository, gav).map { details to it } } - .let { extensions.emitEvent(ResolvedFileEvent(accessToken, repository, gav, it)).result } - - private fun findInputStream(repository: Repository, gav: Location): Result = - if (repository.storageProvider.exists(gav)) { - logger.debug("Gav '$gav' found in '${repository.name}' repository") - repository.storageProvider.getFile(gav) - } else { - logger.debug("Cannot find '$gav' in '${repository.name}' repository, requesting proxied repositories") - mirrorService.findRemoteFile(repository, gav) - } - - private fun findDetails(accessToken: AccessTokenIdentifier?, repository: Repository, gav: Location): Result = - when { - repository.storageProvider.exists(gav) -> findLocalDetails(accessToken, repository, gav) - else -> findProxiedDetails(repository, gav) - }.peek { - recordResolvedRequest(Identifier(repository.name, gav.toString()), it) - } - - private fun findLocalDetails(accessToken: AccessTokenIdentifier?, repository: Repository, gav: Location): Result = - repository.storageProvider.getFileDetails(gav) - .flatMap { - it.takeIf { it.type == DIRECTORY } - ?.let { repositorySecurityProvider.canBrowseResource(accessToken, repository, gav).map { _ -> it } } - ?: it.asSuccess() - } - - private fun findProxiedDetails(repository: Repository, gav: Location): Result = - mirrorService - .findRemoteDetails(repository, gav) - .mapErr { notFound("Cannot find '$gav' in local and remote repositories") } - - private fun recordResolvedRequest(identifier: Identifier, fileDetails: FileDetails) { - if (fileDetails is DocumentInfo && ignoredExtensions.none { extension -> fileDetails.name.endsWith(extension) }) { - statisticsFacade.incrementResolvedRequest(IncrementResolvedRequest(identifier)) - } - } - - private fun resolve(lookupRequest: LookupRequest, block: (Repository, Location) -> Result): Result { - val (accessToken, repositoryName, gav) = lookupRequest - val repository = repositoryService.getRepository(lookupRequest.repository) ?: return notFoundError("Repository $repositoryName not found") - - return canAccessResource(lookupRequest.accessToken, repository.name, gav) - .onError { logger.debug("ACCESS | Unauthorized attempt of access (token: $accessToken) to $gav from ${repository.name}") } - .peek { extensions.emitEvent(PreResolveEvent(accessToken, repository, gav)) } - .flatMap { block(repository, gav) } - } - - override fun getLogger(): Logger = - journalist.logger - -} diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryFactory.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryFactory.kt index d8bae5222..c4d9b8b91 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryFactory.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryFactory.kt @@ -16,6 +16,7 @@ package com.reposilite.maven +import com.reposilite.auth.AuthenticationFacade import com.reposilite.maven.application.MirroredRepositorySettings import com.reposilite.maven.application.RepositorySettings import com.reposilite.shared.http.RemoteClientProvider @@ -28,8 +29,9 @@ import java.util.UUID internal class RepositoryFactory( private val workingDirectory: Path, + private val authenticationFacade: AuthenticationFacade, private val remoteClientProvider: RemoteClientProvider, - private val repositoryProvider: RepositoryProvider, + private val repositoryService: RepositoryService, private val failureFacade: FailureFacade, private val storageFacade: StorageFacade, private val repositoriesNames: Collection, @@ -65,11 +67,17 @@ internal class RepositoryFactory( } val remoteClient = when { - repositoriesNames.contains(host) -> RepositoryLoopbackClient(lazy { repositoryProvider.getRepositories()[host]!! }) - else -> configurationSource.httpProxy - .takeIf { it.isNotEmpty() } - ?.let { createHttpProxy(it) } - .let { remoteClientProvider.createClient(failureFacade, it) } + repositoriesNames.contains(host) -> + RepositoryLoopbackClient( + authenticationFacade = authenticationFacade, + repositoryService = repositoryService, + repositoryName = host + ) + else -> + configurationSource.httpProxy + .takeIf { it.isNotEmpty() } + ?.let { createHttpProxy(it) } + .let { remoteClientProvider.createClient(failureFacade, it) } } return MirrorHost(host, configurationSource, remoteClient) diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryLoopbackClient.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryLoopbackClient.kt index 5103b0ec3..92fb58336 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryLoopbackClient.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryLoopbackClient.kt @@ -16,26 +16,58 @@ package com.reposilite.maven +import com.reposilite.auth.AuthenticationFacade +import com.reposilite.auth.api.Credentials +import com.reposilite.maven.api.LookupRequest import com.reposilite.shared.ErrorResponse +import com.reposilite.shared.http.AuthenticationMethod.LOOPBACK_LINK import com.reposilite.shared.http.RemoteClient import com.reposilite.shared.http.RemoteCredentials -import com.reposilite.shared.notFound +import com.reposilite.shared.toErrorResponse import com.reposilite.storage.api.FileDetails import com.reposilite.storage.api.Location import com.reposilite.storage.api.toLocation +import com.reposilite.token.AccessTokenIdentifier +import io.javalin.http.HttpStatus.UNAUTHORIZED +import panda.std.Option import panda.std.Result import java.io.InputStream -internal class RepositoryLoopbackClient(private val repository: Lazy) : RemoteClient { +internal class RepositoryLoopbackClient( + private val authenticationFacade: AuthenticationFacade, + private val repositoryService: RepositoryService, + private val repositoryName: String +) : RemoteClient { - override fun head(uri: String, credentials: RemoteCredentials?, connectTimeoutInSeconds: Int, readTimeoutInSeconds: Int): Result = - repository.value.storageProvider.getFileDetails(toGav(uri)) - .`is`(FileDetails::class.java) { notFound("Requested file is a directory") } + override fun head(uri: String, credentials: RemoteCredentials?, connectTimeoutInSeconds: Int, readTimeoutInSeconds: Int): Result = + repositoryService.findDetails( + LookupRequest( + accessToken = credentials.toAccessToken(), + repository = repositoryName, + gav = toGav(uri) + ) + ) override fun get(uri: String, credentials: RemoteCredentials?, connectTimeoutInSeconds: Int, readTimeoutInSeconds: Int): Result = - repository.value.storageProvider.getFile(toGav(uri)) + repositoryService.findFile( + LookupRequest( + accessToken = credentials.toAccessToken(), + repository = repositoryName, + gav = toGav(uri) + ) + ).map { (_, content) -> content } + + private fun RemoteCredentials?.toAccessToken(): AccessTokenIdentifier? = + Option.of(this) + .toResult(UNAUTHORIZED.toErrorResponse("Missing credentials")) + .filter({ it.method == LOOPBACK_LINK }, { UNAUTHORIZED.toErrorResponse("") }) + .flatMap { authenticationFacade.authenticateByCredentials(Credentials(it.login, it.password)) } + .fold( + { it.identifier }, + { null } + ) private fun toGav(uri: String): Location = - uri.substring(repository.value.name.length + 1).toLocation() + uri.substring(repositoryName.length + 1).toLocation() } diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryProvider.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryProvider.kt index 15b072258..ae83ffeff 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryProvider.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryProvider.kt @@ -16,21 +16,44 @@ package com.reposilite.maven +import com.reposilite.auth.AuthenticationFacade +import com.reposilite.journalist.Journalist import com.reposilite.maven.application.RepositorySettings +import com.reposilite.plugin.Extensions +import com.reposilite.shared.ErrorResponse import com.reposilite.shared.http.RemoteClientProvider +import com.reposilite.shared.notFoundError +import com.reposilite.statistics.StatisticsFacade import com.reposilite.status.FailureFacade import com.reposilite.storage.StorageFacade +import panda.std.Result +import panda.std.asSuccess import panda.std.reactive.Reference import java.nio.file.Path internal class RepositoryProvider( + private val journalist: Journalist, private val workingDirectory: Path, private val remoteClientProvider: RemoteClientProvider, + private val authenticationFacade: AuthenticationFacade, + private val extensions: Extensions, private val failureFacade: FailureFacade, + private val statisticsFacade: StatisticsFacade, private val storageFacade: StorageFacade, + private val mirrorService: MirrorService, + private val repositorySecurityProvider: RepositorySecurityProvider, repositoriesSource: Reference>, ) { + val repositoryService = RepositoryService( + journalist = journalist, + repositoryProvider = this, + securityProvider = repositorySecurityProvider, + mirrorService = mirrorService, + statisticsFacade = statisticsFacade, + extensions = extensions + ) + private var repositories: Map = createRepositories(repositoriesSource.get()) init { @@ -43,10 +66,11 @@ internal class RepositoryProvider( private fun createRepositories(repositoriesConfiguration: List): Map { val factory = RepositoryFactory( workingDirectory = workingDirectory, + authenticationFacade = authenticationFacade, remoteClientProvider = remoteClientProvider, - repositoryProvider = this, failureFacade = failureFacade, storageFacade = storageFacade, + repositoryService = repositoryService, repositoriesNames = repositoriesConfiguration.map { it.id } ) @@ -59,7 +83,16 @@ internal class RepositoryProvider( .associateBy { it.name } } - fun getRepositories(): Map = - repositories + + fun findRepository(name: String): Result = + getRepository(name) + ?.asSuccess() + ?: notFoundError("Repository $name not found") + + fun getRepository(name: String): Repository? = + repositories[name] + + fun getRepositories(): Collection = + repositories.values } diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryService.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryService.kt index f412977a5..8cb1f2ded 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryService.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryService.kt @@ -17,37 +17,152 @@ package com.reposilite.maven import com.reposilite.journalist.Journalist import com.reposilite.journalist.Logger +import com.reposilite.maven.api.DeleteRequest +import com.reposilite.maven.api.DeployEvent +import com.reposilite.maven.api.DeployRequest +import com.reposilite.maven.api.Identifier +import com.reposilite.maven.api.LookupRequest +import com.reposilite.maven.api.METADATA_FILE +import com.reposilite.maven.api.PreResolveEvent +import com.reposilite.maven.api.ResolvedFileEvent +import com.reposilite.plugin.Extensions import com.reposilite.shared.ErrorResponse +import com.reposilite.shared.badRequestError +import com.reposilite.shared.notFound import com.reposilite.shared.notFoundError +import com.reposilite.shared.unauthorizedError +import com.reposilite.statistics.StatisticsFacade +import com.reposilite.statistics.api.IncrementResolvedRequest import com.reposilite.storage.api.DirectoryInfo +import com.reposilite.storage.api.DocumentInfo +import com.reposilite.storage.api.FileDetails +import com.reposilite.storage.api.FileType.DIRECTORY +import com.reposilite.storage.api.Location import com.reposilite.storage.api.SimpleDirectoryInfo import com.reposilite.token.AccessTokenIdentifier import panda.std.Result import panda.std.asSuccess +import java.io.InputStream internal class RepositoryService( private val journalist: Journalist, - private val repositoryProvider: RepositoryProvider, - private val securityProvider: RepositorySecurityProvider + val repositoryProvider: RepositoryProvider, + private val securityProvider: RepositorySecurityProvider, + private val mirrorService: MirrorService, + private val statisticsFacade: StatisticsFacade, + private val extensions: Extensions, ) : Journalist { - fun findRepository(name: String): Result = - getRepository(name) - ?.asSuccess() - ?: notFoundError("Repository $name not found") + private val ignoredExtensions = listOf( + // Checksums + ".md5", + ".sha1", + ".sha256", + ".sha512", + // Artifact descriptions + ".pom", + ".xml", + ".module", + // Artifact extensions + "-sources.jar", + "-javadoc.jar", + // Sign + ".asc" + ) - fun getRepository(name: String): Repository? = - repositoryProvider.getRepositories()[name] + fun deployFile(deployRequest: DeployRequest): Result { + val (repository, path) = deployRequest + + if (repository.redeployment.not() && !path.getSimpleName().contains(METADATA_FILE) && repository.storageProvider.exists(path)) { + return badRequestError("Redeployment is not allowed") + } + + return repository.storageProvider.putFile(path, deployRequest.content).peek { + logger.info("DEPLOY | Artifact $path successfully deployed to ${repository.name} by ${deployRequest.by}") + extensions.emitEvent(DeployEvent(repository, path, deployRequest.by)) + } + } + + fun deleteFile(deleteRequest: DeleteRequest): Result { + val (accessToken, repository, path) = deleteRequest + + if (!securityProvider.canModifyResource(accessToken, repository, path)) { + return unauthorizedError("Unauthorized access request") + } + + return repository.storageProvider.removeFile(path).peek { + logger.info("DELETE | File $path has been deleted from ${repository.name} by ${deleteRequest.by}") + } + } + + fun findDetails(lookupRequest: LookupRequest): Result = + resolve(lookupRequest) { repository, gav -> findDetails(lookupRequest.accessToken, repository, gav) } + + fun findFile(lookupRequest: LookupRequest): Result, ErrorResponse> = + resolve(lookupRequest) { repository, gav -> findFile(lookupRequest.accessToken, repository, gav) } + + private fun resolve(lookupRequest: LookupRequest, block: (Repository, Location) -> Result): Result { + val (accessToken, repositoryName, gav) = lookupRequest + val repository = repositoryProvider.getRepository(lookupRequest.repository) ?: return notFoundError("Repository $repositoryName not found") + + return canAccessResource(lookupRequest.accessToken, repository.name, gav) + .onError { logger.debug("ACCESS | Unauthorized attempt of access (token: $accessToken) to $gav from ${repository.name}") } + .peek { extensions.emitEvent(PreResolveEvent(accessToken, repository, gav)) } + .flatMap { block(repository, gav) } + } + + fun canAccessResource(accessToken: AccessTokenIdentifier?, repository: String, gav: Location): Result = + repositoryProvider.findRepository(repository) + .flatMap { securityProvider.canAccessResource(accessToken, it, gav) } + + private fun findFile(accessToken: AccessTokenIdentifier?, repository: Repository, gav: Location): Result, ErrorResponse> = + findDetails(accessToken, repository, gav) + .`is`(DocumentInfo::class.java) { notFound("Requested file is a directory") } + .flatMap { details -> findInputStream(repository, gav).map { details to it } } + .let { extensions.emitEvent(ResolvedFileEvent(accessToken, repository, gav, it)).result } + + private fun findInputStream(repository: Repository, gav: Location): Result = + if (repository.storageProvider.exists(gav)) { + logger.debug("Gav '$gav' found in '${repository.name}' repository") + repository.storageProvider.getFile(gav) + } else { + logger.debug("Cannot find '$gav' in '${repository.name}' repository, requesting proxied repositories") + mirrorService.findRemoteFile(repository, gav) + } + + private fun findDetails(accessToken: AccessTokenIdentifier?, repository: Repository, gav: Location): Result = + when { + repository.storageProvider.exists(gav) -> findLocalDetails(accessToken, repository, gav) + else -> findProxiedDetails(repository, gav) + }.peek { + recordResolvedRequest(Identifier(repository.name, gav.toString()), it) + } + + private fun findLocalDetails(accessToken: AccessTokenIdentifier?, repository: Repository, gav: Location): Result = + repository.storageProvider.getFileDetails(gav) + .flatMap { + it.takeIf { it.type == DIRECTORY } + ?.let { securityProvider.canBrowseResource(accessToken, repository, gav).map { _ -> it } } + ?: it.asSuccess() + } + + private fun findProxiedDetails(repository: Repository, gav: Location): Result = + mirrorService + .findRemoteDetails(repository, gav) + .mapErr { notFound("Cannot find '$gav' in local and remote repositories") } + + private fun recordResolvedRequest(identifier: Identifier, fileDetails: FileDetails) { + if (fileDetails is DocumentInfo && ignoredExtensions.none { extension -> fileDetails.name.endsWith(extension) }) { + statisticsFacade.incrementResolvedRequest(IncrementResolvedRequest(identifier)) + } + } fun getRootDirectory(accessToken: AccessTokenIdentifier?): DirectoryInfo = - getRepositories() + repositoryProvider.getRepositories() .filter { securityProvider.canAccessRepository(accessToken, it) } .map { SimpleDirectoryInfo(it.name) } .let { DirectoryInfo("/", it) } - fun getRepositories(): Collection = - repositoryProvider.getRepositories().values - override fun getLogger(): Logger = journalist.logger diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/application/MavenComponents.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/application/MavenComponents.kt index 4b846ff6b..119791b36 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/application/MavenComponents.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/application/MavenComponents.kt @@ -16,16 +16,15 @@ package com.reposilite.maven.application +import com.reposilite.auth.AuthenticationFacade import com.reposilite.frontend.application.FrontendSettings import com.reposilite.journalist.Journalist import com.reposilite.maven.LatestService import com.reposilite.maven.MavenFacade -import com.reposilite.maven.MavenService import com.reposilite.maven.MetadataService import com.reposilite.maven.MirrorService import com.reposilite.maven.RepositoryProvider import com.reposilite.maven.RepositorySecurityProvider -import com.reposilite.maven.RepositoryService import com.reposilite.plugin.Extensions import com.reposilite.plugin.api.PluginComponents import com.reposilite.shared.http.RemoteClientProvider @@ -43,59 +42,53 @@ internal class MavenComponents( private val remoteClientProvider: RemoteClientProvider, private val failureFacade: FailureFacade, private val storageFacade: StorageFacade, + private val authenticationFacade: AuthenticationFacade, private val accessTokenFacade: AccessTokenFacade, private val statisticsFacade: StatisticsFacade, private val mavenSettings: Reference, private val frontendSettings: Reference, ) : PluginComponents { - private fun repositoryProvider(): RepositoryProvider = - RepositoryProvider( - workingDirectory = workingDirectory, - remoteClientProvider = remoteClientProvider, - failureFacade = failureFacade, - storageFacade = storageFacade, - repositoriesSource = mavenSettings.computed { it.repositories } - ) - private fun securityProvider(): RepositorySecurityProvider = RepositorySecurityProvider(accessTokenFacade) - private fun repositoryService(securityProvider: RepositorySecurityProvider): RepositoryService = - RepositoryService(journalist, repositoryProvider(), securityProvider) - - private fun proxyService(): MirrorService = - MirrorService(journalist) - private fun metadataService(): MetadataService = MetadataService(securityProvider()) - private fun latestService(): LatestService = - LatestService(frontendSettings.computed { it.id }) + private fun mirrorService(): MirrorService = + MirrorService(journalist) - private fun mavenService(repositoryService: RepositoryService, securityProvider: RepositorySecurityProvider, mirrorService: MirrorService): MavenService = - MavenService( + private fun repositoryProvider( + mirrorService: MirrorService = mirrorService(), + securityProvider: RepositorySecurityProvider = securityProvider(), + ): RepositoryProvider = + RepositoryProvider( journalist = journalist, - repositoryService = repositoryService, - repositorySecurityProvider = securityProvider, + workingDirectory = workingDirectory, + remoteClientProvider = remoteClientProvider, + authenticationFacade = authenticationFacade, + failureFacade = failureFacade, + storageFacade = storageFacade, mirrorService = mirrorService, statisticsFacade = statisticsFacade, - extensions = extensions + extensions = extensions, + repositorySecurityProvider = securityProvider, + repositoriesSource = mavenSettings.computed { it.repositories } ) + private fun latestService(): LatestService = + LatestService(frontendSettings.computed { it.id }) + fun mavenFacade( securityProvider: RepositorySecurityProvider = securityProvider(), - repositoryService: RepositoryService = repositoryService(securityProvider), - mirrorService: MirrorService = proxyService(), - mavenService: MavenService = mavenService(repositoryService, securityProvider, mirrorService), metadataService: MetadataService = metadataService(), - latestService: LatestService = latestService() + latestService: LatestService = latestService(), + repositoryProvider: RepositoryProvider = repositoryProvider(), ): MavenFacade = MavenFacade( journalist = journalist, repositorySecurityProvider = securityProvider, - repositoryService = repositoryService, - mavenService = mavenService, + repositoryProvider = repositoryProvider, metadataService = metadataService, latestService = latestService ) diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/application/MavenPlugin.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/application/MavenPlugin.kt index acc33c942..8e397cd60 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/application/MavenPlugin.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/application/MavenPlugin.kt @@ -35,7 +35,7 @@ import com.reposilite.web.api.RoutingSetupEvent @Plugin( name = "maven", - dependencies = ["failure", "local-configuration", "shared-configuration", "statistics", "frontend", "access-token", "storage"], + dependencies = ["failure", "local-configuration", "shared-configuration", "statistics", "frontend", "authentication", "access-token", "storage"], settings = MavenSettings::class ) internal class MavenPlugin : ReposilitePlugin() { @@ -51,6 +51,7 @@ internal class MavenPlugin : ReposilitePlugin() { remoteClientProvider = HttpRemoteClientProvider, failureFacade = facade(), storageFacade = facade(), + authenticationFacade = facade(), accessTokenFacade = facade(), statisticsFacade = facade(), mavenSettings = sharedConfigurationFacade.getDomainSettings(), diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/shared/http/HttpRemoteClient.kt b/reposilite-backend/src/main/kotlin/com/reposilite/shared/http/HttpRemoteClient.kt index 80002b3b9..beed1e995 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/shared/http/HttpRemoteClient.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/shared/http/HttpRemoteClient.kt @@ -26,6 +26,9 @@ import com.reposilite.journalist.Journalist import com.reposilite.journalist.Logger import com.reposilite.shared.ErrorResponse import com.reposilite.shared.badRequestError +import com.reposilite.shared.http.AuthenticationMethod.BASIC +import com.reposilite.shared.http.AuthenticationMethod.CUSTOM_HEADER +import com.reposilite.shared.http.AuthenticationMethod.LOOPBACK_LINK import com.reposilite.shared.toErrorResult import com.reposilite.storage.api.DocumentInfo import com.reposilite.storage.api.FileDetails @@ -115,8 +118,8 @@ class HttpRemoteClient(private val journalist: Journalist, proxy: Proxy?) : Remo private fun HttpRequest.authenticateWith(credentials: RemoteCredentials?): HttpRequest = also { if (credentials != null) { when (credentials.method) { - AuthenticationMethod.BASIC -> it.headers.setBasicAuthentication(credentials.login, credentials.password) - AuthenticationMethod.CUSTOM_HEADER -> it.headers[credentials.login] = credentials.password + BASIC, LOOPBACK_LINK -> it.headers.setBasicAuthentication(credentials.login, credentials.password) + CUSTOM_HEADER -> it.headers[credentials.login] = credentials.password } } } diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/shared/http/RemoteClient.kt b/reposilite-backend/src/main/kotlin/com/reposilite/shared/http/RemoteClient.kt index 4ee00795f..89366c1c1 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/shared/http/RemoteClient.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/shared/http/RemoteClient.kt @@ -23,7 +23,8 @@ import java.io.InputStream enum class AuthenticationMethod { BASIC, - CUSTOM_HEADER + CUSTOM_HEADER, + LOOPBACK_LINK } interface RemoteCredentials { @@ -40,7 +41,7 @@ interface RemoteClient { * @param connectTimeoutInSeconds - connection establishment timeout in seconds * @param readTimeoutInSeconds - connection read timeout in seconds */ - fun head(uri: String, credentials: RemoteCredentials?, connectTimeoutInSeconds: Int, readTimeoutInSeconds: Int): Result + fun head(uri: String, credentials: RemoteCredentials?, connectTimeoutInSeconds: Int, readTimeoutInSeconds: Int): Result /** * @param uri - full remote host address with a gav diff --git a/reposilite-backend/src/test/kotlin/com/reposilite/maven/MavenFacadeTest.kt b/reposilite-backend/src/test/kotlin/com/reposilite/maven/MavenFacadeTest.kt index 773d1f8d1..1ee7d98a0 100644 --- a/reposilite-backend/src/test/kotlin/com/reposilite/maven/MavenFacadeTest.kt +++ b/reposilite-backend/src/test/kotlin/com/reposilite/maven/MavenFacadeTest.kt @@ -54,8 +54,11 @@ internal class MavenFacadeTest : MavenSpecification() { RepositorySettings(HIDDEN.name, visibility = HIDDEN), RepositorySettings(PUBLIC.name, visibility = PUBLIC), RepositorySettings("PROXIED", visibility = PUBLIC, proxied = mutableListOf( - MirroredRepositorySettings(REMOTE_REPOSITORY, store = true, authorization = REMOTE_AUTH), - MirroredRepositorySettings(REMOTE_REPOSITORY_WITH_WHITELIST, allowedGroups = listOf("do.allow")) + MirroredRepositorySettings(reference = REMOTE_REPOSITORY, store = true, authorization = REMOTE_AUTH), + MirroredRepositorySettings(reference = REMOTE_REPOSITORY_WITH_WHITELIST, allowedGroups = listOf("do.allow")) + )), + RepositorySettings("PROXIED-LOOPBACK", visibility = PUBLIC, proxied = mutableListOf( + MirroredRepositorySettings(reference = "PROXIED") )) ) @@ -224,6 +227,23 @@ internal class MavenFacadeTest : MavenSpecification() { assertThat(data.readBytes().decodeToString()).isEqualTo(REMOTE_CONTENT) } + @Test + fun `should find mirrored file in local loopback repository` () { + // given: a file available in remote repository + val fileSpec = FileSpec("PROXIED", "/gav/file.pom", REMOTE_CONTENT) + + // when: a remote file is requested through proxied repository + val response = mavenFacade.findFile( + fileSpec + .toLookupRequest(UNAUTHORIZED) + .copy(repository = "PROXIED-LOOPBACK") + ) + + // then: the file has been properly proxied + val (_, data) = assertOk(response) + assertThat(data.readBytes().decodeToString()).isEqualTo(REMOTE_CONTENT) + } + } @Nested diff --git a/reposilite-backend/src/test/kotlin/com/reposilite/maven/specification/MavenSpecification.kt b/reposilite-backend/src/test/kotlin/com/reposilite/maven/specification/MavenSpecification.kt index e058f758a..bcd105b72 100644 --- a/reposilite-backend/src/test/kotlin/com/reposilite/maven/specification/MavenSpecification.kt +++ b/reposilite-backend/src/test/kotlin/com/reposilite/maven/specification/MavenSpecification.kt @@ -16,6 +16,8 @@ package com.reposilite.maven.specification +import com.reposilite.auth.application.AuthenticationComponents +import com.reposilite.auth.application.AuthenticationSettings import com.reposilite.frontend.application.FrontendSettings import com.reposilite.journalist.backend.InMemoryLogger import com.reposilite.maven.MavenFacade @@ -35,19 +37,17 @@ import com.reposilite.shared.notFoundError import com.reposilite.statistics.DailyDateIntervalProvider import com.reposilite.statistics.StatisticsFacade import com.reposilite.statistics.infrastructure.InMemoryStatisticsRepository -import com.reposilite.status.FailureFacade +import com.reposilite.status.application.FailureComponents import com.reposilite.storage.StorageFacade import com.reposilite.storage.api.DocumentInfo import com.reposilite.storage.api.Location import com.reposilite.storage.api.toLocation -import com.reposilite.token.AccessTokenFacade import com.reposilite.token.AccessTokenIdentifier import com.reposilite.token.AccessTokenType.TEMPORARY -import com.reposilite.token.ExportService import com.reposilite.token.Route import com.reposilite.token.RoutePermission import com.reposilite.token.api.CreateAccessTokenRequest -import com.reposilite.token.infrastructure.InMemoryAccessTokenRepository +import com.reposilite.token.application.AccessTokenComponents import io.javalin.http.ContentType.TEXT_XML import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.io.TempDir @@ -73,15 +73,24 @@ internal abstract class MavenSpecification { lateinit var workingDirectory: File protected lateinit var mavenFacade: MavenFacade - protected val logger = InMemoryLogger() + private val logger = InMemoryLogger() protected val extensions = Extensions(logger) - protected val failureFacade = FailureFacade(logger) - protected val accessTokenFacade = AccessTokenFacade( + + private val failureFacade = FailureComponents( + journalist = logger + ).failureFacade() + + private val accessTokenFacade = AccessTokenComponents( + journalist = logger, + database = null + ).accessTokenFacade() + + private val authenticationFacade = AuthenticationComponents( journalist = logger, - temporaryRepository = InMemoryAccessTokenRepository(), - persistentRepository = InMemoryAccessTokenRepository(), - exportService = ExportService() - ) + accessTokenFacade = accessTokenFacade, + failureFacade = failureFacade, + authenticationSettings = AuthenticationSettings().toReference(), + ).authenticationFacade() abstract fun repositories(): List @@ -119,6 +128,7 @@ internal abstract class MavenSpecification { remoteClientProvider = remoteClientProvider, failureFacade = failureFacade, storageFacade = StorageFacade(), + authenticationFacade = authenticationFacade, accessTokenFacade = accessTokenFacade, statisticsFacade = StatisticsFacade(logger, Reference.reference(false), DailyDateIntervalProvider.toReference(), InMemoryStatisticsRepository()), mavenSettings = reference(MavenSettings(