Skip to content

Commit

Permalink
POST /initial: fix auth docs
Browse files Browse the repository at this point in the history
verinice-veo#1116
  • Loading branch information
jj-sn committed Jan 29, 2025
1 parent 2422074 commit f9b7ba1
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 34 deletions.
34 changes: 0 additions & 34 deletions src/main/kotlin/org/veo/accounts/AccountController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package org.veo.accounts

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import jakarta.validation.Valid
import org.springframework.http.HttpStatus.CREATED
Expand All @@ -36,7 +35,6 @@ import org.springframework.web.bind.annotation.RestController
import org.veo.accounts.auth.parseAccount
import org.veo.accounts.dtos.AccountId
import org.veo.accounts.dtos.request.CreateAccountDto
import org.veo.accounts.dtos.request.CreateInitialAccountDto
import org.veo.accounts.dtos.request.UpdateAccountDto
import org.veo.accounts.dtos.response.AccountCreatedDto
import org.veo.accounts.dtos.response.FullAccountDto
Expand Down Expand Up @@ -65,38 +63,6 @@ class AccountController(
.getAccount(id, auth.parseAccount())
.let { FullAccountDto(it) }

@Operation(
description = "Create the initial account for a client. Returns the new account's ID.",
responses = [
ApiResponse(
responseCode = "201",
description = "Initial account created",
),
ApiResponse(
responseCode = "409",
description = "Username or email address already taken",
),
ApiResponse(
responseCode = "409",
description = "Accounts already exist in target veo client",
),
ApiResponse(
responseCode = "422",
description = "Target veo client does not exist",
),
],
)
@PostMapping(path = ["initial"])
@ResponseStatus(CREATED)
fun createInitialAccount(
@Valid
@RequestBody
dto: CreateInitialAccountDto,
): AccountCreatedDto =
dto
.let { accountService.createInitialAccount(it) }
.let { id -> AccountCreatedDto(id) }

@Operation(description = "Create an account. Returns the new account's ID.")
@PostMapping
@ResponseStatus(CREATED)
Expand Down
71 changes: 71 additions & 0 deletions src/main/kotlin/org/veo/accounts/InitialAccountController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* verinice.veo accounts
* Copyright (C) 2025 Jonas Jordan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.veo.accounts

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import jakarta.validation.Valid
import org.springframework.http.HttpStatus.CREATED
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.veo.accounts.dtos.request.CreateInitialAccountDto
import org.veo.accounts.dtos.response.AccountCreatedDto
import org.veo.accounts.keycloak.AccountService

@RestController
@RequestMapping("initial")
@SecurityRequirement(name = SECURITY_SCHEME_CLIENT_INIT_API_KEY)
class InitialAccountController(
private val accountService: AccountService,
) {
@Operation(
description = "Create the initial account for a client. Returns the new account's ID.",
responses = [
ApiResponse(
responseCode = "201",
description = "Initial account created",
),
ApiResponse(
responseCode = "409",
description = "Username or email address already taken",
),
ApiResponse(
responseCode = "409",
description = "Accounts already exist in target veo client",
),
ApiResponse(
responseCode = "422",
description = "Target veo client does not exist",
),
],
)
@PostMapping
@ResponseStatus(CREATED)
fun createInitialAccount(
@Valid
@RequestBody
dto: CreateInitialAccountDto,
): AccountCreatedDto =
dto
.let { accountService.createInitialAccount(it) }
.let { id -> AccountCreatedDto(id) }
}
11 changes: 11 additions & 0 deletions src/main/kotlin/org/veo/accounts/VeoAccountsApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PRO
import com.fasterxml.jackson.databind.ObjectMapper
import io.swagger.v3.oas.annotations.OpenAPIDefinition
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn.HEADER
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType.APIKEY
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType.OAUTH2
import io.swagger.v3.oas.annotations.info.Contact
import io.swagger.v3.oas.annotations.info.Info
Expand All @@ -34,6 +35,7 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Primary

const val SECURITY_SCHEME_OAUTH = "OAuth2"
const val SECURITY_SCHEME_CLIENT_INIT_API_KEY = "ClientInitApiKey"

@SpringBootApplication
@SecurityScheme(
Expand All @@ -49,6 +51,15 @@ const val SECURITY_SCHEME_OAUTH = "OAuth2"
),
),
)
@SecurityScheme(
name = SECURITY_SCHEME_CLIENT_INIT_API_KEY,
type = APIKEY,
`in` = HEADER,
description =
"Client initialization API key - only subscription services should know this key. " +
"It is required for creating the initial account for a client. " +
"The initial user may then authenticate with OAuth2 and create additional accounts in their client.",
)
@OpenAPIDefinition(
info =
Info(
Expand Down
41 changes: 41 additions & 0 deletions src/test/kotlin/org/veo/accounts/rest/SecurityRestTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
*/
package org.veo.accounts.rest

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import org.veo.accounts.Role.CREATE
import org.veo.accounts.Role.DELETE
import org.veo.accounts.Role.READ
import org.veo.accounts.Role.UPDATE
import org.veo.accounts.asListOfMaps
import org.veo.accounts.asMap
import java.util.UUID.randomUUID

class SecurityRestTest : AbstractRestTest() {
Expand Down Expand Up @@ -121,4 +124,42 @@ class SecurityRestTest : AbstractRestTest() {
get("/v3/api-docs", expectedStatus = 200)
get("/v3/api-docs/swagger-config", expectedStatus = 200)
}

@Test
fun `authorization methods are documented`() {
val docs = get("/v3/api-docs").bodyAsMap

docs["components"].asMap()["securitySchemes"].asMap().let {
it["OAuth2"].asMap()["type"] shouldBe "oauth2"
it["ClientInitApiKey"].asMap()["type"] shouldBe "apiKey"
}

val endpointDocs =
docs["paths"].asMap().flatMap { pathEntry ->
pathEntry.value.asMap().map { endpointEntry ->
EndpointDoc(
pathEntry.key,
endpointEntry.key,
endpointEntry.value
.asMap()["security"]
.asListOfMaps()
.flatMap { it.keys },
)
}
}

endpointDocs.forEach {
if (it.path == "/initial" && it.httpMethod == "post") {
it.securitySchemes shouldBe listOf("ClientInitApiKey")
} else {
it.securitySchemes shouldBe listOf("OAuth2")
}
}
}

data class EndpointDoc(
val path: String,
val httpMethod: String,
val securitySchemes: List<String>,
)
}

0 comments on commit f9b7ba1

Please sign in to comment.