Skip to content

Commit

Permalink
gh-2: adds access control and users controller
Browse files Browse the repository at this point in the history
  • Loading branch information
pmhsfelix committed Oct 13, 2022
1 parent 9f863af commit 05f86b8
Show file tree
Hide file tree
Showing 17 changed files with 479 additions and 10 deletions.
4 changes: 2 additions & 2 deletions code/tic-tac-tow-service/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,8 @@ ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 5
ij_kotlin_name_count_to_use_star_import_for_members = 3
ij_kotlin_name_count_to_use_star_import = 50
ij_kotlin_name_count_to_use_star_import_for_members = 30
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,51 @@
package pt.isel.daw.tictactow

import org.jdbi.v3.core.Jdbi
import org.postgresql.ds.PGSimpleDataSource
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import pt.isel.daw.tictactow.http.pipeline.AuthenticationInterceptor
import pt.isel.daw.tictactow.http.pipeline.UserArgumentResolver
import pt.isel.daw.tictactow.repository.jdbi.configure
import pt.isel.daw.tictactow.utils.Sha256TokenEncoder

@SpringBootApplication
class TicTacTowApplication
class TicTacTowApplication {
@Bean
fun jdbi() = Jdbi.create(
PGSimpleDataSource().apply {
setURL("jdbc:postgresql://localhost:5432/db?user=dbuser&password=changeit")
}
).configure()

@Bean
fun passwordEncoder() = BCryptPasswordEncoder()

@Bean
fun tokenEncoder() = Sha256TokenEncoder()
}

// QUESTION: why cannot this be in TicTacTowApplication
@Configuration
class PipelineConfigurer(
val authenticationInterceptor: AuthenticationInterceptor,
val userArgumentResolver: UserArgumentResolver,
) : WebMvcConfigurer {

override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(authenticationInterceptor)
}

override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(userArgumentResolver)
}
}

