diff --git a/src/main/kotlin/eu/fosil/okupamicoche/dto/CreateUserDto.kt b/src/main/kotlin/eu/fosil/okupamicoche/dto/CreateUserDto.kt index 4dc920a..d20cc57 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/dto/CreateUserDto.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/dto/CreateUserDto.kt @@ -1,22 +1,24 @@ package eu.fosil.okupamicoche.dto import eu.fosil.okupamicoche.entities.User -import eu.fosil.okupamicoche.repositories.UserRepository class CreateUserDto( private val id: String, + private val username: String, private val name: String, private val email: String? ) { constructor(user: User) : this( user.id, + user.username, user.name, user.email ) - fun toUser(userRepository: UserRepository): User { + fun toUser(): User { return User( id = id, + username = username, name = name, email = email ) diff --git a/src/main/kotlin/eu/fosil/okupamicoche/dto/UserDto.kt b/src/main/kotlin/eu/fosil/okupamicoche/dto/UserDto.kt index 85687fa..8eaebda 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/dto/UserDto.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/dto/UserDto.kt @@ -7,6 +7,7 @@ import eu.fosil.okupamicoche.repositories.UserRepository class UserDto( // Los campos deben ser públicos para que aparezcan en el JSON val id: UserId, + val username: String, val matrixId: String?, val name: String, val email: String?, @@ -15,6 +16,7 @@ class UserDto( ) { constructor(user: User) : this( user.id, + user.username, user.matrixId, user.name, user.email, @@ -25,6 +27,7 @@ class UserDto( fun toUser(userRepository: UserRepository): User { return User( id, + username, matrixId, name, email, diff --git a/src/main/kotlin/eu/fosil/okupamicoche/dto/UserInfoDto.kt b/src/main/kotlin/eu/fosil/okupamicoche/dto/UserInfoDto.kt index 86f46d1..ed6f522 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/dto/UserInfoDto.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/dto/UserInfoDto.kt @@ -2,16 +2,17 @@ package eu.fosil.okupamicoche.dto import eu.fosil.okupamicoche.entities.User import eu.fosil.okupamicoche.entities.UserId -import eu.fosil.okupamicoche.repositories.UserRepository class UserInfoDto( // Los campos deben ser públicos para que aparezcan en el JSON val id: UserId, + val username: String, val matrixId: String?, val name: String ) { constructor(user: User) : this( user.id, + user.username, user.matrixId, user.name ) @@ -19,6 +20,7 @@ class UserInfoDto( fun toUser(): User { return User( id, + username, matrixId, name ) diff --git a/src/main/kotlin/eu/fosil/okupamicoche/entities/Exceptions.kt b/src/main/kotlin/eu/fosil/okupamicoche/entities/Exceptions.kt index ac38e0d..95de2ae 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/entities/Exceptions.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/entities/Exceptions.kt @@ -1,5 +1,9 @@ package eu.fosil.okupamicoche.entities -class UserIdNotFoundException : Exception() +class UserIdNotFoundException(msg: String = "") : Exception(msg) -class UserNotSpecifiedException : Exception() \ No newline at end of file +class InsufficientPermissions(msg: String = "") : Exception(msg) + +class UserNotSpecifiedException(msg: String = "") : Exception(msg) + +class CannotDuplicateIdException(msg: String = "") : Exception(msg) \ No newline at end of file diff --git a/src/main/kotlin/eu/fosil/okupamicoche/entities/User.kt b/src/main/kotlin/eu/fosil/okupamicoche/entities/User.kt index 9231dc0..68a766b 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/entities/User.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/entities/User.kt @@ -7,6 +7,7 @@ typealias UserId = String @Entity class User( @Id var id: UserId, + var username: String, var matrixId: String? = null, var name: String, var email: String? = null, diff --git a/src/main/kotlin/eu/fosil/okupamicoche/entities/UserKeycloak.kt b/src/main/kotlin/eu/fosil/okupamicoche/entities/UserKeycloak.kt new file mode 100644 index 0000000..11fe68c --- /dev/null +++ b/src/main/kotlin/eu/fosil/okupamicoche/entities/UserKeycloak.kt @@ -0,0 +1,31 @@ +package eu.fosil.okupamicoche.entities + +class UserKeycloak( + val id: UserId, + val username: String, + val admin: Boolean, + val name: String, + val email: String +) { + constructor(claims: Map) : this( + claims["sub"].toString(), + claims["preferred_username"].toString(), + try { + claims["admin"] as Boolean + } catch (e: Exception) { + false + }, + claims["name"].toString(), + claims["email"].toString() + ) + + fun toUser(): User { + return User( + id, + username, + "@$username:fosil.eu", + name, + email + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/ApiRestController.kt b/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/ApiRestController.kt index d35adf5..0aca0d6 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/ApiRestController.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/ApiRestController.kt @@ -2,9 +2,6 @@ package eu.fosil.okupamicoche.spring.controller import eu.fosil.okupamicoche.entities.ApiErrorResponse import eu.fosil.okupamicoche.entities.ApiResponse -import eu.fosil.okupamicoche.entities.UserIdNotFoundException -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.oauth2.jwt.Jwt interface ApiRestController { fun response(function: () -> T): ApiResponse { @@ -16,28 +13,4 @@ interface ApiRestController { ApiErrorResponse(e.javaClass.canonicalName, e.message) } } - - /** - * Devuelve el id del usuario actual. - */ - fun getCurrentUserId(): String { - val authentication = SecurityContextHolder.getContext().authentication - if (authentication.principal is Jwt) { - val jwt = authentication.principal as Jwt - return jwt.claims["sub"].toString() - } - throw UserIdNotFoundException() - } - - /** - * Devuelve el id del usuario actual. - */ - fun getCurrentUserClaims(): Map { - val authentication = SecurityContextHolder.getContext().authentication - if (authentication.principal is Jwt) { - val jwt = authentication.principal as Jwt - return jwt.claims - } - throw UserIdNotFoundException() - } } \ No newline at end of file diff --git a/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/PrivateTravelRestController.kt b/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/PrivateTravelRestController.kt index 97c9e4a..33260a5 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/PrivateTravelRestController.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/PrivateTravelRestController.kt @@ -3,12 +3,10 @@ package eu.fosil.okupamicoche.spring.controller import eu.fosil.okupamicoche.dto.ListDto import eu.fosil.okupamicoche.dto.TravelDto import eu.fosil.okupamicoche.dto.UserInfoDto -import eu.fosil.okupamicoche.entities.ApiResponse -import eu.fosil.okupamicoche.entities.TravelId -import eu.fosil.okupamicoche.entities.UserId -import eu.fosil.okupamicoche.entities.UserIdNotFoundException +import eu.fosil.okupamicoche.entities.* import eu.fosil.okupamicoche.repositories.TravelRepository import eu.fosil.okupamicoche.repositories.UserRepository +import eu.fosil.okupamicoche.spring.services.AuthService import eu.fosil.okupamicoche.usecases.travel.* import org.springframework.data.repository.findByIdOrNull import org.springframework.validation.annotation.Validated @@ -17,6 +15,7 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/api/travel") class PrivateTravelRestController( + private val authService: AuthService, private val userRepository: UserRepository, private val travelRepository: TravelRepository ) : ApiRestController { @@ -24,7 +23,10 @@ class PrivateTravelRestController( @RequestMapping("/create") fun createTravel(@RequestBody @Validated travel: TravelDto): ApiResponse { return response { - val driver = userRepository.findByIdOrNull(getCurrentUserId()) ?: throw UserIdNotFoundException() + val driver = userRepository.findByIdOrNull(authService.currentUser().id) + ?: throw UserIdNotFoundException("Current user not found.") + if (travelRepository.findByIdOrNull(travel.id) != null) + throw CannotDuplicateIdException("Travel id already exists.") println("travel des=${travel.description}") travel.driverInfo = UserInfoDto(driver) CreateTravel(travelRepository).createTravel(travel.toTravel(userRepository)) @@ -41,13 +43,17 @@ class PrivateTravelRestController( @RequestMapping("/delete") fun deleteTravel(@RequestParam @Validated travelId: TravelId): ApiResponse { return response { + if (!authService.canEditTravel(travelId)) + throw InsufficientPermissions("Only admins and travel driver can delete this travel.") DeleteTravel(travelRepository).deleteTravel(travelId) } } @RequestMapping("/edit") - fun editTravel(@RequestParam @Validated travel: TravelDto): ApiResponse { + fun editTravel(@RequestBody @Validated travel: TravelDto): ApiResponse { return response { + if (!authService.canEditTravel(travel.id)) + throw InsufficientPermissions("Only admins and travel driver can edit this travel.") EditTravel(travelRepository).editTravel(travel.toTravel(userRepository)) } } @@ -55,7 +61,7 @@ class PrivateTravelRestController( @RequestMapping("/listusertravels") fun listUserTravels(): ApiResponse> { return response { - val userId = getCurrentUserId() + val userId = authService.currentUser().id val useCase = ListUserTravels(travelRepository) val travels = useCase.listUserTravels(userId).map { t -> TravelDto(t) } println("travels=$travels") @@ -66,7 +72,7 @@ class PrivateTravelRestController( @RequestMapping("/join") fun join(@RequestParam @Validated travelId: TravelId): ApiResponse { return response { - val userId = getCurrentUserId() + val userId = authService.currentUser().id AddTraveler(userRepository, travelRepository).addTraveler(travelId, userId) } } @@ -74,7 +80,7 @@ class PrivateTravelRestController( @RequestMapping("/leave") fun leave(@RequestParam @Validated travelId: TravelId): ApiResponse { return response { - val userId = getCurrentUserId() + val userId = authService.currentUser().id RemoveTraveler(userRepository, travelRepository).removeTraveler(travelId, userId) } } diff --git a/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/PrivateUserRestController.kt b/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/PrivateUserRestController.kt index 5bac122..f797380 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/PrivateUserRestController.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/spring/controller/PrivateUserRestController.kt @@ -3,34 +3,32 @@ package eu.fosil.okupamicoche.spring.controller import eu.fosil.okupamicoche.dto.CreateUserDto import eu.fosil.okupamicoche.dto.UserDto import eu.fosil.okupamicoche.entities.ApiResponse -import eu.fosil.okupamicoche.entities.User +import eu.fosil.okupamicoche.entities.InsufficientPermissions import eu.fosil.okupamicoche.entities.UserId import eu.fosil.okupamicoche.repositories.UserRepository +import eu.fosil.okupamicoche.spring.services.AuthService import eu.fosil.okupamicoche.usecases.user.* import org.springframework.data.repository.findByIdOrNull import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.CrossOrigin import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/user") -class PrivateUserRestController(private val userRepository: UserRepository) : ApiRestController { +class PrivateUserRestController( + private val authService: AuthService, + private val userRepository: UserRepository +) : ApiRestController { @RequestMapping("/user") fun getCurrentUserCreateIfNeeded(): ApiResponse { return response { - var user = userRepository.findByIdOrNull(getCurrentUserId()) - + val userKeycloak = authService.currentUser() + var user = userRepository.findByIdOrNull(userKeycloak.id) + if (user == null) { - val claims = getCurrentUserClaims() - user = User( - claims["sub"].toString(), - "@${claims["preferred_username"].toString()}:fosil.eu", - claims["given_name"].toString(), - claims["email"].toString() - ) + user = userKeycloak.toUser() CreateUser(userRepository).createUser(user) } @@ -41,7 +39,9 @@ class PrivateUserRestController(private val userRepository: UserRepository) : Ap @RequestMapping("/create") fun createUser(@RequestBody @Validated createUserDto: CreateUserDto): ApiResponse { return response { - CreateUser(userRepository).createUser(createUserDto.toUser(userRepository)) + if (!authService.currentUser().admin) + throw InsufficientPermissions("Only admins can create users.") + CreateUser(userRepository).createUser(createUserDto.toUser()) } } diff --git a/src/main/kotlin/eu/fosil/okupamicoche/spring/services/AuthService.kt b/src/main/kotlin/eu/fosil/okupamicoche/spring/services/AuthService.kt new file mode 100644 index 0000000..a575df2 --- /dev/null +++ b/src/main/kotlin/eu/fosil/okupamicoche/spring/services/AuthService.kt @@ -0,0 +1,36 @@ +package eu.fosil.okupamicoche.spring.services + +import eu.fosil.okupamicoche.entities.* +import eu.fosil.okupamicoche.repositories.TravelRepository +import eu.fosil.okupamicoche.repositories.UserRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.stereotype.Service + +@Service +class AuthService( + private val userRepository: UserRepository, + private val travelRepository: TravelRepository +) { + /** + * Devuelve el id del usuario actual. + */ + fun currentUser(): UserKeycloak { + val authentication = SecurityContextHolder.getContext().authentication + if (authentication.principal is Jwt) { + val jwt = authentication.principal as Jwt + return UserKeycloak(jwt.claims) + } + throw UserIdNotFoundException() + } + + fun canEditTravel(travelId: TravelId?): Boolean { + val travel = travelRepository.findByIdOrNull(travelId) ?: return false + return currentUser().admin || currentUser().id == travel.driver.id + } + + fun canEditUser(user: User): Boolean { + return currentUser().admin || currentUser().id == user.id + } +} \ No newline at end of file