diff --git a/dependencies.gradle b/dependencies.gradle index b32763a0509..2082f949ac5 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -84,6 +84,7 @@ libraries.springContextSupport = "org.springframework:spring-context-support" libraries.springJdbc = "org.springframework:spring-jdbc" libraries.springLdapCore = "org.springframework.ldap:spring-ldap-core" libraries.springRestdocs = "org.springframework.restdocs:spring-restdocs-mockmvc" +libraries.springdocOpenapi = "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0" libraries.springRetry = "org.springframework.retry:spring-retry" libraries.springSecurityConfig = "org.springframework.security:spring-security-config" libraries.springSecurityCore = "org.springframework.security:spring-security-core" diff --git a/server/build.gradle b/server/build.gradle index e4874fc6f17..cfe490910c4 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -35,6 +35,9 @@ dependencies { implementation(libraries.guava) + // OpenAPI documentation + implementation(libraries.springdocOpenapi) + implementation(libraries.aspectJRt) implementation(libraries.aspectJWeaver) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlSecurityConfiguration.java b/server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlSecurityConfiguration.java index a0362b2147c..7032fefc0ad 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlSecurityConfiguration.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlSecurityConfiguration.java @@ -67,6 +67,12 @@ public class SpringServletXmlSecurityConfiguration { "/session_management", "/oauth/token/.well-known/openid-configuration", "/.well-known/openid-configuration", + // OpenAPI documentation endpoints + "/v3/api-docs/**", + "/v3/api-docs", + "/v3/api-docs.yaml", + "/swagger-ui/**", + "/swagger-ui.html", "/logged_out" }; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/UaaConfiguration.java b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/UaaConfiguration.java index 7402730adc3..a77248a1ba9 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/UaaConfiguration.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/UaaConfiguration.java @@ -117,6 +117,7 @@ public class UaaConfiguration { public Integer groupMaxCount; public Integer clientMaxCount; public RateLimit ratelimit; + public Map springdoc; public static class Zones { @Valid diff --git a/uaa/build.gradle b/uaa/build.gradle index 721462e19de..4b9913d701f 100644 --- a/uaa/build.gradle +++ b/uaa/build.gradle @@ -59,6 +59,9 @@ dependencies { implementation(libraries.braveInstrumentation) implementation(libraries.braveContextSlf4j) + + // OpenAPI documentation + implementation(libraries.springdocOpenapi) implementation(libraries.springWeb) implementation(libraries.springWebMvc) diff --git a/uaa/src/main/java/org/cloudfoundry/identity/uaa/OpenApiConfiguration.java b/uaa/src/main/java/org/cloudfoundry/identity/uaa/OpenApiConfiguration.java new file mode 100644 index 00000000000..2202eb6f818 --- /dev/null +++ b/uaa/src/main/java/org/cloudfoundry/identity/uaa/OpenApiConfiguration.java @@ -0,0 +1,80 @@ +package org.cloudfoundry.identity.uaa; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.cloudfoundry.identity.uaa.home.BuildInfo; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * OpenAPI 3.0 configuration for UAA API documentation. + * + * This configuration provides interactive API documentation for all UAA endpoints, + * including users, groups, clients, identity zones, and identity providers. + */ +@Configuration +public class OpenApiConfiguration { + + private final BuildInfo buildInfo; + + public OpenApiConfiguration(BuildInfo buildInfo) { + this.buildInfo = buildInfo; + } + + @Bean + public OpenAPI uaaOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("UAA API Reference") + .description(""" + UAA (User Account and Authentication) is an OAuth2/OpenID Connect server + for centralized identity management. This API reference provides endpoints + for managing users, groups, and client applications. + + Key Features: + - OAuth2 & OpenID Connect authentication + - SCIM 2.0 user and group management + - Identity provider integration (SAML, LDAP, OIDC) + - Multi-tenancy via identity zones + - Client application management + """) + .version(buildInfo.getVersion()) + .contact(new Contact() + .name("Cloud Foundry Foundation") + .url("https://github.com/cloudfoundry/uaa")) + .license(new License() + .name("Apache 2.0") + .url("https://www.apache.org/licenses/LICENSE-2.0"))) + .servers(List.of( + new Server() + .url(buildInfo.getUaaUrl()) + .description("UAA Server") + )) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) + .components(new Components() + .addSecuritySchemes("bearerAuth", new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description(""" + OAuth2 Bearer token (JWT format). + + Required scopes vary by endpoint: + - OAuth/Token: uaa.admin, clients.admin + - Users/Groups: scim.read, scim.write, groups.update + - Clients: clients.read, clients.write, clients.admin + - Identity Zones: zones.read, zones.write, uaa.admin + - Identity Providers: idps.read, idps.write + + Obtain tokens via /oauth/token endpoint. + """))); + } +} \ No newline at end of file diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 3f47ed49da0..166324fa048 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -590,4 +590,25 @@ ldap: maxSearchDepth: 10 autoAdd: true externalGroupsWhitelist: - - '*' \ No newline at end of file + - '*' + +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + resolve-schema-properties: true + swagger-ui: + enabled: true + path: /swagger-ui.html + url: /v3/api-docs + disable-swagger-default-url: true + try-it-out-enabled: true + tags-sorter: alpha + operations-sorter: alpha + show-actuator: false + default-consumes-media-type: application/json + default-produces-media-type: application/json + use-management-port: false + writer-with-default-pretty-printer: false + model-and-view-allowed: false + override-with-generic-response: false \ No newline at end of file diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/OpenApiConfigurationTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/OpenApiConfigurationTest.java new file mode 100644 index 00000000000..beb6f2bc32a --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/OpenApiConfigurationTest.java @@ -0,0 +1,119 @@ +package org.cloudfoundry.identity.uaa; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.cloudfoundry.identity.uaa.home.BuildInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class OpenApiConfigurationTest { + + private BuildInfo buildInfo; + private OpenApiConfiguration openApiConfiguration; + + @BeforeEach + void setUp() { + buildInfo = mock(BuildInfo.class); + when(buildInfo.getVersion()).thenReturn("1.0.0-test"); + when(buildInfo.getUaaUrl()).thenReturn("https://uaa.example.com"); + openApiConfiguration = new OpenApiConfiguration(buildInfo); + } + + @Test + void uaaOpenAPIBeanIsCreated() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + assertThat(openAPI).isNotNull(); + } + + @Test + void openAPIHasCorrectTitle() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + assertThat(openAPI.getInfo().getTitle()).isEqualTo("UAA API Reference"); + } + + @Test + void openAPIHasDescriptionWithKeyFeatures() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + String description = openAPI.getInfo().getDescription(); + assertThat(description) + .contains("OAuth2/OpenID Connect") + .contains("SCIM 2.0") + .contains("Identity provider integration") + .contains("Multi-tenancy"); + } + + @Test + void openAPIVersionComesFromBuildInfo() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + assertThat(openAPI.getInfo().getVersion()).isEqualTo("1.0.0-test"); + } + + @Test + void openAPIHasCorrectContactInfo() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + assertThat(openAPI.getInfo().getContact().getName()).isEqualTo("Cloud Foundry Foundation"); + assertThat(openAPI.getInfo().getContact().getUrl()).isEqualTo("https://github.com/cloudfoundry/uaa"); + } + + @Test + void openAPIHasApache2License() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + assertThat(openAPI.getInfo().getLicense().getName()).isEqualTo("Apache 2.0"); + assertThat(openAPI.getInfo().getLicense().getUrl()).isEqualTo("https://www.apache.org/licenses/LICENSE-2.0"); + } + + @Test + void openAPIServerUrlComesFromBuildInfo() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + assertThat(openAPI.getServers()).hasSize(1); + assertThat(openAPI.getServers().get(0).getUrl()).isEqualTo("https://uaa.example.com"); + assertThat(openAPI.getServers().get(0).getDescription()).isEqualTo("UAA Server"); + } + + @Test + void openAPIHasBearerAuthSecurityRequirement() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + assertThat(openAPI.getSecurity()).hasSize(1); + assertThat(openAPI.getSecurity().get(0).get("bearerAuth")).isNotNull(); + } + + @Test + void openAPIHasBearerAuthSecurityScheme() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + SecurityScheme securityScheme = openAPI.getComponents().getSecuritySchemes().get("bearerAuth"); + assertThat(securityScheme).isNotNull(); + assertThat(securityScheme.getType()).isEqualTo(SecurityScheme.Type.HTTP); + assertThat(securityScheme.getScheme()).isEqualTo("bearer"); + assertThat(securityScheme.getBearerFormat()).isEqualTo("JWT"); + } + + @Test + void securitySchemeDescriptionDocumentsRequiredScopes() { + OpenAPI openAPI = openApiConfiguration.uaaOpenAPI(); + + String description = openAPI.getComponents().getSecuritySchemes().get("bearerAuth").getDescription(); + assertThat(description) + .contains("uaa.admin") + .contains("scim.read") + .contains("scim.write") + .contains("clients.read") + .contains("clients.write") + .contains("zones.read") + .contains("zones.write") + .contains("idps.read") + .contains("idps.write"); + } +}