Merge pull request 'FEAT-addition-security' (#4) from FEAT-addition-security into main
Reviewed-on: https://gitea.com/NickKalar/LMS-APIs/pulls/4
This commit is contained in:
@@ -16,7 +16,7 @@ import io.ktor.server.response.respond
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
embeddedServer(Netty, port = 8080) {
|
embeddedServer(Netty, host = "127.0.0.1", port = 8080) {
|
||||||
install(CORS) {
|
install(CORS) {
|
||||||
anyHost()
|
anyHost()
|
||||||
allowHeader(HttpHeaders.ContentType)
|
allowHeader(HttpHeaders.ContentType)
|
||||||
@@ -39,7 +39,7 @@ fun Application.module() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install(Authentication) {
|
install(Authentication) {
|
||||||
jwt("auth-jwt") {
|
jwt("general") {
|
||||||
realm = myRealm
|
realm = myRealm
|
||||||
verifier(
|
verifier(
|
||||||
JWT
|
JWT
|
||||||
@@ -55,7 +55,45 @@ fun Application.module() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
challenge { defaultScheme, realm ->
|
challenge { defaultScheme, realm ->
|
||||||
call.respond(HttpStatusCode.Unauthorized, "${defaultScheme}, $realm Token is not valid or has expired")
|
call.respond(HttpStatusCode.Unauthorized, "$defaultScheme, $realm Token is not valid or has expired")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jwt("patron"){
|
||||||
|
realm = myRealm
|
||||||
|
verifier(
|
||||||
|
JWT
|
||||||
|
.require(Algorithm.HMAC256(secret))
|
||||||
|
.withAudience(audience)
|
||||||
|
.withIssuer(issuer)
|
||||||
|
.build())
|
||||||
|
validate { credential ->
|
||||||
|
if (credential.payload.getClaim("role").asString() != "patron") {
|
||||||
|
JWTPrincipal(credential.payload)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
challenge { _, _ ->
|
||||||
|
call.respond(HttpStatusCode.Unauthorized, "Insufficient permissions to access this resource.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jwt("staff"){
|
||||||
|
realm = myRealm
|
||||||
|
verifier(
|
||||||
|
JWT
|
||||||
|
.require(Algorithm.HMAC256(secret))
|
||||||
|
.withAudience(audience)
|
||||||
|
.withIssuer(issuer)
|
||||||
|
.build())
|
||||||
|
validate { credential ->
|
||||||
|
if (credential.payload.getClaim("role").asString() != "staff") {
|
||||||
|
JWTPrincipal(credential.payload)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
challenge { _, _ ->
|
||||||
|
call.respond(HttpStatusCode.Unauthorized, "Insufficient permissions to access this resource.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ fun Application.configureCollectionItemRoutes(dbConnection: Connection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate("auth-jwt") {
|
authenticate("staff") {
|
||||||
post("/items") {
|
post("/items") {
|
||||||
try {
|
try {
|
||||||
val item = call.receive<NewCollectionItem>()
|
val item = call.receive<NewCollectionItem>()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import codes.kalar.model.NewLibrary
|
|||||||
import codes.kalar.service.LibraryService
|
import codes.kalar.service.LibraryService
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
@@ -41,7 +42,10 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) {
|
|||||||
} catch (cause: DbElementNotFoundException) {
|
} catch (cause: DbElementNotFoundException) {
|
||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to find Library.")
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to find Library.")
|
||||||
} catch (cause: NumberFormatException) {
|
} catch (cause: NumberFormatException) {
|
||||||
call.respond(HttpStatusCode.BadRequest, "Unable to parse number format. \"${call.pathParameters["id"]}\" is not a number.")
|
call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
"Unable to parse number format. \"${call.pathParameters["id"]}\" is not a number."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +53,7 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) {
|
|||||||
// TODO Add search for collection_it where itemID && libraryID
|
// TODO Add search for collection_it where itemID && libraryID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticate("staff") {
|
||||||
post("/libraries") {
|
post("/libraries") {
|
||||||
val library = call.receive<NewLibrary>()
|
val library = call.receive<NewLibrary>()
|
||||||
try {
|
try {
|
||||||
@@ -71,7 +76,6 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) {
|
|||||||
log.error(cause.message)
|
log.error(cause.message)
|
||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete("/libraries") {
|
delete("/libraries") {
|
||||||
@@ -89,5 +93,6 @@ fun Application.configureLibraryRoutes(dbConnection: Connection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package codes.kalar.routes
|
|||||||
import codes.kalar.exception.DbElementNotFoundException
|
import codes.kalar.exception.DbElementNotFoundException
|
||||||
import codes.kalar.model.User
|
import codes.kalar.model.User
|
||||||
import codes.kalar.service.PatronService
|
import codes.kalar.service.PatronService
|
||||||
|
import codes.kalar.service.StaffService
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
@@ -18,8 +19,10 @@ fun Application.configureLoginRoutes(dbConnection: Connection) {
|
|||||||
val secret = environment.config.property("jwt.secret").getString()
|
val secret = environment.config.property("jwt.secret").getString()
|
||||||
val issuer = environment.config.property("jwt.issuer").getString()
|
val issuer = environment.config.property("jwt.issuer").getString()
|
||||||
val audience = environment.config.property("jwt.audience").getString()
|
val audience = environment.config.property("jwt.audience").getString()
|
||||||
|
val auth0Map = mapOf("secret" to secret, "issuer" to issuer, "audience" to audience)
|
||||||
|
|
||||||
val patronService = PatronService(dbConnection)
|
val patronService = PatronService(dbConnection)
|
||||||
|
val staffService = StaffService(dbConnection)
|
||||||
routing {
|
routing {
|
||||||
post("/login") {
|
post("/login") {
|
||||||
try {
|
try {
|
||||||
@@ -28,20 +31,35 @@ fun Application.configureLoginRoutes(dbConnection: Connection) {
|
|||||||
val password = user.password
|
val password = user.password
|
||||||
|
|
||||||
if (patronService.loginPatronByLoginUsername(name, password)) {
|
if (patronService.loginPatronByLoginUsername(name, password)) {
|
||||||
val token = JWT.create()
|
val token = createToken(user.name, "patron", auth0Map)
|
||||||
.withAudience(audience)
|
call.respond(HttpStatusCode.OK, mapOf("token" to token))
|
||||||
.withIssuer(issuer)
|
}
|
||||||
.withClaim("name", name)
|
else if (staffService.loginStaffByLoginUsername(name, password)) {
|
||||||
.withExpiresAt(Date(System.currentTimeMillis() + 160000))
|
val token = createToken(user.name, "staff", auth0Map)
|
||||||
.sign(Algorithm.HMAC256(secret))
|
call.respond(HttpStatusCode.OK, mapOf("token" to token))
|
||||||
call.respond(hashMapOf("token" to token))
|
}
|
||||||
} else {
|
else {
|
||||||
call.respond(HttpStatusCode.Unauthorized, "Invalid login")
|
log.error("Unauthorized use: $name")
|
||||||
|
call.respond(HttpStatusCode.Unauthorized, mapOf("message" to "Invalid login", "User" to user.name))
|
||||||
}
|
}
|
||||||
} catch (cause: DbElementNotFoundException) {
|
} catch (cause: DbElementNotFoundException) {
|
||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Something went wrong")
|
log.error(cause.message)
|
||||||
|
call.respond(HttpStatusCode.BadRequest, mapOf("message" to cause.message))
|
||||||
|
} catch (cause: Exception) {
|
||||||
|
log.error(cause.message)
|
||||||
|
call.respond(HttpStatusCode.BadRequest, mapOf("message" to "An unexpected error occurred: ${cause.message}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createToken(username: String, role: String, auth: Map<String, String>): String {
|
||||||
|
return JWT.create()
|
||||||
|
.withAudience(auth["audience"])
|
||||||
|
.withIssuer(auth["issuer"])
|
||||||
|
.withClaim("name", username)
|
||||||
|
.withClaim("role", role)
|
||||||
|
.withExpiresAt(Date(System.currentTimeMillis() + 160000))
|
||||||
|
.sign(Algorithm.HMAC256(auth["secret"]))
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import codes.kalar.model.Patron
|
|||||||
import codes.kalar.service.PatronService
|
import codes.kalar.service.PatronService
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
@@ -42,6 +43,7 @@ fun Application.configurePatronRoutes(dbConnection: Connection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticate("staff") {
|
||||||
post("/patron") {
|
post("/patron") {
|
||||||
try {
|
try {
|
||||||
val patron = call.receive<NewPatron>()
|
val patron = call.receive<NewPatron>()
|
||||||
@@ -53,19 +55,27 @@ fun Application.configurePatronRoutes(dbConnection: Connection) {
|
|||||||
call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid CollectionItem object.")
|
call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid CollectionItem object.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate("general") {
|
||||||
patch("/patron") {
|
patch("/patron") {
|
||||||
try {
|
try {
|
||||||
val patron = call.receive<Patron>()
|
val patron = call.receive<Patron>()
|
||||||
val patchedPatron = patronService.update(patron)
|
val isPatched = patronService.update(patron)
|
||||||
call.respondText("${patron.name} is patched")
|
if (isPatched) {
|
||||||
|
call.respond(HttpStatusCode.OK, "${patron.name} is patched")
|
||||||
|
} else {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "${patron.name} is not patched")
|
||||||
|
}
|
||||||
} catch (cause: DbElementInsertionException) {
|
} catch (cause: DbElementInsertionException) {
|
||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Patron.")
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Unable to update Patron.")
|
||||||
} catch (cause: ContentTransformationException) {
|
} catch (cause: ContentTransformationException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid Patron object.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticate("staff") {
|
||||||
delete("/patron/{id}") {
|
delete("/patron/{id}") {
|
||||||
try {
|
try {
|
||||||
val id = call.pathParameters["id"]!!.toLong()
|
val id = call.pathParameters["id"]!!.toLong()
|
||||||
@@ -79,3 +89,4 @@ fun Application.configurePatronRoutes(dbConnection: Connection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package codes.kalar.routes
|
package codes.kalar.routes
|
||||||
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
@@ -16,6 +17,7 @@ fun Application.configureStaffRoutes(dbConnection: Connection) {
|
|||||||
call.respondText(call.parameters["id"]!!)
|
call.respondText(call.parameters["id"]!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticate("staff") {
|
||||||
post("/staff") {
|
post("/staff") {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -29,3 +31,4 @@ fun Application.configureStaffRoutes(dbConnection: Connection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,17 @@ class StaffService(private val connection: Connection) {
|
|||||||
private const val SELECT_STAFF_BY_ID = ""
|
private const val SELECT_STAFF_BY_ID = ""
|
||||||
private const val INSERT_STAFF = ""
|
private const val INSERT_STAFF = ""
|
||||||
private const val UPDATE_STAFF_BY_ID = ""
|
private const val UPDATE_STAFF_BY_ID = ""
|
||||||
// In the event are "deleted" erroneously, having a flag set instead of actually removing the entry allows
|
// In the event staff are "deleted" erroneously, having a flag set instead of actually removing the entry allows
|
||||||
// for quick reversal.
|
// for quick reversal.
|
||||||
private const val ARCHIVE_STAFF_BY_ID = ""
|
private const val ARCHIVE_STAFF_BY_ID = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun create() {}
|
suspend fun create() {}
|
||||||
|
|
||||||
|
fun loginStaffByLoginUsername(username: String, password: String): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun read() {}
|
suspend fun read() {}
|
||||||
|
|
||||||
suspend fun update() {}
|
suspend fun update() {}
|
||||||
|
|||||||
Reference in New Issue
Block a user