Skip to content

Commit

Permalink
feat: super basic RBAC starting point
Browse files Browse the repository at this point in the history
  • Loading branch information
cif committed Oct 6, 2024
1 parent 53cd3e6 commit 414e4bc
Show file tree
Hide file tree
Showing 18 changed files with 466 additions and 69 deletions.
4 changes: 2 additions & 2 deletions src/main/kotlin/com/stabledata/Application.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.stabledata

import com.stabledata.endpoint.configureChoresRouting
import com.stabledata.endpoint.configureSchemaRouting
import com.stabledata.endpoint.configureApplicationRouting
import com.stabledata.plugins.configureAuth
import com.stabledata.plugins.configureDocsRouting
import io.ktor.http.*
Expand Down Expand Up @@ -35,7 +35,7 @@ fun Application.module() {


// configure routes.
configureSchemaRouting()
configureApplicationRouting()
configureChoresRouting()
configureDocsRouting()
}
Expand Down
12 changes: 10 additions & 2 deletions src/main/kotlin/com/stabledata/JwtUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.exceptions.JWTVerificationException
import com.auth0.jwt.interfaces.DecodedJWT
import com.stabledata.plugins.Roles
import com.stabledata.plugins.UserCredentials
import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.*
Expand All @@ -20,12 +21,19 @@ fun getStableJwtSecret(): String {
.withClaim("email", userCredentials.email)
.withClaim("team", userCredentials.team)
.withClaim("id", userCredentials.id)
.withClaim("role", userCredentials.role)
.withExpiresAt(Date(System.currentTimeMillis() + oneWeekInMillis))
.sign(Algorithm.HMAC256(getStableJwtSecret()))
}

