Skip to content

Commit

Permalink
add rest of the endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
auguwu committed Apr 27, 2023
1 parent 03d2a2e commit 7e9bb87
Show file tree
Hide file tree
Showing 18 changed files with 739 additions and 26 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 📦 Noelware's Charts Platform (charted-server)
# 🐻‍❄️📦 charted-server

[![Kotlin v1.8.10](https://img.shields.io/badge/kotlin-1.8.10-blue.svg?logo=kotlin)](https://kotlinlang.org)
[![Kotlin v1.8.21](https://img.shields.io/badge/kotlin-1.8.21-blue.svg?logo=kotlin)](https://kotlinlang.org)
[![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0)
[![Linting and Unit Testing](https://github.com/charted-dev/charted/actions/workflows/Linting.yaml/badge.svg?branch=main)](https://github.com/charted-dev/charted/actions/workflows/Linting.yaml)
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
Expand All @@ -19,5 +19,5 @@ is meant to be a very easy solution to distribute Helm charts on the cloud witho

## License

**Noelware's Charts Platform** (charted-server) is released under the **Apache 2.0** License with love by Noelware~!
If you wish to know more, you can read the [LICENSE](./LICENSE) file for more information.
**charted-server** is released under the **Apache 2.0** License with love by Noelware~! If you wish to know more,
you can read the [LICENSE](./LICENSE) file for more information.
54 changes: 48 additions & 6 deletions cli/src/main/kotlin/commands/accounts/CreateAccountCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ import org.noelware.charted.cli.ktor.NoOpApplicationCall
import org.noelware.charted.cli.logger
import org.noelware.charted.common.extensions.regexp.matchesPasswordRegex
import org.noelware.charted.configuration.kotlin.dsl.sessions.SessionType
import org.noelware.charted.models.users.User
import org.noelware.charted.modules.postgresql.controllers.users.CreateUserPayload
import org.noelware.charted.modules.postgresql.controllers.users.UserDatabaseController
import org.noelware.charted.modules.postgresql.entities.UserEntity
import org.noelware.charted.modules.postgresql.extensions.fromEntity
import org.noelware.charted.modules.postgresql.tables.UserTable
import org.noelware.charted.snowflake.Snowflake
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder
Expand Down Expand Up @@ -66,7 +69,7 @@ class CreateAccountCommand(private val terminal: Terminal): AccountsAwareCommand
).optional()

private val verifiedPublisher: Boolean by option(
"--verified-publisher", "-vp",
"--verified-publisher", "--vp",
help = "If the created user should be a verified publisher",
).flag(default = false)

Expand All @@ -90,15 +93,54 @@ class CreateAccountCommand(private val terminal: Terminal): AccountsAwareCommand
)
}

val argon2 = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val snowflake = Snowflake(0, SNOWFLAKE_EPOCH)
val config = resolveConfigHost().load(resolveConfigFile())
val controller = UserDatabaseController(argon2, config, snowflake)
if (config.sessions.type != SessionType.Local) {
terminal.logger.warn("Session manager is not configured to be the local one, please create it yourself.")
exitProcess(1)
terminal.logger.warn(
"""
|Your configured session manager is [${config.sessions.type}], which means you will need
|to create an account that can be resolved to the local account @$username.
|
|~> LDAP: Create a user in the specified group (from the `config.sessions.ldap.group_id` configuration
|key) and it will automatically be queried and resolved on every login invocation.
""".trimMargin("|"),
)

if (password != null) {
terminal.logger.warn("Providing a password is optional and will not be resolved in the final local account creation.")
}

val userByUsername = runBlocking { controller.getOrNull(UserTable::username to username) }
if (userByUsername != null) {
terminal.logger.fatal("Username [$username] is already taken!")
}

val userByEmail = runBlocking { controller.getOrNull(UserTable::email to email) }
if (userByEmail != null) {
terminal.logger.fatal("Email [$email] is already taken!")
}

val id = runBlocking { snowflake.generate() }
val user = transaction {
UserEntity.new(id.value) {
createdAt = LocalDateTime.now().toKotlinLocalDateTime()
updatedAt = LocalDateTime.now().toKotlinLocalDateTime()
username = username
email = email
}
}.let { entity -> User.fromEntity(entity) }

val abilities = listOfNotNull(
if (user.admin) "Administrator" else null,
if (user.verifiedPublisher) "Verified Publisher" else null,
).joinToString(", ")

terminal.logger.info("Created user @$username with${if (abilities.isNotBlank()) " abilities [$abilities]" else " no abilities"}. (${user.id})")
exitProcess(0) // force kill for koin
}

val argon2 = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val snowflake = Snowflake(0, SNOWFLAKE_EPOCH)
val controller = UserDatabaseController(argon2, config, snowflake)
val user = runBlocking {
controller.create(
NoOpApplicationCall(),
Expand Down
2 changes: 1 addition & 1 deletion config/dsl/src/main/kotlin/sessions/SessionsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public open class SessionsConfig(public val type: SessionType) {
* control authentication.
*
* The `PUT /users` REST controller is disabled, and users are automatically pulled from
* the specified [group] it resides in, and will create users based off that.
* the specified group it resides in, and will create users based off that.
*
* @param abandonOnTimeout If a connection timed out, abandon the connection in the pool
* @param maxConnections Maximum amount of connections to pool connections over.
Expand Down
1 change: 1 addition & 0 deletions gradle/build.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ prometheus-simpleclient = { module = "io.prometheus:simpleclient", version.ref =

# jackson libraries
jackson-kotlin-module = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-format-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }

# swagger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class RepositoryDatabaseController(

override suspend fun update(call: ApplicationCall, id: Long, patched: PatchRepositoryPayload) {
val ownerId = call.ownerId ?: error("BUG: Missing owner id when updating a repository!")
val sqlSelector: SqlExpressionBuilder.() -> Op<Boolean> = { RepositoryTable.id eq id }
val sqlSelector: SqlExpressionBuilder.() -> Op<Boolean> = { (RepositoryTable.owner eq ownerId) and (RepositoryTable.id eq id) }

if (patched.name != null) {
val repoWithName = getEntityOrNull { (RepositoryTable.name eq patched.name) and (RepositoryTable.owner eq ownerId) }
Expand Down
2 changes: 1 addition & 1 deletion modules/redis/src/main/kotlin/DefaultRedisClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class DefaultRedisClient(config: RedisConfig): RedisClient {
builder.build()
}

log.debug("Configured Redis URI ~> [$redisURI]")
log.debug("Configured with Redis URL [{}]", redisURI)
_client.value = LettuceRedisClient.create(redisURI)
}

Expand Down
3 changes: 2 additions & 1 deletion server/src/main/kotlin/plugins/sessions/Sessions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package org.noelware.charted.server.plugins.sessions

import com.auth0.jwt.exceptions.JWTDecodeException
import com.auth0.jwt.exceptions.TokenExpiredException
import org.noelware.charted.server.extensions.currentUser
import dev.floofy.utils.koin.inject
import dev.floofy.utils.slf4j.logging
import io.ktor.http.*
Expand Down Expand Up @@ -59,7 +60,7 @@ class Sessions private constructor(private val config: Configuration) {
var assertSessionOnly: Boolean = false

/**
* Allows non authorization requests to be passed by. This means that [currentUser] will be null
* Allows non authorization requests to be passed by. This means that [currentUser][ApplicationCall.currentUser] will be null
* if no authorization header was passed and all checks are bypassed, except for precondition
* checks.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ class GetUserOrganizationsRestController(
"noelwareorg",
null,
"\uD83D\uDC3B\u200D❄️ Noelware, LLC.",
LocalDateTime.parse("2023-04-17T08:28:31.302Z"),
LocalDateTime.parse("2023-04-17T08:28:31.302Z"),
LocalDateTime.parse("2023-04-08T02:37:53.741502369"),
LocalDateTime.parse("2023-04-08T02:37:53.741502369"),
null,
false,
User(
Expand Down
2 changes: 2 additions & 0 deletions server/src/main/kotlin/routing/v1/organizations/koinModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import org.koin.dsl.bind
import org.koin.dsl.module
import org.noelware.charted.server.routing.RestController
import org.noelware.charted.server.routing.v1.organizations.crud.organizationsV1Crud
import org.noelware.charted.server.routing.v1.organizations.repositories.organizationRepositoriesV1Module
import org.noelware.charted.server.util.composeKoinModules

val organizationsV1Module = composeKoinModules(
organizationsV1Crud,
organizationRepositoriesV1Module,
module {
single { MainOrganizationsRestController() } bind RestController::class
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Kotlin.
* Copyright 2022-2023 Noelware, LLC. <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.noelware.charted.server.routing.v1.organizations.repositories

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import io.swagger.v3.oas.models.PathItem
import org.noelware.charted.common.types.helm.RepoType
import org.noelware.charted.common.types.responses.ApiResponse
import org.noelware.charted.models.flags.ApiKeyScope.Repositories
import org.noelware.charted.models.repositories.Repository
import org.noelware.charted.modules.openapi.kotlin.dsl.idOrName
import org.noelware.charted.modules.openapi.kotlin.dsl.json
import org.noelware.charted.modules.openapi.kotlin.dsl.schema
import org.noelware.charted.modules.openapi.toPaths
import org.noelware.charted.modules.postgresql.controllers.getByIdOrNameOrNull
import org.noelware.charted.modules.postgresql.controllers.organizations.OrganizationDatabaseController
import org.noelware.charted.modules.postgresql.controllers.repositories.CreateRepositoryPayload
import org.noelware.charted.modules.postgresql.controllers.repositories.RepositoryDatabaseController
import org.noelware.charted.modules.postgresql.ktor.OwnerIdAttributeKey
import org.noelware.charted.modules.postgresql.tables.OrganizationTable
import org.noelware.charted.modules.search.SearchModule
import org.noelware.charted.server.extensions.addAuthenticationResponses
import org.noelware.charted.server.extensions.putAndRemove
import org.noelware.charted.server.plugins.sessions.Sessions
import org.noelware.charted.server.plugins.sessions.preconditions.canAccessOrganization
import org.noelware.charted.server.plugins.sessions.preconditions.canEditMetadata
import org.noelware.charted.server.routing.APIVersion
import org.noelware.charted.server.routing.RestController

class CreateOrganizationRepositoryRestController(
private val organizations: OrganizationDatabaseController,
private val repositories: RepositoryDatabaseController,
private val search: SearchModule? = null
): RestController("/organizations/{idOrName}/repositories", HttpMethod.Put) {
override val apiVersion: APIVersion = APIVersion.V1
override fun Route.init() {
install(Sessions) {
this += Repositories.Create

condition(::canAccessOrganization)
condition { call -> canEditMetadata(call, organizations) }
}
}

override suspend fun call(call: ApplicationCall) {
val organization = organizations.getByIdOrNameOrNull(call.parameters.getOrFail("idOrName"), OrganizationTable::name)!!
return call.attributes.putAndRemove(OwnerIdAttributeKey, organization.id) {
val repository = repositories.create(call, call.receive())
search?.indexRepository(repository)

call.respond(HttpStatusCode.Created, ApiResponse.ok(repository))
}
}

override fun toPathDsl(): PathItem = toPaths("/organizations/{idOrName}/repositories") {
put {
description = "Creates a repository that is owned by an organization"

idOrName()
requestBody {
json {
schema(
CreateRepositoryPayload(
"helm library to provide common stuff",
false,
"# Hello, world!\n> we do magic stuff here~!",
"common",
RepoType.LIBRARY,
),
)
}
}

addAuthenticationResponses()
response(HttpStatusCode.Created) {
contentType(ContentType.Application.Json) {
schema<ApiResponse.Ok<Repository>>()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Kotlin.
* Copyright 2022-2023 Noelware, LLC. <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.noelware.charted.server.routing.v1.organizations.repositories

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import io.swagger.v3.oas.models.PathItem
import org.noelware.charted.common.types.responses.ApiResponse
import org.noelware.charted.models.repositories.Repository
import org.noelware.charted.modules.openapi.kotlin.dsl.idOrName
import org.noelware.charted.modules.openapi.kotlin.dsl.schema
import org.noelware.charted.modules.openapi.toPaths
import org.noelware.charted.modules.postgresql.controllers.getEntityByIdOrNameOrNull
import org.noelware.charted.modules.postgresql.controllers.organizations.OrganizationDatabaseController
import org.noelware.charted.modules.postgresql.controllers.repositories.RepositoryDatabaseController
import org.noelware.charted.modules.postgresql.tables.OrganizationTable
import org.noelware.charted.modules.postgresql.tables.RepositoryTable
import org.noelware.charted.server.extensions.currentUser
import org.noelware.charted.server.plugins.sessions.Sessions
import org.noelware.charted.server.plugins.sessions.preconditions.canAccessOrganization
import org.noelware.charted.server.routing.APIVersion
import org.noelware.charted.server.routing.RestController
import kotlin.reflect.typeOf

class GetAllOrganizationRepositoriesRestController(
private val organizations: OrganizationDatabaseController,
private val controller: RepositoryDatabaseController
): RestController("/organizations/{idOrName}/repositories") {
override val apiVersion: APIVersion = APIVersion.V1
override fun Route.init() {
install(Sessions) {
allowNonAuthorizedRequests = true

condition { call -> canAccessOrganization(call, false) }
}
}

override suspend fun call(call: ApplicationCall) {
val idOrName = call.parameters.getOrFail("idOrName")
val org = organizations.getEntityByIdOrNameOrNull(idOrName, OrganizationTable::name) ?: return call.respond(
HttpStatusCode.BadRequest,
ApiResponse.err(
"UNKNOWN_USER",
"Unknown user with username or snowflake [$idOrName]",
),
)

val repos = controller.all(RepositoryTable::owner to org.id.value)
if (call.currentUser == null) {
return call.respond(HttpStatusCode.OK, ApiResponse.ok(repos.filterNot { it.private }))
}

if (org.owner.id.value != call.currentUser?.id || !org.members.any { it.account.id.value == call.currentUser!!.id }) {
return call.respond(HttpStatusCode.OK, ApiResponse.ok(repos.filterNot { it.private }))
}

call.respond(HttpStatusCode.OK, ApiResponse.ok(repos))
}

override fun toPathDsl(): PathItem = toPaths("/organizations/{idOrName}/repositories") {
get {
description = "Returns all of an organization's repositories"

idOrName()
response(HttpStatusCode.OK) {
contentType(ContentType.Application.Json) {
schema(typeOf<ApiResponse.Ok<List<Repository>>>(), ApiResponse.ok(listOf<Repository>()))
}
}
}
}
}
Loading

0 comments on commit 7e9bb87

Please sign in to comment.