Skip to content

Commit

Permalink
GH-458 Provide dedicated permissions for AccessToken and Route, impro…
Browse files Browse the repository at this point in the history
…ve MavenFacadeSpec and provide MavenFacade#findFile unit tests
  • Loading branch information
dzikoysk committed Aug 4, 2021
1 parent da5dcce commit ded4766
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 123 deletions.
2 changes: 1 addition & 1 deletion reposilite-backend/reposilite-backend.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ dependencies {
/* Utilities */

implementation("net.dzikoysk:cdn:1.9.1")
implementation("org.panda-lang:expressible:1.0.2")
implementation("org.panda-lang:expressible:1.0.3")
implementation("info.picocli:picocli:4.6.1")
implementation("com.google.guava:guava:30.1.1-jre")
implementation("org.apache.commons:commons-collections4:4.4")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ package org.panda_lang.reposilite.auth

import org.panda_lang.reposilite.token.AccessTokenFacade
import org.panda_lang.reposilite.token.api.AccessToken
import org.panda_lang.reposilite.token.api.Permission.READ
import org.panda_lang.reposilite.token.api.RoutePermission.READ
import panda.std.Result
import panda.std.Result.error
import panda.std.Result.ok
import panda.utilities.StringUtils
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.Base64

class Authenticator(private val accessTokenFacade: AccessTokenFacade) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
package org.panda_lang.reposilite.auth

import org.panda_lang.reposilite.token.api.AccessToken
import org.panda_lang.reposilite.token.api.Permission
import org.panda_lang.reposilite.token.api.AccessTokenPermission
import org.panda_lang.reposilite.token.api.RoutePermission
import java.nio.file.Path

data class Session internal constructor(
Expand All @@ -29,19 +30,19 @@ data class Session internal constructor(

companion object {
val METHOD_PERMISSIONS = mapOf(
SessionMethod.HEAD to Permission.READ,
SessionMethod.GET to Permission.READ,
SessionMethod.PUT to Permission.WRITE,
SessionMethod.POST to Permission.WRITE,
SessionMethod.DELETE to Permission.WRITE
SessionMethod.HEAD to RoutePermission.READ,
SessionMethod.GET to RoutePermission.READ,
SessionMethod.PUT to RoutePermission.WRITE,
SessionMethod.POST to RoutePermission.WRITE,
SessionMethod.DELETE to RoutePermission.WRITE
)
}

fun isAuthorized() =
isManager() || accessToken.hasPermissionTo(path, METHOD_PERMISSIONS[method]!!)

fun isManager() =
accessToken.hasPermission(Permission.MANAGER)
accessToken.hasPermission(AccessTokenPermission.MANAGER)

fun getSessionIdentifier() =
"${accessToken.alias}@$address"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import io.javalin.websocket.WsMessageContext
import org.panda_lang.reposilite.auth.AuthenticationFacade
import org.panda_lang.reposilite.console.ConsoleFacade
import org.panda_lang.reposilite.shared.CachedLogger
import org.panda_lang.reposilite.token.api.Permission.MANAGER
import org.panda_lang.reposilite.token.api.AccessTokenPermission.MANAGER
import org.panda_lang.reposilite.web.ReposiliteContextFactory
import panda.utilities.StringUtils
import java.util.function.Consumer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ import net.dzikoysk.dynamiclogger.Journalist
import net.dzikoysk.dynamiclogger.Logger
import org.panda_lang.reposilite.failure.api.ErrorResponse
import org.panda_lang.reposilite.failure.api.errorResponse
import org.panda_lang.reposilite.maven.MetadataUtils.toNormalizedPath
import org.panda_lang.reposilite.maven.api.DeployRequest
import org.panda_lang.reposilite.maven.api.DocumentInfo
import org.panda_lang.reposilite.maven.api.FileDetails
import org.panda_lang.reposilite.maven.api.LookupRequest
import org.panda_lang.reposilite.maven.api.Repository
import org.panda_lang.reposilite.shared.toNormalizedPath
import org.panda_lang.reposilite.web.toPath
import panda.std.Result
import java.nio.file.Path
Expand Down Expand Up @@ -73,7 +73,7 @@ class MavenFacade internal constructor(
return errorResponse(INSUFFICIENT_STORAGE, "Not enough storage space available")
}

val path = deployRequest.gav.toNormalizedPath() ?: return errorResponse(BAD_REQUEST, "Invalid GAV")
val path = deployRequest.gav.toNormalizedPath().orNull() ?: return errorResponse(BAD_REQUEST, "Invalid GAV")

return try {
val result: Result<DocumentInfo, ErrorResponse> =
Expand All @@ -93,7 +93,7 @@ class MavenFacade internal constructor(

fun deleteFile(repositoryName: String, gav: String): Result<*, ErrorResponse> {
val repository = repositoryService.getRepository(repositoryName) ?: return errorResponse<Any>(NOT_FOUND, "Repository $repositoryName not found")
val path = gav.toNormalizedPath() ?: return errorResponse<Any>(NOT_FOUND, "Invalid GAV")
val path = gav.toNormalizedPath().orNull() ?: return errorResponse<Any>(NOT_FOUND, "Invalid GAV")

return repository.removeFile(path)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package org.panda_lang.reposilite.maven
import org.panda_lang.reposilite.failure.api.ErrorResponse
import org.panda_lang.reposilite.maven.api.Repository
import org.panda_lang.reposilite.shared.FilesUtils.getExtension
import org.panda_lang.reposilite.web.toPath
import panda.std.Result
import panda.utilities.StringUtils
import panda.utilities.text.Joiner
Expand Down Expand Up @@ -160,36 +159,4 @@ internal object MetadataUtils {
.any { !Character.isDigit(it) }
.not()

/**
* Process uri applying following changes:
*
*
* * Remove root slash
* * Remove illegal path modifiers like .. and ~
*
*
* @param uri the uri to process
* @return the normalized uri
*/
fun normalizeUri(uri: String): String? {
var normalizedUri = uri

if (normalizedUri.contains("..") || normalizedUri.contains("~") || normalizedUri.contains(":") || normalizedUri.contains("\\")) {
return null
}

while (normalizedUri.contains("//")) {
normalizedUri = normalizedUri.replace("//", "/")
}

if (normalizedUri.startsWith("/")) {
normalizedUri = normalizedUri.substring(1)
}

return normalizedUri
}

fun String.toNormalizedPath(): Path? =
normalizeUri(this)?.toPath()

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.panda_lang.reposilite.maven.api.RepositoryVisibility.HIDDEN
import org.panda_lang.reposilite.maven.api.RepositoryVisibility.PRIVATE
import org.panda_lang.reposilite.maven.api.RepositoryVisibility.PUBLIC
import org.panda_lang.reposilite.token.api.AccessToken
import org.panda_lang.reposilite.token.api.Permission.READ
import org.panda_lang.reposilite.token.api.RoutePermission
import java.nio.file.Path

internal class RepositorySecurityProvider {
Expand All @@ -14,17 +14,17 @@ internal class RepositorySecurityProvider {
when (repository.visibility) {
PUBLIC -> true
HIDDEN -> true
PRIVATE -> hasPermissionTo(accessToken, gav)
PRIVATE -> hasPermissionTo(accessToken, repository, gav)
}

fun canBrowseResource(accessToken: AccessToken?, repository: Repository, gav: Path): Boolean =
when (repository.visibility) {
PUBLIC -> true
HIDDEN -> hasPermissionTo(accessToken, gav)
PRIVATE -> hasPermissionTo(accessToken, gav)
HIDDEN -> hasPermissionTo(accessToken, repository, gav)
PRIVATE -> hasPermissionTo(accessToken, repository, gav)
}

private fun hasPermissionTo(accessToken: AccessToken?, gav: Path): Boolean =
accessToken?.hasPermissionTo(gav.toString(), READ) ?: false
private fun hasPermissionTo(accessToken: AccessToken?, repository: Repository, gav: Path): Boolean =
accessToken?.hasPermissionTo("/" + repository.name + "/" + gav.toString().replace("\\", "/"), RoutePermission.READ) ?: false

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import org.panda_lang.reposilite.failure.api.ErrorResponse
import org.panda_lang.reposilite.failure.api.errorResponse
import org.panda_lang.reposilite.shared.FileType.DIRECTORY
import org.panda_lang.reposilite.shared.FileType.FILE
import org.panda_lang.reposilite.web.toPath
import panda.std.Result
import panda.std.Result.error
import panda.std.Result.ok
import java.io.IOException
import java.io.InputStream
import java.nio.file.Files
Expand Down Expand Up @@ -61,6 +64,40 @@ fun Path.size(): Result<Long, ErrorResponse> =
}
}

fun Path.append(path: String): Result<Path, IOException> =
path.toNormalizedPath().map { this.resolve(it) }

fun String.toNormalizedPath(): Result<Path, IOException> =
normalizedAsUri().map { it.toPath() }

/**
* Process uri applying following changes:
*
*
* * Remove root slash
* * Remove illegal path modifiers like .. and ~
*
*
* @return the normalized uri
*/
fun String.normalizedAsUri(): Result<String, IOException> {
var normalizedUri = this

if (normalizedUri.contains("..") || normalizedUri.contains("~") || normalizedUri.contains(":") || normalizedUri.contains("\\")) {
return error(IOException("Illegal path operator in URI"))
}

while (normalizedUri.contains("//")) {
normalizedUri = normalizedUri.replace("//", "/")
}

if (normalizedUri.startsWith("/")) {
normalizedUri = normalizedUri.substring(1)
}

return ok(normalizedUri)
}

fun <VALUE> catchIOException(consumer: () -> Result<VALUE, ErrorResponse>): Result<VALUE, ErrorResponse> =
try {
consumer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ package org.panda_lang.reposilite.token
import net.dzikoysk.dynamiclogger.Journalist
import net.dzikoysk.dynamiclogger.Logger
import org.panda_lang.reposilite.token.api.AccessToken
import org.panda_lang.reposilite.token.api.AccessTokenPermission
import org.panda_lang.reposilite.token.api.CreateAccessTokenResponse
import org.panda_lang.reposilite.token.api.Permission
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import java.security.SecureRandom
import java.util.Base64
Expand All @@ -34,13 +34,13 @@ class AccessTokenFacade internal constructor(
val B_CRYPT_TOKENS_ENCODER = BCryptPasswordEncoder()
}

fun createAccessToken(alias: String, permissions: Set<Permission> = emptySet()): CreateAccessTokenResponse {
fun createAccessToken(alias: String, permissions: Set<AccessTokenPermission> = emptySet()): CreateAccessTokenResponse {
val randomBytes = ByteArray(48)
SECURE_RANDOM.nextBytes(randomBytes)
return createAccessToken(alias, Base64.getEncoder().encodeToString(randomBytes), permissions)
}

private fun createAccessToken(alias: String, token: String, permissions: Set<Permission>): CreateAccessTokenResponse {
private fun createAccessToken(alias: String, token: String, permissions: Set<AccessTokenPermission>): CreateAccessTokenResponse {
val encodedToken = B_CRYPT_TOKENS_ENCODER.encode(token)

accessTokenRepository.saveAccessToken(AccessToken(alias = alias, secret = encodedToken, permissions = permissions))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,21 @@ data class AccessToken internal constructor(
val secret: String,
val createdAt: LocalDate = LocalDate.now(),
val description: String = "",
val permissions: Set<Permission> = emptySet(),
val permissions: Set<AccessTokenPermission> = emptySet(),
val routes: Set<Route> = emptySet()
) : IdentifiableEntity {

fun hasPermission(permission: Permission): Boolean =
fun hasPermission(permission: AccessTokenPermission): Boolean =
permissions.contains(permission)

fun hasPermissionTo(toPath: String, routePermission: Permission): Boolean =
fun hasPermissionTo(toPath: String, routePermission: RoutePermission): Boolean =
routes.any { it.hasPermissionTo(toPath, routePermission) }

}
}

enum class AccessTokenPermission(val identifier: String) {
MANAGER("access-token:manager");
}

fun findAccessTokenPermissionByIdentifier(identifier: String): AccessTokenPermission =
AccessTokenPermission.values().first { it.identifier == identifier }

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ package org.panda_lang.reposilite.token.api

data class Route internal constructor(
val path: String,
val permissions: Collection<Permission>
val permissions: Set<RoutePermission>
) {

fun hasPermissionTo(toPath: String, routePermission: Permission): Boolean =
fun hasPermissionTo(toPath: String, routePermission: RoutePermission): Boolean =
toPath.startsWith(path) && permissions.contains(routePermission)

}
}

enum class RoutePermission(val identifier: String) {
READ("route:read"),
WRITE("route:write");
}

fun findRoutePermissionByIdentifier(identifier: String): RoutePermission =
RoutePermission.values().first { it.identifier == identifier }
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ import org.panda_lang.reposilite.shared.firstAndMap
import org.panda_lang.reposilite.shared.transactionUnit
import org.panda_lang.reposilite.token.AccessTokenRepository
import org.panda_lang.reposilite.token.api.AccessToken
import org.panda_lang.reposilite.token.api.Permission
import org.panda_lang.reposilite.token.api.PermissionType.ACCESS_TOKEN
import org.panda_lang.reposilite.token.api.PermissionType.ROUTE
import org.panda_lang.reposilite.token.api.AccessTokenPermission
import org.panda_lang.reposilite.token.api.Route
import org.panda_lang.reposilite.token.api.findAccessTokenPermissionByIdentifier
import org.panda_lang.reposilite.token.api.findRoutePermissionByIdentifier

internal class SqlAccessTokenRepository : AccessTokenRepository {

Expand Down Expand Up @@ -73,16 +73,18 @@ internal class SqlAccessTokenRepository : AccessTokenRepository {
override fun deleteAccessToken(accessToken: AccessToken) =
transactionUnit { AccessTokenTable.deleteWhere { AccessTokenTable.id eq accessToken.id } }

private fun findAccessTokenPermissionsById(id: Int): Set<Permission> =
private fun findAccessTokenPermissionsById(id: Int): Set<AccessTokenPermission> =
PermissionToAccessTokenTable.select { PermissionToAccessTokenTable.accessTokenId eq id }
.map { Permission.of(ACCESS_TOKEN, it[PermissionToAccessTokenTable.permission]) }
.map { findAccessTokenPermissionByIdentifier(it[PermissionToAccessTokenTable.permission]) }
.toSet()

private fun findRoutesById(id: Int): Set<Route> =
PermissionToRouteTable.select { PermissionToRouteTable.accessTokenId eq id }
.map { Pair(it[PermissionToRouteTable.route], it[PermissionToRouteTable.permission]) }
.groupBy { it.first }
.map { (route, permissions) -> Route(route, permissions.map { Permission.of(ROUTE, it.second) }) }
.map { (route, permissions) ->
Route(route, permissions.map { findRoutePermissionByIdentifier(it.second) }.toSet())
}
.toSet()

private fun toAccessToken(result: ResultRow): AccessToken =
Expand Down
Loading

0 comments on commit ded4766

Please sign in to comment.