Skip to content
Open
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
1 change: 1 addition & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ dependencies {

implementation(libraries.guava)

// OpenAPI documentation
implementation(libraries.springdocOpenapi)

implementation(libraries.aspectJRt)
implementation(libraries.aspectJWeaver)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public class UaaConfiguration {
public Integer groupMaxCount;
public Integer clientMaxCount;
public RateLimit ratelimit;
public Map<String, Object> springdoc;

public static class Zones {
@Valid
Expand Down
3 changes: 3 additions & 0 deletions uaa/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ dependencies {

implementation(libraries.braveInstrumentation)
implementation(libraries.braveContextSlf4j)

// OpenAPI documentation
implementation(libraries.springdocOpenapi)

implementation(libraries.springWeb)
implementation(libraries.springWebMvc)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
""")));
}
}
23 changes: 22 additions & 1 deletion uaa/src/main/resources/uaa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -590,4 +590,25 @@ ldap:
maxSearchDepth: 10
autoAdd: true
externalGroupsWhitelist:
- '*'
- '*'

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
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading