Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components/secrets/backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {

implementation(projects.components.authorization.backend)
implementation(projects.model)
implementation(projects.services.hierarchyService)
implementation(projects.services.secretService)
implementation(projects.shared.apiMappings)
implementation(projects.shared.apiModel)
Expand All @@ -46,4 +47,5 @@ dependencies {
testImplementation(ktorLibs.server.testHost)
testImplementation(libs.kotestAssertionsCore)
testImplementation(libs.kotestAssertionsKtor)
testImplementation(libs.mockk)
}
5 changes: 4 additions & 1 deletion components/secrets/backend/src/main/kotlin/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ import org.eclipse.apoapsis.ortserver.components.secrets.routes.product.getSecre
import org.eclipse.apoapsis.ortserver.components.secrets.routes.product.patchSecretByProductIdAndName
import org.eclipse.apoapsis.ortserver.components.secrets.routes.product.postSecretForProduct
import org.eclipse.apoapsis.ortserver.components.secrets.routes.repository.deleteSecretByRepositoryIdAndName
import org.eclipse.apoapsis.ortserver.components.secrets.routes.repository.getAvailableSecretsByRepositoryId
import org.eclipse.apoapsis.ortserver.components.secrets.routes.repository.getSecretByRepositoryIdAndName
import org.eclipse.apoapsis.ortserver.components.secrets.routes.repository.getSecretsByRepositoryId
import org.eclipse.apoapsis.ortserver.components.secrets.routes.repository.patchSecretByRepositoryIdAndName
import org.eclipse.apoapsis.ortserver.components.secrets.routes.repository.postSecretForRepository
import org.eclipse.apoapsis.ortserver.services.RepositoryService
import org.eclipse.apoapsis.ortserver.services.SecretService

fun Route.secretsRoutes(secretService: SecretService) {
fun Route.secretsRoutes(repositoryService: RepositoryService, secretService: SecretService) {
// Organization secrets
deleteSecretByOrganizationIdAndName(secretService)
getSecretByOrganizationIdAndName(secretService)
Expand All @@ -55,6 +57,7 @@ fun Route.secretsRoutes(secretService: SecretService) {

// Repository secrets
deleteSecretByRepositoryIdAndName(secretService)
getAvailableSecretsByRepositoryId(repositoryService, secretService)
getSecretByRepositoryIdAndName(secretService)
getSecretsByRepositoryId(secretService)
patchSecretByRepositoryIdAndName(secretService)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* 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
*
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.components.secrets.routes.repository

import io.github.smiley4.ktoropenapi.get

import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respond
import io.ktor.server.routing.Route

import org.eclipse.apoapsis.ortserver.components.authorization.permissions.RepositoryPermission
import org.eclipse.apoapsis.ortserver.components.authorization.requirePermission
import org.eclipse.apoapsis.ortserver.components.secrets.Secret
import org.eclipse.apoapsis.ortserver.components.secrets.mapToApi
import org.eclipse.apoapsis.ortserver.services.RepositoryService
import org.eclipse.apoapsis.ortserver.services.SecretService
import org.eclipse.apoapsis.ortserver.shared.apimodel.PagedResponse
import org.eclipse.apoapsis.ortserver.shared.ktorutils.jsonBody
import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireIdParameter
import org.eclipse.apoapsis.ortserver.shared.ktorutils.standardListQueryParameters

internal fun Route.getAvailableSecretsByRepositoryId(
repositoryService: RepositoryService,
secretService: SecretService
) = get("/repositories/{repositoryId}/availableSecrets", {
operationId = "GetAvailableSecretsByRepositoryId"
summary = "Get all available secrets for a repository"
description = "Get all secrets that are available in the context of the provided repository. In addition to " +
"the repository secrets, this includes secrets from the product and organization the repository " +
"belongs to."
tags = listOf("Repositories")

request {
pathParameter<Long>("repositoryId") {
description = "The ID of the repository."
}
standardListQueryParameters()
}

response {
HttpStatusCode.OK to {
description = "Success"
jsonBody<List<Secret>> {
example("Get all available secrets for a repository") {
value = listOf(
Secret(name = "USERNAME-SECRET", description = "The username."),
Secret(name = "PASSWORD-SECRET", description = "The password.")
)
}
}
}
}
}) {
requirePermission(RepositoryPermission.READ)

val repositoryId = call.requireIdParameter("repositoryId")

val hierarchy = repositoryService.getHierarchy(repositoryId)
val secrets = secretService.listForHierarchy(hierarchy)

call.respond(HttpStatusCode.OK, secrets.map { it.mapToApi() })
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,38 @@ package org.eclipse.apoapsis.ortserver.components.secrets
import io.ktor.client.HttpClient
import io.ktor.server.testing.ApplicationTestBuilder

import io.mockk.mockk

import org.eclipse.apoapsis.ortserver.model.repositories.SecretRepository
import org.eclipse.apoapsis.ortserver.secrets.SecretStorage
import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting
import org.eclipse.apoapsis.ortserver.services.RepositoryService
import org.eclipse.apoapsis.ortserver.services.SecretService
import org.eclipse.apoapsis.ortserver.shared.ktorutils.AbstractIntegrationTest

/** An [AbstractIntegrationTest] pre-configured for testing the secrets routes. */
@Suppress("UnnecessaryAbstractClass")
abstract class SecretsIntegrationTest(body: SecretsIntegrationTest.() -> Unit) : AbstractIntegrationTest({}) {
lateinit var repositoryService: RepositoryService
lateinit var secretRepository: SecretRepository
lateinit var secretService: SecretService

val secretErrorPath = "error-path"

init {
beforeEach {
repositoryService = RepositoryService(
dbExtension.db,
dbExtension.fixtures.ortRunRepository,
dbExtension.fixtures.repositoryRepository,
dbExtension.fixtures.analyzerJobRepository,
dbExtension.fixtures.advisorJobRepository,
dbExtension.fixtures.scannerJobRepository,
dbExtension.fixtures.evaluatorJobRepository,
dbExtension.fixtures.reporterJobRepository,
dbExtension.fixtures.notifierJobRepository,
mockk()
)
secretRepository = dbExtension.fixtures.secretRepository
secretService = SecretService(
dbExtension.db,
Expand All @@ -53,7 +69,7 @@ abstract class SecretsIntegrationTest(body: SecretsIntegrationTest.() -> Unit) :
fun secretsTestApplication(
block: suspend ApplicationTestBuilder.(client: HttpClient) -> Unit
) = integrationTestApplication(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
validations = { secretsValidations() },
block = block
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.eclipse.apoapsis.ortserver.components.secrets.UpdateSecret
import org.eclipse.apoapsis.ortserver.components.secrets.secretsRoutes
import org.eclipse.apoapsis.ortserver.secrets.SecretStorage
import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting
import org.eclipse.apoapsis.ortserver.services.RepositoryService
import org.eclipse.apoapsis.ortserver.services.SecretService
import org.eclipse.apoapsis.ortserver.shared.apimodel.asPresent
import org.eclipse.apoapsis.ortserver.shared.ktorutils.AbstractAuthorizationTest
Expand All @@ -42,6 +43,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
var orgId = 0L
var prodId = 0L
var repoId = 0L
lateinit var repositoryService: RepositoryService
lateinit var secretService: SecretService

beforeEach {
Expand All @@ -51,6 +53,19 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({

authorizationService.ensureSuperuserAndSynchronizeRolesAndPermissions()

repositoryService = RepositoryService(
dbExtension.db,
dbExtension.fixtures.ortRunRepository,
dbExtension.fixtures.repositoryRepository,
dbExtension.fixtures.analyzerJobRepository,
dbExtension.fixtures.advisorJobRepository,
dbExtension.fixtures.scannerJobRepository,
dbExtension.fixtures.evaluatorJobRepository,
dbExtension.fixtures.reporterJobRepository,
dbExtension.fixtures.notifierJobRepository,
authorizationService
)

secretService = SecretService(
dbExtension.db,
dbExtension.fixtures.secretRepository,
Expand All @@ -62,7 +77,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"DeleteSecretByOrganizationIdAndName" should {
"require OrganizationPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = OrganizationPermission.WRITE_SECRETS.roleName(orgId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -74,7 +89,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"DeleteSecretByProductIdAndName" should {
"require ProductPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = ProductPermission.WRITE_SECRETS.roleName(prodId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -86,7 +101,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"DeleteSecretByRepositoryIdAndName" should {
"require RepositoryPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = RepositoryPermission.WRITE_SECRETS.roleName(repoId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -95,10 +110,21 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
}
}

"GetAvailableSecretsByRepositoryId" should {
"require RepositoryPermission.READ" {
requestShouldRequireRole(
routes = { secretsRoutes(repositoryService, secretService) },
role = RepositoryPermission.READ.roleName(repoId)
) {
get("/repositories/$repoId/secrets/availableSecrets")
}
}
}

"GetSecretByOrganizationIdAndName" should {
"require OrganizationPermission.READ" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = OrganizationPermission.READ.roleName(orgId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -110,7 +136,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"GetSecretByProductIdAndName" should {
"require ProductPermission.READ" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = ProductPermission.READ.roleName(prodId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -122,7 +148,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"GetSecretByRepositoryIdAndName" should {
"require RepositoryPermission.READ" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = RepositoryPermission.READ.roleName(repoId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -134,7 +160,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"GetSecretsByOrganizationId" should {
"require OrganizationPermission.READ" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = OrganizationPermission.READ.roleName(orgId)
) {
get("/organizations/$orgId/secrets")
Expand All @@ -145,7 +171,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"GetSecretsByProductId" should {
"require ProductPermission.READ" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = ProductPermission.READ.roleName(prodId)
) {
get("/products/$prodId/secrets")
Expand All @@ -156,7 +182,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"GetSecretsByRepositoryId" should {
"require RepositoryPermission.READ" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = RepositoryPermission.READ.roleName(repoId)
) {
get("/repositories/$repoId/secrets")
Expand All @@ -167,7 +193,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"PatchSecretByOrganizationIdAndName" should {
"require OrganizationPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = OrganizationPermission.WRITE_SECRETS.roleName(orgId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -180,7 +206,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"PatchSecretByProductIdAndName" should {
"require ProductPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = ProductPermission.WRITE_SECRETS.roleName(prodId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -193,7 +219,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"PatchSecretByRepositoryIdAndName" should {
"require RepositoryPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = RepositoryPermission.WRITE_SECRETS.roleName(repoId),
successStatus = HttpStatusCode.NotFound
) {
Expand All @@ -206,7 +232,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"PostSecretForOrganization" should {
"require OrganizationPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = OrganizationPermission.WRITE_SECRETS.roleName(orgId),
successStatus = HttpStatusCode.Created
) {
Expand All @@ -219,7 +245,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"PostSecretForProduct" should {
"require ProductPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = ProductPermission.WRITE_SECRETS.roleName(prodId),
successStatus = HttpStatusCode.Created
) {
Expand All @@ -232,7 +258,7 @@ class SecretsAuthorizationTest : AbstractAuthorizationTest({
"PostSecretForRepository" should {
"require RepositoryPermission.WRITE_SECRETS" {
requestShouldRequireRole(
routes = { secretsRoutes(secretService) },
routes = { secretsRoutes(repositoryService, secretService) },
role = RepositoryPermission.WRITE_SECRETS.roleName(repoId),
successStatus = HttpStatusCode.Created
) {
Expand Down
Loading
Loading