fun generateTokenForTesting(): String {
val token = generateJwtTokenWithCredentials(UserCredentials("[email protected]", "test", "fake.id"))
fun generateTokenForTesting(withRole: String? = Roles.Default): String {
val fakeCreds = UserCredentials(
"[email protected]",
"test",
"fake.id",
withRole
)
val token = generateJwtTokenWithCredentials(fakeCreds)
return token
}

Expand Down
76 changes: 76 additions & 0 deletions src/main/kotlin/com/stabledata/dao/AccessDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.stabledata.dao

import com.stabledata.endpoint.io.AccessRequest
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*

data class AccessRecord(
val id: String,
val teamId: String,
val type: String,
val role: String,
val operation: String?,
val path: String?
)

object AccessTable: Table("stable.access") {
val accessId = uuid("id")
val teamId = varchar("team_id", 255)
val kind = varchar("type", 5).check {
it inList listOf("grant", "deny")
}
val role = varchar("role", 255)
val operation = varchar("operation", 255).nullable()
val path = varchar("path", 255).nullable()

init {
check("either_operation_or_path") {
(operation.isNotNull() and path.isNull()) or (operation.isNull() and path.isNotNull())
}
}

fun insertFromRequest(type: String, team: String, record: AccessRequest) {
AccessTable.insert { row ->
row[accessId] = UUID.fromString(record.id)
row[kind] = type
row[teamId] = team
row[role] = record.role
row[path] = record.path
row[operation] = record.operation



}
}

fun findMatchingRules(operationOrPath: String, team: String, checkRole: String): Pair<List<AccessRecord>, List<AccessRecord>> {
val rules = transaction {
// Later: make paths matchable in parts
AccessTable
.select {
(teamId eq team) and
(role eq checkRole) and
(
(operation eq operationOrPath) or
(path eq operationOrPath)
)
}
.map {
AccessRecord(
id = it[accessId].toString(),
teamId = it[teamId],
type = it[kind],
role = it[role],
operation = it[operation],
path = it[path]
)
}
}

val allowingRules = rules.filter { it.type == "grant" }
val blockingRules = rules.filter { it.type == "deny" }

return Pair(allowingRules, blockingRules)
}
}
6 changes: 3 additions & 3 deletions src/main/kotlin/com/stabledata/dao/LogsDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class LogEntryBuilder {
return LogEntry(
id = requireNotNull(id) { "id $providedExplainer" },
teamId = requireNotNull(teamId) { "team_id $providedExplainer" },
path = requireNotNull(path) { "path $providedExplainer" },
path = path.orEmpty(),
actorId = requireNotNull(actorId) { "actorId $providedExplainer" },
eventType = requireNotNull(eventType) { "eventType $providedExplainer" },
createdAt = requireNotNull(createdAt) { "createdAt timestamp $providedExplainer" },
Expand All @@ -62,7 +62,7 @@ object LogsTable : Table("stable.logs") {
val eventId = uuid("id")
val teamId = text("team_id")
val actorId = text("actor_id")
val path = text("path")
val path = text("path").nullable()
val eventType = text("event_type")
val createdAt = long("created_at")
val confirmedAt = long("confirmed_at")
Expand All @@ -83,7 +83,7 @@ object LogsTable : Table("stable.logs") {
id = it[eventId].toString(),
teamId = it[teamId],
actorId = it[actorId],
path = it[path],
path = it[path].orEmpty(),
eventType = it[eventType],
confirmedAt = it[confirmedAt],
createdAt = it[createdAt]
Expand Down
58 changes: 58 additions & 0 deletions src/main/kotlin/com/stabledata/endpoint/AccessCreateRoute.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.stabledata.endpoint

import com.stabledata.Ably
import com.stabledata.dao.AccessTable
import com.stabledata.dao.LogsTable
import com.stabledata.endpoint.io.AccessRequest
import com.stabledata.plugins.JWT_NAME
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.jetbrains.exposed.sql.transactions.transaction

fun Application.configureAccessCreateRoute() {

val logger = KotlinLogging.logger {}

routing {
authenticate(JWT_NAME) {
post("access/grant") {
val (access, user, envelope, logEntry) = contextualize(
"access/create"
) { postData ->
AccessRequest.fromJSON(postData)
} ?: return@post

// slightly borrowed, but not crazy use case issues in logs anyway
logEntry.path("grant")

logger.debug { "Create collection requested by ${user.id} with event id ${envelope.eventId}" }

try {
val finalLogEntry = logEntry.build()

transaction {
AccessTable.insertFromRequest("grant", user.team, access)
LogsTable.insertLogEntry(finalLogEntry)
Ably.publish(user.team, "collection/create", finalLogEntry)
}

logger.debug {"Collection access control record for path: ${access.path} or operation: ${access.operation}" }

return@post call.respond(
HttpStatusCode.Created,
finalLogEntry
)

} catch (e: ExposedSQLException) {
logger.error { "Create access record failed: ${e.localizedMessage}" }
return@post call.respond(HttpStatusCode.InternalServerError, e.localizedMessage)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.stabledata.endpoint

import io.ktor.server.application.*
fun Application.configureSchemaRouting() {
fun Application.configureApplicationRouting() {
// access controls
configureAccessCreateRoute()

// schema
configureCreateCollectionRoute()
configureUpdateCollectionRoute()
configureDeleteCollectionRoute()
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/com/stabledata/endpoint/ChoresRouting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ fun Application.configureChoresRouting() {

return@get call.respond(HttpStatusCode.OK, "Migration success")
} catch (fe: FlywayException) {
logger.error { "Flyway migration exception: $fe" }
return@get call.respond(HttpStatusCode.InternalServerError, "Migration failed")
}
}
Expand Down
29 changes: 11 additions & 18 deletions src/main/kotlin/com/stabledata/endpoint/RequestContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.stabledata.endpoint

import com.stabledata.dao.LogEntryBuilder
import com.stabledata.plugins.*

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
Expand All @@ -25,18 +24,12 @@ suspend fun <T>PipelineContext<Unit, ApplicationCall>.contextualize(
}
} ?: return null

val userCredentials = permissions(operation) { hasPermission ->
if (!hasPermission) {
call.respond(HttpStatusCode.Forbidden,
"You do not have permissions to $operation")

val userCredentials = permissions(operation) { error ->
if (error !== null) {
call.respond(error.status, error.message)
}
} ?: run {
call.respond(
HttpStatusCode.Unauthorized,
"Unable to validate request credentials"
)
return null
}
} ?: return null

val envelope = idempotent { existingRecord, envelope ->
existingRecord?.let {
Expand All @@ -51,12 +44,12 @@ suspend fun <T>PipelineContext<Unit, ApplicationCall>.contextualize(

val body = bodyParser(postData)

val logEntry = LogEntryBuilder().eventType(operation)

logEntry.actorId(userCredentials.id)
logEntry.teamId(userCredentials.team)
logEntry.id(envelope.eventId)
logEntry.createdAt(envelope.createdAt)
val logEntry = LogEntryBuilder()
.eventType(operation)
.actorId(userCredentials.id)
.teamId(userCredentials.team)
.id(envelope.eventId)
.createdAt(envelope.createdAt)

return RequestContext(
body,
Expand Down
24 changes: 24 additions & 0 deletions src/main/kotlin/com/stabledata/endpoint/io/AccessRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stabledata.endpoint.io

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class AccessRequest (
val id: String,
val role: String,
val operation: String?,
val path: String?
) {
companion object {
fun fromJSON (json: String): AccessRequest {
val jsonParser = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
explicitNulls = false
}
return jsonParser.decodeFromString<AccessRequest>(json)
}
}
}
Loading

0 comments on commit 414e4bc

Please sign in to comment.