Skip to content

Commit 21838d9

Browse files
authored
Use environment variables to configure the Hub backend settings (#34)
* Use environment variables to configure the backend settings * lint * add review changes
1 parent 648da45 commit 21838d9

5 files changed

Lines changed: 106 additions & 85 deletions

File tree

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ ktor-server-sessions = { module = "io.ktor:ktor-server-sessions" }
2424
ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages" }
2525
ktor-serialization-kotlinx = { module = "io.ktor:ktor-serialization-kotlinx" }
2626
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } # TODO: Remove
27+
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
2728

2829
kubernetes-client = "io.fabric8:kubernetes-client:6.11.0"
2930
logging-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }

hub/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies {
1515
implementation(libs.logging.api)
1616
implementation(libs.logging.core)
1717
implementation(libs.logging.slf4jimpl)
18+
implementation(libs.ktor.server.cors)
1819
implementation(libs.exposed.core)
1920
implementation(libs.exposed.crypt)
2021
implementation(libs.exposed.dao)
@@ -26,6 +27,7 @@ dependencies {
2627
implementation(libs.ktor.client.cio)
2728
implementation(libs.ktor.server.call.logging)
2829
implementation(libs.ktor.client.logging)
30+
implementation(libs.kotlin.reflect)
2931
}
3032

3133
application {

hub/src/main/kotlin/org/sourcegrade/lab/hub/Module.kt

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import com.expediagroup.graphql.server.ktor.graphQLGetRoute
55
import com.expediagroup.graphql.server.ktor.graphQLPostRoute
66
import com.expediagroup.graphql.server.ktor.graphQLSDLRoute
77
import com.expediagroup.graphql.server.ktor.graphiQLRoute
8+
import io.ktor.http.HttpHeaders
9+
import io.ktor.http.HttpMethod
810
import io.ktor.http.Url
911
import io.ktor.server.application.Application
1012
import io.ktor.server.application.install
1113
import io.ktor.server.config.tryGetString
1214
import io.ktor.server.plugins.callloging.CallLogging
15+
import io.ktor.server.plugins.cors.routing.CORS
1316
import io.ktor.server.request.path
1417
import io.ktor.server.routing.Routing
1518
import org.jetbrains.exposed.sql.Database
@@ -20,13 +23,13 @@ import org.sourcegrade.lab.hub.queries.CourseQueries
2023
import org.sourcegrade.lab.hub.queries.HelloWorldQuery
2124
import org.sourcegrade.lab.hub.queries.UserMutations
2225
import org.sourcegrade.lab.hub.queries.UserQueries
23-
import kotlin.collections.listOf
2426

2527
fun Application.module() {
2628
val environment = environment
2729
val url =
2830
Url(
29-
environment.config.tryGetString("ktor.deployment.url") ?: throw IllegalStateException("No deployment url set"),
31+
environment.config.tryGetString("ktor.deployment.url")
32+
?: throw IllegalStateException("No deployment url set"),
3033
)
3134

3235
val databaseConfig =
@@ -42,6 +45,15 @@ fun Application.module() {
4245
databaseConfig = databaseConfig,
4346
)
4447

48+
install(CORS) {
49+
allowMethod(HttpMethod.Options)
50+
allowMethod(HttpMethod.Post)
51+
allowMethod(HttpMethod.Get)
52+
allowHeader(HttpHeaders.AccessControlAllowOrigin)
53+
allowHeader(HttpHeaders.ContentType)
54+
anyHost()
55+
}
56+
4557
install(GraphQL) {
4658
schema {
4759
packages = listOf("org.sourcegrade.lab.hub")
@@ -63,20 +75,9 @@ fun Application.module() {
6375
graphQLGetRoute()
6476
graphQLPostRoute()
6577
graphQLSDLRoute()
66-
// graphQLSubscriptionsRoute()
6778
graphiQLRoute()
6879
}
6980

70-
// install(ContentNegotiation) {
71-
// json(
72-
// Json {
73-
// prettyPrint = true
74-
// isLenient = true
75-
// ignoreUnknownKeys = true
76-
// },
77-
// )
78-
// }
79-
8081
authenticationModule()
8182
configureRouting()
8283
install(CallLogging) {

hub/src/main/kotlin/org/sourcegrade/lab/hub/http/AuthenticationModule.kt

Lines changed: 83 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package org.sourcegrade.lab.hub.http
33
import io.ktor.client.HttpClient
44
import io.ktor.client.call.body
55
import io.ktor.client.engine.cio.CIO
6-
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
76
import io.ktor.client.request.get
87
import io.ktor.client.request.header
98
import io.ktor.http.HttpMethod
@@ -26,6 +25,7 @@ import io.ktor.server.response.respond
2625
import io.ktor.server.response.respondRedirect
2726
import io.ktor.server.response.respondText
2827
import io.ktor.server.routing.get
28+
import io.ktor.server.routing.route
2929
import io.ktor.server.routing.routing
3030
import io.ktor.server.sessions.Sessions
3131
import io.ktor.server.sessions.clear
@@ -43,14 +43,17 @@ import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransacti
4343
import org.sourcegrade.lab.hub.models.User
4444
import org.sourcegrade.lab.hub.models.Users
4545
import java.io.File
46+
import kotlin.collections.set
4647
import kotlin.time.Duration.Companion.hours
48+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation
49+
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
4750

4851
fun Application.authenticationModule() {
4952
val ktorEnv = environment
5053

5154
val httpClient =
5255
HttpClient(CIO) {
53-
install(ContentNegotiation) {
56+
install(ClientContentNegotiation) {
5457
json(
5558
Json {
5659
encodeDefaults = false
@@ -85,81 +88,96 @@ fun Application.authenticationModule() {
8588

8689
client = httpClient
8790

88-
val oauthSettings = OAuthServerSettings.OAuth2ServerSettings(
89-
name = "Authentik",
90-
authorizeUrl = ktorEnv.config.property("ktor.oauth.authorizeUrl").getString(),
91-
accessTokenUrl = ktorEnv.config.property("ktor.oauth.accessTokenUrl").getString(),
92-
requestMethod = HttpMethod.Post,
93-
clientId = ktorEnv.config.property("ktor.oauth.clientId").getString(),
94-
clientSecret = ktorEnv.config.property("ktor.oauth.clientSecret").getString(),
95-
defaultScopes = ktorEnv.config.tryGetString("ktor.oauth.scopes")
96-
?.split(" ")
97-
?: listOf("openid", "profile", "email"),
98-
onStateCreated = { call, state ->
99-
// saves new state with redirect url value
100-
call.request.queryParameters["redirectUrl"]?.let {
101-
redirects[state] = it
102-
}
103-
},
104-
)
105-
91+
val oauthSettings =
92+
OAuthServerSettings.OAuth2ServerSettings(
93+
name = "Authentik",
94+
authorizeUrl = ktorEnv.config.property("ktor.oauth.authorizeUrl")
95+
.getString(),
96+
accessTokenUrl = ktorEnv.config.property("ktor.oauth.accessTokenUrl")
97+
.getString(),
98+
requestMethod = HttpMethod.Post,
99+
clientId = ktorEnv.config.property("ktor.oauth.clientId")
100+
.getString(),
101+
clientSecret = ktorEnv.config.property("ktor.oauth.clientSecret")
102+
.getString(),
103+
defaultScopes = ktorEnv.config.tryGetString("ktor.oauth.scopes")
104+
?.split(" ")
105+
?: listOf("openid", "profile", "email"),
106+
onStateCreated = { call, state ->
107+
// saves new state with redirect url value
108+
call.request.queryParameters["redirectUrl"]?.let {
109+
redirects[state] = it
110+
}
111+
},
112+
)
106113
providerLookup = { oauthSettings }
107114
}
108115
}
109116
routing {
110-
authenticate("Authentik") {
111-
get("/api/session/login") {
112-
// Redirects to 'authorizeUrl' automatically
117+
route("/api/session") {
118+
install(ServerContentNegotiation) {
119+
json(
120+
Json {
121+
prettyPrint = true
122+
isLenient = true
123+
ignoreUnknownKeys = true
124+
},
125+
)
113126
}
127+
authenticate("Authentik") {
128+
get("login") {
129+
// Redirects to 'authorizeUrl' automatically
130+
}
114131

115-
get(callback) {
116-
val principal: OAuthAccessTokenResponse.OAuth2 = checkNotNull(call.principal()) { "No principal" }
117-
118-
val userInfo =
119-
httpClient.get(
120-
this@authenticationModule.environment.config.tryGetString("ktor.oauth.userInfoUrl")
121-
?: throw IllegalStateException("Missing OAuth user info Url"),
122-
) {
123-
header("Authorization", "Bearer ${principal.accessToken}")
124-
}.body<OAuthUserInfo>()
125-
126-
// find user in db
127-
128-
val user =
129-
newSuspendedTransaction {
130-
User.find { Users.email eq userInfo.email }.firstOrNull()
131-
} ?: newSuspendedTransaction {
132-
User.new {
133-
username = userInfo.preferredUsername
134-
email = userInfo.email
132+
get("callback") {
133+
val principal: OAuthAccessTokenResponse.OAuth2 = checkNotNull(call.principal()) { "No principal" }
134+
135+
val userInfo =
136+
httpClient.get(
137+
this@authenticationModule.environment.config.tryGetString("ktor.oauth.userInfoUrl")
138+
?: throw IllegalStateException("Missing OAuth user info Url"),
139+
) {
140+
header("Authorization", "Bearer ${principal.accessToken}")
141+
}.body<OAuthUserInfo>()
142+
143+
// find user in db
144+
145+
val user =
146+
newSuspendedTransaction {
147+
User.find { Users.email eq userInfo.email }.firstOrNull()
148+
} ?: newSuspendedTransaction {
149+
User.new {
150+
username = userInfo.preferredUsername
151+
email = userInfo.email
152+
}
135153
}
136-
}
137154

138-
val session =
139-
UserSession(
140-
user.id.value,
141-
checkNotNull(principal.state) { "No state" },
142-
principal.accessToken,
143-
userInfo.email,
144-
)
145-
146-
call.sessions.set(session)
147-
principal.state?.let { state ->
148-
redirects[state]?.let { redirect ->
149-
call.respondRedirect(redirect)
150-
return@get
155+
val session =
156+
UserSession(
157+
user.id.value,
158+
checkNotNull(principal.state) { "No state" },
159+
principal.accessToken,
160+
userInfo.email,
161+
)
162+
163+
call.sessions.set(session)
164+
principal.state?.let { state ->
165+
redirects[state]?.let { redirect ->
166+
call.respondRedirect(redirect)
167+
return@get
168+
}
151169
}
170+
call.respondRedirect("/")
152171
}
153-
call.respondRedirect("/")
154-
}
155172

156-
get("/api/session/logout") {
157-
call.sessions.clear<UserSession>()
158-
call.respondRedirect("/")
173+
get("logout") {
174+
call.sessions.clear<UserSession>()
175+
call.respondRedirect("/")
176+
}
177+
}
178+
get("current-user") {
179+
withUser { call.respond(it.toDTO()) }
159180
}
160-
}
161-
get("/api/session/current-user") {
162-
withUser { call.respond(it.toDTO()) }
163181
}
164182
}
165183
}

hub/src/main/kotlin/org/sourcegrade/lab/hub/models/DummyDataPopulator.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ suspend fun main(args: Array<String>) {
2020
addLogger(StdOutSqlLogger)
2121

2222
// delete and re-create tables
23-
val tables = SchemaUtils.listTables()
24-
SchemaUtils.drop(CourseMembers, Courses, Users)
23+
val wantedTables = listOf(Users, Courses, CourseMembers)
2524

26-
SchemaUtils.create(Users)
25+
val tables = SchemaUtils.listTables()
26+
if (tables.isNotEmpty()) {
27+
SchemaUtils.drop(*wantedTables.toTypedArray())
28+
}
29+
SchemaUtils.create(*wantedTables.toTypedArray())
2730

2831
val dummyUsers =
2932
listOf(
@@ -44,8 +47,6 @@ suspend fun main(args: Array<String>) {
4447
},
4548
)
4649

47-
SchemaUtils.create(Courses)
48-
4950
val dummyCourses =
5051
listOf(
5152
Course.new {
@@ -70,8 +71,6 @@ suspend fun main(args: Array<String>) {
7071
},
7172
)
7273

73-
SchemaUtils.create(CourseMembers)
74-
7574
val dummyCourseMembers =
7675
listOf(
7776
CourseMember.new {

0 commit comments

Comments
 (0)