fun main(args: Array<String>) {
runApplication<TicTacTowApplication>(*args)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package pt.isel.daw.tictactow.domain

import org.springframework.stereotype.Component
import java.security.SecureRandom
import java.util.*

@Component
class UserLogic {

fun generateToken(): String =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ object Uris {
const val USER_HOME = "/me"
const val GAME_BY_ID = "/games/{gid}"

const val USERS_CREATE = "/users"
const val USERS_TOKEN = "/users/token"
const val USERS_GET_BY_ID = "/users/{id}"

fun home(): URI = URI(HOME)
fun userHome(): URI = URI(USER_HOME)
fun gameById(game: Game) = UriTemplate(GAME_BY_ID).expand(game.id)

fun userById(id: String) = UriTemplate(USERS_GET_BY_ID).expand(id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package pt.isel.daw.tictactow.http

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import pt.isel.daw.tictactow.Either
import pt.isel.daw.tictactow.domain.User
import pt.isel.daw.tictactow.http.model.Problem
import pt.isel.daw.tictactow.http.model.UserCreateInputModel
import pt.isel.daw.tictactow.http.model.UserCreateTokenInputModel
import pt.isel.daw.tictactow.http.model.UserHomeOutputModel
import pt.isel.daw.tictactow.http.model.UserTokenCreateOutputModel
import pt.isel.daw.tictactow.service.TokenCreationError
import pt.isel.daw.tictactow.service.UserCreationError
import pt.isel.daw.tictactow.service.UsersService

@RestController
class UsersController(
private val userService: UsersService
) {

@PostMapping(Uris.USERS_CREATE)
fun create(@RequestBody input: UserCreateInputModel): ResponseEntity<*> {
val res = userService.createUser(input.username, input.password)
return when (res) {
is Either.Right -> ResponseEntity.status(201)
.header(
"Location",
Uris.userById(res.value).toASCIIString()
).build<Unit>()
is Either.Left -> when (res.value) {
UserCreationError.InsecurePassword -> Problem.response(400, Problem.insecurePassword)
UserCreationError.UserAlreadyExists -> Problem.response(400, Problem.userAlreadyExists)
}
}
}

@PostMapping(Uris.USERS_TOKEN)
fun token(@RequestBody input: UserCreateTokenInputModel): ResponseEntity<*> {
val res = userService.createToken(input.username, input.password)
return when (res) {
is Either.Right -> ResponseEntity.status(200)
.body(UserTokenCreateOutputModel(res.value))
is Either.Left -> when (res.value) {
TokenCreationError.UserOrPasswordAreInvalid -> Problem.response(400, Problem.userOrPasswordAreInvalid)
}
}
}

@GetMapping(Uris.USERS_GET_BY_ID)
fun getById(@PathVariable id: String) {
TODO("TODO")
}

@GetMapping(Uris.USER_HOME)
fun getUserHome(user: User): UserHomeOutputModel {
return UserHomeOutputModel(
id = user.id.toString(),
username = user.username,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package pt.isel.daw.tictactow.http.model

import org.springframework.http.ResponseEntity
import java.net.URI

class Problem(
typeUri: URI
) {
val type = typeUri.toASCIIString()

companion object {
const val MEDIA_TYPE = "application/problem+json"
fun response(status: Int, problem: Problem) = ResponseEntity
.status(status)
.header("Content-Type", MEDIA_TYPE)
.body(problem)

val userAlreadyExists = Problem(
URI(
"https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/tic-tac-tow-service/" +
"docs/problems/user-already-exists"
)
)
val insecurePassword = Problem(
URI(
"https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/tic-tac-tow-service/" +
"docs/problems/insecure-password"
)
)

val userOrPasswordAreInvalid = Problem(
URI(
"https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/tic-tac-tow-service/" +
"docs/problems/user-or-password-are-invalid"
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package pt.isel.daw.tictactow.http.model

data class UserCreateInputModel(
val username: String,
val password: String,
)

data class UserCreateTokenInputModel(
val username: String,
val password: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package pt.isel.daw.tictactow.http.model

class UserTokenCreateOutputModel(
val token: String
)

class UserHomeOutputModel(
val id: String,
val username: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package pt.isel.daw.tictactow.http.pipeline

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor
import pt.isel.daw.tictactow.domain.User
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class AuthenticationInterceptor(
private val authorizationHeaderProcessor: AuthorizationHeaderProcessor
) : HandlerInterceptor {

override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
if (handler is HandlerMethod && handler.methodParameters.any { it.parameterType == User::class.java }
) {
// enforce authentication
val user = authorizationHeaderProcessor.process(request.getHeader(NAME_AUTHORIZATION_HEADER))
if (user == null) {
response.status = 401
response.addHeader(NAME_WWW_AUTHENTICATE_HEADER, AuthorizationHeaderProcessor.SCHEME)
return false
} else {
UserArgumentResolver.addUserTo(user, request)
return true
}
}

return true
}

companion object {
private val logger = LoggerFactory.getLogger(AuthenticationInterceptor::class.java)
private const val NAME_AUTHORIZATION_HEADER = "Authorization"
private const val NAME_WWW_AUTHENTICATE_HEADER = "WWW-Authenticate"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package pt.isel.daw.tictactow.http.pipeline

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import pt.isel.daw.tictactow.domain.User
import pt.isel.daw.tictactow.service.UsersService

@Component
class AuthorizationHeaderProcessor(
val usersService: UsersService
) {

fun process(authorizationValue: String?): User? {
if (authorizationValue == null) {
return null
}
val parts = authorizationValue.trim().split(" ")
if (parts.size != 2) {
return null
}
if (parts[0].lowercase() != SCHEME) {
return null
}
return usersService.getUserByToken(parts[1])
}

companion object {
private val logger = LoggerFactory.getLogger(AuthorizationHeaderProcessor::class.java)
const val SCHEME = "bearer"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package pt.isel.daw.tictactow.http.pipeline

import org.springframework.core.MethodParameter
import org.springframework.stereotype.Component
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer
import pt.isel.daw.tictactow.domain.User
import javax.servlet.http.HttpServletRequest

@Component
class UserArgumentResolver : HandlerMethodArgumentResolver {

override fun supportsParameter(parameter: MethodParameter) = parameter.parameterType == User::class.java

override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): Any? {
val request = webRequest.getNativeRequest(HttpServletRequest::class.java)
?: throw IllegalStateException("TODO")
return getUserFrom(request) ?: throw IllegalStateException("TODO")
}

companion object {
private const val KEY = "UserArgumentResolver"

fun addUserTo(user: User, request: HttpServletRequest) {
return request.setAttribute(KEY, user)
}

fun getUserFrom(request: HttpServletRequest): User? {
return request.getAttribute(KEY)?.let {
it as? User
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface UsersRepository {
fun storeUser(
username: String,
passwordValidation: PasswordValidationInfo,
): Boolean
): String

fun getUserByUsername(username: String): User?

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package pt.isel.daw.tictactow.repository.jdbi

import org.jdbi.v3.core.Jdbi
import org.springframework.stereotype.Component
import pt.isel.daw.tictactow.repository.Transaction
import pt.isel.daw.tictactow.repository.TransactionManager

@Component
class JdbiTransactionManager(
private val jdbi: Jdbi
) : TransactionManager {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@ class JdbiUsersRepository(
.mapTo<User>()
.singleOrNull()

override fun storeUser(username: String, passwordValidation: PasswordValidationInfo): Boolean =
override fun storeUser(username: String, passwordValidation: PasswordValidationInfo): String =
handle.createUpdate(
"""
insert into dbo.Users (username, password_validation) values (:username, :password_validation)
"""
)
.bind("username", username)
.bind("password_validation", passwordValidation.validationInfo)
.execute() == 1
.executeAndReturnGeneratedKeys()
.mapTo<Int>()
.one()
.toString()

override fun isUserStoredByUsername(username: String): Boolean =
handle.createQuery("select count(*) from dbo.Users where username = :username")
Expand Down
Loading

0 comments on commit 05f86b8

Please sign in to comment.