diff --git a/src/main/kotlin/fr/dcproject/application/Application.kt b/src/main/kotlin/fr/dcproject/application/Application.kt index eb55b1d..e769236 100644 --- a/src/main/kotlin/fr/dcproject/application/Application.kt +++ b/src/main/kotlin/fr/dcproject/application/Application.kt @@ -6,17 +6,14 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.joda.JodaModule -import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException import fr.dcproject.application.Env.PROD import fr.dcproject.application.Env.TEST -import fr.dcproject.common.security.AccessDeniedException +import fr.dcproject.application.http.statusPagesInstallation import fr.dcproject.component.article.articleKoinModule import fr.dcproject.component.article.routes.installArticleRoutes -import fr.dcproject.component.auth.ForbiddenException import fr.dcproject.component.auth.authKoinModule import fr.dcproject.component.auth.jwt.jwtInstallation import fr.dcproject.component.auth.routes.installAuthRoutes -import fr.dcproject.component.auth.user import fr.dcproject.component.citizen.citizenKoinModule import fr.dcproject.component.citizen.routes.installCitizenRoutes import fr.dcproject.component.comment.article.routes.installCommentArticleRoutes @@ -41,7 +38,6 @@ import fr.dcproject.component.workgroup.workgroupKoinModule import fr.postgresjson.migration.Migrations import io.ktor.application.Application import io.ktor.application.ApplicationStopped -import io.ktor.application.call import io.ktor.application.install import io.ktor.auth.Authentication import io.ktor.client.HttpClient @@ -51,17 +47,14 @@ import io.ktor.features.CORS import io.ktor.features.CallLogging import io.ktor.features.ContentNegotiation import io.ktor.features.DataConversion -import io.ktor.features.NotFoundException import io.ktor.features.StatusPages import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode import io.ktor.http.cio.websocket.pingPeriod import io.ktor.http.cio.websocket.timeout import io.ktor.jackson.jackson import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Locations -import io.ktor.response.respond import io.ktor.routing.Routing import io.ktor.server.jetty.EngineMain import io.ktor.util.KtorExperimentalAPI @@ -73,7 +66,6 @@ import org.koin.ktor.ext.Koin import org.koin.ktor.ext.get import org.slf4j.event.Level import java.time.Duration -import java.util.concurrent.CompletionException fun main(args: Array): Unit = EngineMain.main(args) @@ -171,26 +163,7 @@ fun Application.module(env: Env = PROD) { installDocRoutes() } - install(StatusPages) { - exception { e -> - val parent = e.cause?.cause - if (parent is GenericDatabaseException) { - call.respond(HttpStatusCode.BadRequest, parent.errorMessage.message!!) - } else { - throw e - } - } - exception { e -> - call.respond(HttpStatusCode.NotFound, e.message!!) - } - exception { - if (call.user == null) call.respond(HttpStatusCode.Unauthorized) - else call.respond(HttpStatusCode.Forbidden) - } - exception { - call.respond(HttpStatusCode.Forbidden) - } - } + install(StatusPages, statusPagesInstallation()) install(CORS) { method(HttpMethod.Options) diff --git a/src/main/kotlin/fr/dcproject/application/http/HttpStatusError.kt b/src/main/kotlin/fr/dcproject/application/http/HttpStatusError.kt new file mode 100644 index 0000000..215add9 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/application/http/HttpStatusError.kt @@ -0,0 +1,82 @@ +package fr.dcproject.application.http + +import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException +import fr.dcproject.common.security.AccessDeniedException +import fr.dcproject.component.auth.ForbiddenException +import fr.dcproject.component.auth.user +import io.ktor.application.call +import io.ktor.features.NotFoundException +import io.ktor.features.StatusPages +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond +import java.util.concurrent.CompletionException + +class HttpError( + statusCode: HttpStatusCode, + val cause: Throwable? = null, + val type: String? = null, + val title: String = cause?.message ?: statusCode.description, + val detail: String? = null, + val invalidParams: List? = null, + val stackTrace: String? = cause?.stackTraceToString() +) { + val statusCode: Int = statusCode.value + data class InvalidParam( + val name: String, + val reason: String + ) +} + +fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = { + exception { e -> + val parent = e.cause?.cause + if (parent is GenericDatabaseException) { + HttpError( + HttpStatusCode.BadRequest, + cause = parent + ).let { + call.respond(HttpStatusCode.BadRequest, it) + } + } else { + HttpError( + HttpStatusCode.BadRequest, + cause = e + ).let { + call.respond(HttpStatusCode.InternalServerError, it) + } + } + } + exception { e -> + HttpError( + HttpStatusCode.NotFound, + cause = e + ).let { + call.respond(HttpStatusCode.NotFound, it) + } + } + exception { e -> + if (call.user == null) { + HttpError( + HttpStatusCode.Unauthorized, + cause = e + ).let { + call.respond(HttpStatusCode.Unauthorized, it) + } + } else { + HttpError( + HttpStatusCode.Forbidden, + cause = e + ).let { + call.respond(HttpStatusCode.Forbidden, it) + } + } + } + exception { e -> + HttpError( + HttpStatusCode.Forbidden, + cause = e + ).let { + call.respond(HttpStatusCode.Forbidden, it) + } + } +} diff --git a/src/main/kotlin/fr/dcproject/component/article/routes/UpsertArticle.kt b/src/main/kotlin/fr/dcproject/component/article/routes/UpsertArticle.kt index 4d408ad..20beaef 100644 --- a/src/main/kotlin/fr/dcproject/component/article/routes/UpsertArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/article/routes/UpsertArticle.kt @@ -8,6 +8,7 @@ import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.Input import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.notification.ArticleUpdateNotification import fr.dcproject.component.notification.Publisher import fr.dcproject.component.workgroup.database.WorkgroupRef @@ -54,6 +55,7 @@ object UpsertArticle { } post { + mustBeAuth() val article = call.convertRequestToEntity() ac.assert { canUpsert(article, citizenOrNull) } repo.upsert(article)?.let { a -> diff --git a/src/main/kotlin/fr/dcproject/component/auth/CitizenContext.kt b/src/main/kotlin/fr/dcproject/component/auth/CitizenContext.kt index 466998a..ee766e1 100644 --- a/src/main/kotlin/fr/dcproject/component/auth/CitizenContext.kt +++ b/src/main/kotlin/fr/dcproject/component/auth/CitizenContext.kt @@ -26,7 +26,21 @@ val ApplicationCall.citizenOrNull: CitizenEntity? GlobalContext.get().koin.get().findByUser(it) } +val ApplicationCall.isAuth: Boolean + get() = citizenOrNull == null + +fun ApplicationCall.mustBeAuth() { + citizenOrNull ?: throw ForbiddenException("No User Connected") +} + val PipelineContext.citizen get() = context.citizen val PipelineContext.citizenOrNull get() = context.citizenOrNull val ApplicationCall.user get() = authentication.principal() + +val PipelineContext.isAuth: Boolean + get() = citizenOrNull == null + +fun PipelineContext.mustBeAuth() { + citizenOrNull ?: throw ForbiddenException("No User Connected") +} diff --git a/src/main/kotlin/fr/dcproject/component/citizen/routes/ChangeMyPassword.kt b/src/main/kotlin/fr/dcproject/component/citizen/routes/ChangeMyPassword.kt index 1c3e302..6a360de 100644 --- a/src/main/kotlin/fr/dcproject/component/citizen/routes/ChangeMyPassword.kt +++ b/src/main/kotlin/fr/dcproject/component/citizen/routes/ChangeMyPassword.kt @@ -6,6 +6,7 @@ import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.database.UserRepository import fr.dcproject.component.auth.database.UserWithPassword +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.CitizenAccessControl import fr.dcproject.component.citizen.database.CitizenRef import io.ktor.application.call @@ -29,6 +30,7 @@ object ChangeMyPassword { fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) { put { + mustBeAuth() ac.assert { canChangePassword(it.citizen, citizenOrNull) } val content = call.receiveOrBadRequest() userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password") diff --git a/src/main/kotlin/fr/dcproject/component/citizen/routes/FindCitizens.kt b/src/main/kotlin/fr/dcproject/component/citizen/routes/FindCitizens.kt index d3b1ef4..6ca46f2 100644 --- a/src/main/kotlin/fr/dcproject/component/citizen/routes/FindCitizens.kt +++ b/src/main/kotlin/fr/dcproject/component/citizen/routes/FindCitizens.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.CitizenAccessControl import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenRepository @@ -30,6 +31,7 @@ object FindCitizens { fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) { get { + mustBeAuth() val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) ac.assert { canView(citizens.result, citizenOrNull) } call.respond( diff --git a/src/main/kotlin/fr/dcproject/component/citizen/routes/GetCurrentCitizen.kt b/src/main/kotlin/fr/dcproject/component/citizen/routes/GetCurrentCitizen.kt index d0b4d3d..4ede796 100644 --- a/src/main/kotlin/fr/dcproject/component/citizen/routes/GetCurrentCitizen.kt +++ b/src/main/kotlin/fr/dcproject/component/citizen/routes/GetCurrentCitizen.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.CitizenAccessControl import io.ktor.application.call import io.ktor.http.HttpStatusCode @@ -22,6 +23,7 @@ object GetCurrentCitizen { fun Route.getCurrentCitizen(ac: CitizenAccessControl) { get { + mustBeAuth() val currentUser = citizenOrNull if (currentUser === null) { call.respond(HttpStatusCode.Unauthorized) diff --git a/src/main/kotlin/fr/dcproject/component/citizen/routes/GetOneCitizen.kt b/src/main/kotlin/fr/dcproject/component/citizen/routes/GetOneCitizen.kt index b8366c2..e49a25b 100644 --- a/src/main/kotlin/fr/dcproject/component/citizen/routes/GetOneCitizen.kt +++ b/src/main/kotlin/fr/dcproject/component/citizen/routes/GetOneCitizen.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.CitizenAccessControl import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRepository @@ -26,6 +27,7 @@ object GetOneCitizen { fun Route.getOneCitizen(ac: CitizenAccessControl, citizenRepository: CitizenRepository) { get { + mustBeAuth() val citizen = citizenRepository.findById(it.citizen.id) ?: throw NotFoundException("Citizen not found ${it.citizen.id}") ac.assert { canView(citizen, citizenOrNull) } diff --git a/src/main/kotlin/fr/dcproject/component/comment/article/routes/CreateCommentArticle.kt b/src/main/kotlin/fr/dcproject/component/comment/article/routes/CreateCommentArticle.kt index f4a103e..fd10799 100644 --- a/src/main/kotlin/fr/dcproject/component/comment/article/routes/CreateCommentArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/comment/article/routes/CreateCommentArticle.kt @@ -6,6 +6,7 @@ import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.comment.article.database.CommentArticleRepository import fr.dcproject.component.comment.article.routes.CreateCommentArticle.PostArticleCommentRequest.Input import fr.dcproject.component.comment.generic.CommentAccessControl @@ -30,6 +31,7 @@ object CreateCommentArticle { fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) { post { + mustBeAuth() call.receiveOrBadRequest().run { CommentForUpdate( target = it.article, diff --git a/src/main/kotlin/fr/dcproject/component/comment/article/routes/GetCitizenArticleComments.kt b/src/main/kotlin/fr/dcproject/component/comment/article/routes/GetCitizenArticleComments.kt index 3e4409f..7aa6d1d 100644 --- a/src/main/kotlin/fr/dcproject/component/comment/article/routes/GetCitizenArticleComments.kt +++ b/src/main/kotlin/fr/dcproject/component/comment/article/routes/GetCitizenArticleComments.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.comment.article.routes import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.comment.article.database.CommentArticleRepository import fr.dcproject.component.comment.generic.CommentAccessControl @@ -25,6 +26,7 @@ object GetCitizenArticleComments { fun Route.getCitizenArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) { get { + mustBeAuth() repo.findByCitizen(it.citizen).let { comments -> ac.assert { canView(comments.result, citizenOrNull) } call.respond( diff --git a/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/CreateConstitutionComment.kt b/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/CreateConstitutionComment.kt index e1034c7..cf368e4 100644 --- a/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/CreateConstitutionComment.kt +++ b/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/CreateConstitutionComment.kt @@ -5,6 +5,7 @@ import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.comment.constitution.database.CommentConstitutionRepository import fr.dcproject.component.comment.constitution.routes.CreateConstitutionComment.CreateConstitutionCommentRequest.Input import fr.dcproject.component.comment.generic.CommentAccessControl @@ -30,6 +31,7 @@ object CreateConstitutionComment { fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) { post { + mustBeAuth() call.receiveOrBadRequest().run { CommentForUpdate( target = it.constitution, diff --git a/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/GetCitizenCommentConstitution.kt b/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/GetCitizenCommentConstitution.kt index 3296bea..9d9b3e2 100644 --- a/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/GetCitizenCommentConstitution.kt +++ b/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/GetCitizenCommentConstitution.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.comment.constitution.routes import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.comment.constitution.database.CommentConstitutionRepository import fr.dcproject.component.comment.generic.CommentAccessControl @@ -25,6 +26,7 @@ object GetCitizenCommentConstitution { fun Route.getCitizenCommentConstitution(repo: CommentConstitutionRepository, ac: CommentAccessControl) { get { + mustBeAuth() val comments = repo.findByCitizen(it.citizen) ac.assert { canView(comments.result, citizenOrNull) } call.respond( diff --git a/src/main/kotlin/fr/dcproject/component/comment/generic/routes/CreateCommentChildren.kt b/src/main/kotlin/fr/dcproject/component/comment/generic/routes/CreateCommentChildren.kt index c05b973..b95d2cd 100644 --- a/src/main/kotlin/fr/dcproject/component/comment/generic/routes/CreateCommentChildren.kt +++ b/src/main/kotlin/fr/dcproject/component/comment/generic/routes/CreateCommentChildren.kt @@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.database.CommentForUpdate import fr.dcproject.component.comment.generic.database.CommentRef @@ -29,6 +30,7 @@ object CreateCommentChildren { fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) { post { + mustBeAuth() val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val newComment = CommentForUpdate( content = call.receiveOrBadRequest().content, diff --git a/src/main/kotlin/fr/dcproject/component/comment/generic/routes/EditComment.kt b/src/main/kotlin/fr/dcproject/component/comment/generic/routes/EditComment.kt index 305fa32..7aa20c0 100644 --- a/src/main/kotlin/fr/dcproject/component/comment/generic/routes/EditComment.kt +++ b/src/main/kotlin/fr/dcproject/component/comment/generic/routes/EditComment.kt @@ -4,6 +4,7 @@ import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.database.CommentRef import fr.dcproject.component.comment.generic.database.CommentRepository @@ -28,6 +29,7 @@ object EditComment { fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) { put { + mustBeAuth() val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") ac.assert { canUpdate(comment, citizenOrNull) } diff --git a/src/main/kotlin/fr/dcproject/component/constitution/routes/CreateConstitution.kt b/src/main/kotlin/fr/dcproject/component/constitution/routes/CreateConstitution.kt index 6e4a7fb..dfc69d0 100644 --- a/src/main/kotlin/fr/dcproject/component/constitution/routes/CreateConstitution.kt +++ b/src/main/kotlin/fr/dcproject/component/constitution/routes/CreateConstitution.kt @@ -6,6 +6,7 @@ import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.Citizen import fr.dcproject.component.citizen.database.CitizenWithUserI import fr.dcproject.component.constitution.ConstitutionAccessControl @@ -68,6 +69,7 @@ object CreateConstitution { fun Route.createConstitution(repo: ConstitutionRepository, ac: ConstitutionAccessControl) { post { + mustBeAuth() getNewConstitution(call.receiveOrBadRequest(), citizen).let { ac.assert { canCreate(it, citizenOrNull) } val c = repo.upsert(it) ?: error("Unable to create Constitution") diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/article/FollowArticle.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/article/FollowArticle.kt index e9fbfc6..f9d82e4 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/routes/article/FollowArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/article/FollowArticle.kt @@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowForUpdate @@ -25,6 +26,7 @@ object FollowArticle { fun Route.followArticle(repo: FollowArticleRepository, ac: FollowAccessControl) { post { + mustBeAuth() val follow = FollowForUpdate(target = it.article, createdBy = this.citizen) ac.assert { canCreate(follow, citizenOrNull) } repo.follow(follow) diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/article/GetMyFollowsArticle.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/article/GetMyFollowsArticle.kt index 29aa22b..ea40bec 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/routes/article/GetMyFollowsArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/article/GetMyFollowsArticle.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.article import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.database.FollowArticleRepository @@ -25,6 +26,7 @@ object GetMyFollowsArticle { fun Route.getMyFollowsArticle(repo: FollowArticleRepository, ac: FollowAccessControl) { get { + mustBeAuth() val follows = repo.findByCitizen(it.citizen) ac.assert { canView(follows.result, citizenOrNull) } call.respond( diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/article/UnfollowArticle.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/article/UnfollowArticle.kt index bf33cec..e90b797 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/routes/article/UnfollowArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/article/UnfollowArticle.kt @@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowForUpdate @@ -25,6 +26,7 @@ object UnfollowArticle { fun Route.unfollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) { delete { + mustBeAuth() val follow = FollowForUpdate(target = it.article, createdBy = this.citizen) ac.assert { canDelete(follow, citizenOrNull) } repo.unfollow(follow) diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/FollowConstitution.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/FollowConstitution.kt index debfbab..cba056c 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/FollowConstitution.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/FollowConstitution.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.database.FollowConstitutionRepository @@ -25,6 +26,7 @@ object FollowConstitution { fun Route.followConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) { post { + mustBeAuth() val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen) ac.assert { canCreate(follow, citizenOrNull) } repo.follow(follow) diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/GetMyFollowsConstitution.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/GetMyFollowsConstitution.kt index bc398f2..6a11a2e 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/GetMyFollowsConstitution.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/GetMyFollowsConstitution.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.database.FollowConstitutionRepository @@ -25,6 +26,7 @@ object GetMyFollowsConstitution { fun Route.getMyFollowsConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) { get { + mustBeAuth() val follows = repo.findByCitizen(it.citizen) ac.assert { canView(follows.result, citizenOrNull) } call.respond( diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/UnfollowConstitution.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/UnfollowConstitution.kt index 7f2f6fe..f892e3c 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/UnfollowConstitution.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/UnfollowConstitution.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.database.FollowConstitutionRepository @@ -25,6 +26,7 @@ object UnfollowConstitution { fun Route.unfollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) { delete { + mustBeAuth() val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen) ac.assert { canDelete(follow, citizenOrNull) } repo.unfollow(follow) diff --git a/src/main/kotlin/fr/dcproject/component/opinion/routes/GetCitizenOpinions.kt b/src/main/kotlin/fr/dcproject/component/opinion/routes/GetCitizenOpinions.kt index f330ce8..3857944 100644 --- a/src/main/kotlin/fr/dcproject/component/opinion/routes/GetCitizenOpinions.kt +++ b/src/main/kotlin/fr/dcproject/component/opinion/routes/GetCitizenOpinions.kt @@ -5,6 +5,7 @@ import fr.dcproject.common.security.assert import fr.dcproject.common.utils.toUUID import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.opinion.OpinionAccessControl import fr.dcproject.component.opinion.database.Opinion @@ -31,6 +32,7 @@ object GetCitizenOpinions { fun Route.getCitizenOpinions(repo: OpinionArticleRepository, ac: OpinionAccessControl) { get { + mustBeAuth() val opinionsEntities: List> = repo.findCitizenOpinionsByTargets(it.citizen, it.id) ac.assert { canView(opinionsEntities, citizenOrNull) } diff --git a/src/main/kotlin/fr/dcproject/component/opinion/routes/GetMyOpinionsArticle.kt b/src/main/kotlin/fr/dcproject/component/opinion/routes/GetMyOpinionsArticle.kt index bfd7d20..7679ace 100644 --- a/src/main/kotlin/fr/dcproject/component/opinion/routes/GetMyOpinionsArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/opinion/routes/GetMyOpinionsArticle.kt @@ -5,6 +5,7 @@ import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.opinion.OpinionAccessControl import fr.dcproject.component.opinion.database.Opinion @@ -37,6 +38,7 @@ object GetMyOpinionsArticle { fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) { get { + mustBeAuth() val opinions: Paginated> = repo.findCitizenOpinions(citizen, it.page, it.limit) ac.assert { canView(opinions.result, citizenOrNull) } call.respond( diff --git a/src/main/kotlin/fr/dcproject/component/opinion/routes/OpinionArticle.kt b/src/main/kotlin/fr/dcproject/component/opinion/routes/OpinionArticle.kt index 4d4d5b9..210671d 100644 --- a/src/main/kotlin/fr/dcproject/component/opinion/routes/OpinionArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/opinion/routes/OpinionArticle.kt @@ -6,6 +6,7 @@ import fr.dcproject.common.utils.toUUID import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.opinion.OpinionAccessControl import fr.dcproject.component.opinion.database.OpinionChoiceRef import fr.dcproject.component.opinion.database.OpinionForUpdate @@ -34,6 +35,7 @@ object OpinionArticle { fun Route.setOpinionOnArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) { put { + mustBeAuth() call.receiveOrBadRequest().ids.map { id -> OpinionForUpdate( choice = OpinionChoiceRef(id), diff --git a/src/main/kotlin/fr/dcproject/component/vote/routes/GetCitizenVotes.kt b/src/main/kotlin/fr/dcproject/component/vote/routes/GetCitizenVotes.kt index 9524a29..d4920ee 100644 --- a/src/main/kotlin/fr/dcproject/component/vote/routes/GetCitizenVotes.kt +++ b/src/main/kotlin/fr/dcproject/component/vote/routes/GetCitizenVotes.kt @@ -4,6 +4,7 @@ import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.common.utils.toUUID import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.database.VoteRepository @@ -26,6 +27,7 @@ object GetCitizenVotes { fun Route.getCitizenVote(repo: VoteRepository, ac: VoteAccessControl) { get { + mustBeAuth() val votes = repo.findCitizenVotesByTargets(it.citizen, it.id) if (votes.isNotEmpty()) { ac.assert { canView(votes, citizenOrNull) } diff --git a/src/main/kotlin/fr/dcproject/component/vote/routes/GetCitizenVotesOnArticle.kt b/src/main/kotlin/fr/dcproject/component/vote/routes/GetCitizenVotesOnArticle.kt index 5328f1f..d8b5654 100644 --- a/src/main/kotlin/fr/dcproject/component/vote/routes/GetCitizenVotesOnArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/vote/routes/GetCitizenVotesOnArticle.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.vote.routes import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.database.VoteArticleRepository @@ -31,6 +32,7 @@ object GetCitizenVotesOnArticle { fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) { get { + mustBeAuth() val votes = repo.findByCitizen(it.citizen, it.page, it.limit) ac.assert { canView(votes.result, citizenOrNull) } diff --git a/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnArticle.kt b/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnArticle.kt index 85212ad..8f45229 100644 --- a/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnArticle.kt @@ -6,6 +6,7 @@ import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.database.VoteArticleRepository import fr.dcproject.component.vote.database.VoteForUpdate @@ -29,6 +30,7 @@ object PutVoteOnArticle { fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) { put { + mustBeAuth() val input = call.receiveOrBadRequest() val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found") val vote = VoteForUpdate( diff --git a/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnComment.kt b/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnComment.kt index 6d65e4e..8884047 100644 --- a/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnComment.kt +++ b/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnComment.kt @@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.database.VoteCommentRepository @@ -26,6 +27,7 @@ object PutVoteOnComment { fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, ac: VoteAccessControl) { put { + mustBeAuth() val comment = commentRepo.findById(it.comment)!! val content = call.receiveOrBadRequest() val vote = VoteForUpdate( diff --git a/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnConstitution.kt b/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnConstitution.kt index c643e57..03c30ec 100644 --- a/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnConstitution.kt +++ b/src/main/kotlin/fr/dcproject/component/vote/routes/PutVoteOnConstitution.kt @@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRepository import fr.dcproject.component.vote.VoteAccessControl @@ -30,6 +31,7 @@ object PutVoteOnConstitution { fun Route.voteConstitution(repo: VoteConstitutionRepository, ac: VoteAccessControl, constitutionRepo: ConstitutionRepository) { put { + mustBeAuth() val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}") val content = call.receiveOrBadRequest() val vote = VoteForUpdate( diff --git a/src/main/kotlin/fr/dcproject/component/workgroup/routes/CreateWorkgroup.kt b/src/main/kotlin/fr/dcproject/component/workgroup/routes/CreateWorkgroup.kt index e97128e..4c7f2b1 100644 --- a/src/main/kotlin/fr/dcproject/component/workgroup/routes/CreateWorkgroup.kt +++ b/src/main/kotlin/fr/dcproject/component/workgroup/routes/CreateWorkgroup.kt @@ -5,6 +5,7 @@ import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.database.WorkgroupForUpdate import fr.dcproject.component.workgroup.database.WorkgroupRepository @@ -33,6 +34,7 @@ object CreateWorkgroup { fun Route.createWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { post { + mustBeAuth() call.receiveOrBadRequest().run { WorkgroupForUpdate( id ?: UUID.randomUUID(), diff --git a/src/main/kotlin/fr/dcproject/component/workgroup/routes/DeleteWorkgroup.kt b/src/main/kotlin/fr/dcproject/component/workgroup/routes/DeleteWorkgroup.kt index 17fbdcd..cb6788d 100644 --- a/src/main/kotlin/fr/dcproject/component/workgroup/routes/DeleteWorkgroup.kt +++ b/src/main/kotlin/fr/dcproject/component/workgroup/routes/DeleteWorkgroup.kt @@ -2,6 +2,7 @@ package fr.dcproject.component.workgroup.routes import fr.dcproject.common.security.assert import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.database.WorkgroupRepository import io.ktor.application.call @@ -20,6 +21,7 @@ object DeleteWorkgroup { fun Route.deleteWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { delete { + mustBeAuth() repo.findById(it.workgroupId)?.let { workgroup -> ac.assert { canDelete(workgroup, citizenOrNull) } repo.delete(workgroup) diff --git a/src/main/kotlin/fr/dcproject/component/workgroup/routes/EditWorkgroup.kt b/src/main/kotlin/fr/dcproject/component/workgroup/routes/EditWorkgroup.kt index b4c61f0..4d96298 100644 --- a/src/main/kotlin/fr/dcproject/component/workgroup/routes/EditWorkgroup.kt +++ b/src/main/kotlin/fr/dcproject/component/workgroup/routes/EditWorkgroup.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.database.WorkgroupForUpdate import fr.dcproject.component.workgroup.database.WorkgroupRepository @@ -31,6 +32,7 @@ object EditWorkgroup { fun Route.editWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { put { + mustBeAuth() repo.findById(it.workgroupId)?.let { old -> call.receiveOrBadRequest().run { WorkgroupForUpdate( diff --git a/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/AddMemberToWorkgroup.kt b/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/AddMemberToWorkgroup.kt index dd73f81..add4574 100644 --- a/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/AddMemberToWorkgroup.kt +++ b/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/AddMemberToWorkgroup.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.database.WorkgroupRepository @@ -44,6 +45,7 @@ object AddMemberToWorkgroup { fun Route.addMemberToWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { /* Add members to workgroup */ post { + mustBeAuth() repo.findById(it.workgroupId)?.let { workgroup -> call.getMembersFromRequest().let { members -> ac.assert { canAddMembers(workgroup, citizenOrNull) } diff --git a/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/DeleteMembersOfWorkgroup.kt b/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/DeleteMembersOfWorkgroup.kt index fb22715..daf93b1 100644 --- a/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/DeleteMembersOfWorkgroup.kt +++ b/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/DeleteMembersOfWorkgroup.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.database.WorkgroupRepository @@ -35,6 +36,7 @@ object DeleteMembersOfWorkgroup { fun Route.deleteMemberOfWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { /* Delete members of workgroup */ delete { + mustBeAuth() repo.findById(it.workgroupId)?.let { workgroup -> call.getMembersFromRequest() .let { members -> diff --git a/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/UpdateMemberOfWorkgroup.kt b/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/UpdateMemberOfWorkgroup.kt index b36a2b3..885a789 100644 --- a/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/UpdateMemberOfWorkgroup.kt +++ b/src/main/kotlin/fr/dcproject/component/workgroup/routes/members/UpdateMemberOfWorkgroup.kt @@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members import fr.dcproject.common.security.assert import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.database.WorkgroupRepository @@ -42,6 +43,7 @@ object UpdateMemberOfWorkgroup { fun Route.updateMemberOfWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { /* Update members of workgroup */ put { + mustBeAuth() repo.findById(it.workgroupId)?.let { workgroup -> call.getMembersFromRequest().let { members -> ac.assert { canUpdateMembers(workgroup, citizenOrNull) } diff --git a/src/main/resources/openapi.yaml b/src/main/resources/openapi.yaml index e016f54..b204c7e 100644 --- a/src/main/resources/openapi.yaml +++ b/src/main/resources/openapi.yaml @@ -108,12 +108,22 @@ paths: type: integer 401: $ref: '#/components/responses/401' + 403: + description: Forbiden + content: + application/json: + schema: + description: Forbiden + properties: + statusCode: + type: integer + title: + type: string + /articles/{article}: parameters: - $ref: '#/components/parameters/article' get: - security: - - JWTAuth: [] summary: Get one article tags: - article @@ -1126,8 +1136,6 @@ paths: /workgroups: get: summary: Get all Workgroup (Paginated) - security: - - JWTAuth: [ ] tags: - workgroup parameters: @@ -1194,8 +1202,6 @@ paths: - $ref: '#/components/parameters/workgroup' get: summary: Get one workgroup by ID - security: - - JWTAuth: [ ] tags: - workgroup responses: diff --git a/src/test/kotlin/integration/Article routes.kt b/src/test/kotlin/integration/Article routes.kt index 5c9140b..661e4f9 100644 --- a/src/test/kotlin/integration/Article routes.kt +++ b/src/test/kotlin/integration/Article routes.kt @@ -12,11 +12,13 @@ import integration.steps.given.`authenticated as` import integration.steps.then.`And have property` import integration.steps.then.`And the response should contain list` import integration.steps.then.`And the response should contain pattern` +import integration.steps.then.`And the response should contain` import integration.steps.then.`And the response should not be null` import integration.steps.then.`And the response should not contain` import integration.steps.then.`Then the response should be` import integration.steps.then.`whish contains` import integration.steps.then.and +import io.ktor.http.HttpStatusCode.Companion.Forbidden import io.ktor.http.HttpStatusCode.Companion.OK import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags @@ -104,4 +106,30 @@ class `Article routes` : BaseTest() { } } } + + @Test + fun `I cannot create an article if I'm not connected`() { + withIntegrationApplication { + `When I send a POST request`("/articles") { + `with body`( + """ + { + "versionId": "e3c7ce42-241c-4caf-9a59-aba4e466440e", + "title": "title2", + "anonymous": false, + "content": "content2", + "description": "description2", + "tags": [ + "green" + ] + } + """ + ) + } `Then the response should be` Forbidden and { + `And the response should not be null`() + `And the response should contain`("$.statusCode", 403) + `And the response should contain`("$.title", "No User Connected") + } + } + } } diff --git a/src/test/kotlin/integration/Check auth on all routes.kt b/src/test/kotlin/integration/Check auth on all routes.kt new file mode 100644 index 0000000..e0400d8 --- /dev/null +++ b/src/test/kotlin/integration/Check auth on all routes.kt @@ -0,0 +1,147 @@ +package integration + +import fr.dcproject.common.utils.getResource +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.openapi4j.core.model.OAIContext +import org.openapi4j.parser.OpenApi3Parser +import org.openapi4j.parser.model.v3.OpenApi3 +import org.openapi4j.parser.model.v3.Operation +import org.openapi4j.parser.model.v3.Parameter +import org.openapi4j.parser.model.v3.Path +import java.io.File +import java.util.UUID +import kotlin.test.assertTrue + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tags(Tag("integration"), Tag("auth")) +class `Check auth on all routes` : BaseTest() { + @Test + fun `Check all routes`() { + val filePath = "/openapi.yaml" + OpenApi3Parser().parse(File(filePath.getResource().toURI()), true).let { api: OpenApi3 -> + /* Loop on paths and http methods */ + api.paths.flatMap { (pathName: String, path: Path) -> + path.operations + /* Take only the secure route */ + .filter { (_, operation: Operation) -> operation.hasSecurityRequirements() } + .map { (methodName, _) -> + /* Send request to check security */ + sendRequest( + path.buildUrl(pathName, methodName, api.context), /* Replace route to real URL */ + HttpMethod.parse(methodName.toUpperCase()) /* Convert http method name to enum */ + ) + } + }.let { requests -> + /* Check security of routes */ + assertTrue( + requests.all { it.statusCode == HttpStatusCode.Forbidden }, + requests + .filter { it.statusCode != HttpStatusCode.Forbidden } + .joinToString("\n") { it.toString() } + ) + } + } + } + + private fun sendRequest(uri: String, method: HttpMethod): RequestResponse { + return try { + withIntegrationApplication { + handleRequest(true) { + this.method = method + this.uri = uri + addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + addHeader(HttpHeaders.Accept, ContentType.Application.Json.toString()) + }.run { + RequestResponse( + response.status() ?: error("Request error"), + method, + uri + ) + } + } + } catch (e: Throwable) { + RequestResponse( + HttpStatusCode.InternalServerError, + method, + uri + ) + } + } + + private data class RequestResponse( + val statusCode: HttpStatusCode, + val method: HttpMethod, + val uri: String + ) { + override fun toString(): String { + return """HttpStatus ${statusCode.value} for: ${method.value.padStart(6, ' ')} $uri""" + } + } +} + +private fun Path.buildUrl(path: String, methodName: String, context: OAIContext): String { + val urlReplaced = this.getParametersIn(context, "path") + .fold(path) { pathToReplace: String, parameter: Parameter -> + """\{${parameter.name}}""".toRegex().replace( + pathToReplace, + parameter.generateFakeValue() + ) + } + + val rootQueryParameters = this.getParametersIn(context, "query") + .filter { it.isRequired } + .map { parameter -> + parameter + .generateFakeArray() + .joinToString("&") { "${parameter.name}=$it" } + } + + val queryParameters = this.getOperation(methodName).getParametersIn(context, "query") + .filter { it.isRequired } + .map { parameter -> + parameter + .generateFakeArray() + .joinToString("&") { "${parameter.name}=$it" } + } + val allParameters: String = (rootQueryParameters + queryParameters) + .joinToString("&") + .let { + if (it.isNotEmpty()) { + "?$it" + } else { + it + } + } + + return "$urlReplaced$allParameters" +} + +private fun Parameter.generateFakeValue(): String { + return if (example != null) { + example.toString() + } else if (schema.type == "string" && schema.format == "uuid") { + UUID.randomUUID().toString() + } else { + "example123" + } +} + +private fun Parameter.generateFakeArray(): List { + if (schema.type != "array") { + error("Parameter is not an array") + } + return if (example != null && example is Iterable<*>) { + (example as Iterable<*>).map { it.toString() } + } else if (schema.itemsSchema.type == "string" && schema.itemsSchema.format == "uuid") { + listOf(UUID.randomUUID().toString()) + } else { + listOf("example123") + } +} diff --git a/src/test/kotlin/integration/Comment articles routes.kt b/src/test/kotlin/integration/Comment articles routes.kt index 74e8107..6c7c00e 100644 --- a/src/test/kotlin/integration/Comment articles routes.kt +++ b/src/test/kotlin/integration/Comment articles routes.kt @@ -84,6 +84,7 @@ class `Comment articles routes` : BaseTest() { `Given I have article`(id = "17df7fb9-b388-4e20-ab19-29c29972da01", createdBy = Name("Erwin", "Schrodinger")) `Given I have comment on article`(article = "17df7fb9-b388-4e20-ab19-29c29972da01", createdBy = Name("Erwin", "Schrodinger")) `When I send a GET request`("/citizens/292a20cc-4a60-489e-9866-a95d38ffaf47/comments/articles") { + `authenticated as`("Erwin", "Schrodinger") } `Then the response should be` OK and { `And the response should not be null`() `And the response should contain`("$.currentPage", 1) diff --git a/src/test/kotlin/integration/Comment constitutions routes.kt b/src/test/kotlin/integration/Comment constitutions routes.kt index 8af64ed..f887ce1 100644 --- a/src/test/kotlin/integration/Comment constitutions routes.kt +++ b/src/test/kotlin/integration/Comment constitutions routes.kt @@ -50,6 +50,7 @@ class `Comment constitutions routes` : BaseTest() { `Given I have constitution`(id = "34ddd50a-da00-4a90-a869-08baa2a121be", createdBy = Name("Charles", "Darwin")) `Given I have comment on constitution`(constitution = "34ddd50a-da00-4a90-a869-08baa2a121be", createdBy = Name("Charles", "Darwin")) `When I send a GET request`("/citizens/46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5/comments/constitutions") { + `authenticated as`("Charles", "Darwin") } `Then the response should be` OK and { `And the response should not be null`() `And the response should contain`("$.currentPage", 1) diff --git a/src/test/kotlin/integration/steps/then/schema.kt b/src/test/kotlin/integration/steps/then/schema.kt index bde3e69..0b1ceb2 100644 --- a/src/test/kotlin/integration/steps/then/schema.kt +++ b/src/test/kotlin/integration/steps/then/schema.kt @@ -63,8 +63,9 @@ fun TestApplicationResponse.`And the schema response body must be valid`(content val schema = response.getContentMediaType(contentType.toString())?.schema if (content != null) { + val httpMethod = call.request.httpMethod.value schema?.validate(api, responseContent) - ?: fail("""No Status "${status.value}" found with media type "$contentType" for "$this $uri".""") + ?: fail("""No Status "${status.value}" found with media type "$contentType" for "$httpMethod $uri".""") } } }