Adding openapi to cxf

This commit is contained in:
Martin Stockhammer 2020-07-13 22:53:15 +02:00
parent 7dca6a23be
commit bf23b137d7
17 changed files with 192 additions and 14 deletions

View File

@ -595,6 +595,11 @@
<artifactId>cxf-rt-rs-extension-providers</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-service-description-openapi-v3</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>

View File

@ -80,9 +80,6 @@
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -18,6 +18,8 @@
* under the License.
*/
import io.swagger.v3.oas.annotations.media.Schema;
import javax.xml.bind.annotation.XmlRootElement;
import java.time.OffsetDateTime;
@ -25,6 +27,7 @@
* @author Martin Stockhammer <martin_s@apache.org>
*/
@XmlRootElement(name="pingResult")
@Schema(name="PingResult", description = "Response of a ping request.")
public class PingResult
{
boolean success;
@ -39,6 +42,7 @@ public PingResult( boolean success ) {
this.requestTime = OffsetDateTime.now( );
}
@Schema(description = "Request successfully parsed and response sent")
public boolean isSuccess( )
{
return success;
@ -49,6 +53,7 @@ public void setSuccess( boolean success )
this.success = success;
}
@Schema( description = "The time, when the request arrived on the server" )
public OffsetDateTime getRequestTime( )
{
return requestTime;

View File

@ -18,6 +18,8 @@
* under the License.
*/
import io.swagger.v3.oas.annotations.media.Schema;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@ -25,6 +27,7 @@
* @author Martin Stockhammer <martin_s@apache.org>
*/
@XmlRootElement(name="refreshToken")
@Schema(name="Request Token Data", description = "Schema used for requesting a Bearer token.")
public class RequestTokenRequest
{
String grantType = "";
@ -55,6 +58,7 @@ public RequestTokenRequest( String userId, String password, String scope )
}
@XmlElement(name = "grant_type", required = true, nillable = false)
@Schema(description = "The grant type. Normally 'authorization_code'.")
public String getGrantType( )
{
return grantType;
@ -99,18 +103,19 @@ public void setScope( String scope )
}
@XmlElement(name="user_id", required = true, nillable = false)
@Schema(description = "The user identifier.")
public String getUserId( )
{
return userId;
}
@XmlElement(name="user_id", required = true, nillable = false)
public void setUserId( String userId )
{
this.userId = userId;
}
@XmlElement(name="password", required = true, nillable = false)
@Schema(description = "The user password")
public String getPassword( )
{
return password;

View File

@ -19,6 +19,8 @@
*/
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.archiva.redback.authorization.RedbackAuthorization;
import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants;
@ -43,7 +45,9 @@
* @since 2.1
*/
@Path("/ldapGroupMappingService/")
@Tag( name = "LDAP", description = "LDAP Service" )
@Tag( name = "v1" )
@Tag( name = "v1/LDAP" )
@SecurityScheme( scheme = "BasicAuth", type = SecuritySchemeType.HTTP )
@Deprecated
public interface LdapGroupMappingService
{

View File

@ -20,6 +20,9 @@
*/
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.archiva.redback.authorization.RedbackAuthorization;
import org.apache.archiva.redback.keys.AuthenticationKey;
import org.apache.archiva.redback.rest.api.model.ActionStatus;
@ -37,6 +40,9 @@
@Deprecated
@Path( "/loginService/" )
@Tag(name = "v1")
@Tag(name = "v1/Login")
@SecurityScheme( scheme = "BasicAuth", type = SecuritySchemeType.HTTP )
public interface LoginService
{

View File

@ -20,7 +20,12 @@
*/
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.archiva.redback.authorization.RedbackAuthorization;
import org.apache.archiva.redback.rest.api.model.ActionStatus;
import org.apache.archiva.redback.rest.api.model.LoginRequest;
@ -43,6 +48,9 @@
* Version 2 of authentication service
*/
@Path( "/auth" )
@SecurityScheme( scheme = "BearerAuth", type = SecuritySchemeType.HTTP )
@Tag(name = "v2")
@Tag(name = "v2/Authentication")
public interface AuthenticationService
{
@ -58,6 +66,7 @@ PingResult ping()
@GET
@Produces( { MediaType.APPLICATION_JSON } )
@RedbackAuthorization( noRestriction = false, noPermission = true )
@Operation( summary = "Ping request to restricted service. You have to provide a valid authentication token." )
PingResult pingWithAutz()
throws RedbackServiceException;
@ -72,7 +81,8 @@ PingResult pingWithAutz()
@Produces( { MediaType.APPLICATION_JSON } )
@Operation( summary = "Authenticate by user/password login and return a bearer token, usable for further requests",
responses = {
@ApiResponse( description = "The bearer token. The token data contains the token string that should be added to the Bearer header" )
@ApiResponse( description = "A access token, that has to be added to the Authorization header on authenticated requests. " +
"And refresh token, used to refresh the access token. Each token as a lifetime. After expiration it cannot be used anymore." )
}
)
TokenResponse logIn( RequestTokenRequest loginRequest )

View File

@ -20,7 +20,9 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.archiva.redback.authorization.RedbackAuthorization;
import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants;
@ -49,7 +51,9 @@
* @since 2.1
*/
@Path( "/groups" )
@Tag( name = "Groups", description = "Groups and Group to Role Mappings" )
@SecurityScheme( scheme = "BearerAuth", type = SecuritySchemeType.HTTP )
@Tag(name = "v2")
@Tag(name = "v2/Groups")
public interface GroupService
{

View File

@ -1,15 +1,41 @@
resourcePackages:
- org.apache.archiva.redback.rest.api
prettyPrint: true
cacheTTL: 0
openAPI:
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
BasicAuth:
type: http
scheme: basic
servers:
- url: /api/v2/redback
description: Base URL Version 2 API
- url: /redbackServices
description: Base URL Version 1 API
tags:
- name: v2
description: Version 2 REST API
- name: v2/Authentication
description: Authentication operations for Login and token refresh
- name: v2/Groups
description: Group operations
- name: v1
description: Version 1 REST API (deprecated)
info:
version: '3.0'
title: Apache Archiva Redback REST API
description: 'This is the Apache Archiva Redback REST API documentation'
termsOfService: https://archiva.apache.org
contact:
email: dev@archiva.apache.org
email: users@archiva.apache.org
url: https://archiva.apache.org/index.html
license:
name: Apache 2.0

View File

@ -216,6 +216,15 @@
<artifactId>cxf-rt-rs-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-service-description-openapi-v3</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>3.28.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>

View File

@ -103,11 +103,18 @@ protected void setResourceInfo( ResourceInfo resourceInfo )
public void filter( ContainerRequestContext requestContext ) throws IOException
{
log.debug( "Intercepting request for bearer token" );
log.debug( "Request {}", requestContext.getUriInfo( ).getPath( ) );
final String requestPath = requestContext.getUriInfo( ).getPath( );
if ("api-docs".equals(requestPath) || requestPath.startsWith( "api-docs/" )
|| "openapi.json".equals(requestPath)) {
return;
}
// If no redback resource info, we deny the request
RedbackAuthorization redbackAuthorization = getRedbackAuthorization( resourceInfo );
if ( redbackAuthorization == null )
{
log.warn( "http path {} doesn't contain any informations regarding permissions ",
log.warn( "Request path {} doesn't contain any information regarding permissions. Denying access.",
requestContext.getUriInfo( ).getRequestUri( ) );
// here we failed to authenticate so 403 as there is no detail on karma for this
// it must be marked as it's exposed
@ -117,7 +124,7 @@ public void filter( ContainerRequestContext requestContext ) throws IOException
String bearerHeader = StringUtils.defaultIfEmpty( requestContext.getHeaderString( "Authorization" ), "" ).trim( );
if ( !"".equals( bearerHeader ) )
{
log.debug( "Found token" );
log.debug( "Found Bearer token in header" );
String bearerToken = bearerHeader.replaceFirst( "\\s*Bearer\\s+(\\S+)\\s*", "$1" );
final HttpServletRequest request = getHttpServletRequest( );
BearerTokenAuthenticationDataSource source = new BearerTokenAuthenticationDataSource( "", bearerToken );
@ -226,6 +233,8 @@ public void filter( ContainerRequestContext requestContext ) throws IOException
}
} else {
log.debug( "No Bearer token found" );
}
}
}

View File

@ -74,6 +74,12 @@ public class PermissionsInterceptor
public void filter( ContainerRequestContext containerRequestContext )
{
log.debug( "Filtering request" );
final String requestPath = containerRequestContext.getUriInfo( ).getPath( );
if ("api-docs".equals(requestPath) || requestPath.startsWith( "api-docs/" )
|| "openapi.json".equals(requestPath)) {
return;
}
RedbackAuthorization redbackAuthorization = getRedbackAuthorization( resourceInfo );
if ( redbackAuthorization != null )
@ -85,11 +91,11 @@ public void filter( ContainerRequestContext containerRequestContext )
return;
}
String[] permissions = redbackAuthorization.permissions();
HttpServletRequest request = getHttpServletRequest( );
//olamy: no value is an array with an empty String
if ( permissions != null && permissions.length > 0 //
&& !( permissions.length == 1 && StringUtils.isEmpty( permissions[0] ) ) )
{
HttpServletRequest request = getHttpServletRequest( );
SecuritySession securitySession = getSecuritySession( containerRequestContext, httpAuthenticator, request );
AuthenticationResult authenticationResult = getAuthenticationResult( containerRequestContext, httpAuthenticator, request );
log.debug( "authenticationResult from message: {}", authenticationResult );
@ -157,8 +163,15 @@ public void filter( ContainerRequestContext containerRequestContext )
{
if ( redbackAuthorization.noPermission() )
{
log.debug( "path {} doesn't need special permission", containerRequestContext.getUriInfo().getRequestUri() );
return;
AuthenticationResult authenticationResult = getAuthenticationResult( containerRequestContext, httpAuthenticator, request );
if (authenticationResult!=null && authenticationResult.isAuthenticated())
{
log.debug( "Path {} doesn't need special permission. User authenticated.", requestPath );
return;
} else {
log.debug( "Path {} is protected and needs authentication. User not authenticated.", requestPath );
containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
}
}
containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
return;

View File

@ -376,8 +376,16 @@ public void init()
public void filter( ContainerRequestContext containerRequestContext )
throws IOException
{
if ( enabled )
{
final String requestPath = containerRequestContext.getUriInfo( ).getPath( );
if ("api-docs".equals(requestPath) || requestPath.startsWith( "api-docs/" )
|| "openapi.json".equals(requestPath)) {
return;
}
HttpServletRequest request = getRequest();
List<URL> targetUrls = getTargetUrl( request );
if ( targetUrls == null )

View File

@ -51,6 +51,11 @@
</bean>
<bean id="redbackJacksonXMLMapper" class="com.fasterxml.jackson.dataformat.xml.XmlMapper" >
</bean>
<!-- CXF OpenApiFeature -->
<bean id="openApiFeature" class="org.apache.cxf.jaxrs.openapi.OpenApiFeature">
<property name="scanKnownConfigLocations" value="true"/>
<!-- customize some of the properties -->
</bean>
<jaxrs:server name="redbackServices" address="/redbackServices">
@ -95,6 +100,9 @@
<ref bean="requestValidationInterceptor#rest" />
<ref bean="threadLocalUserCleaner#rest"/>
</jaxrs:providers>
<jaxrs:features>
<ref bean="openApiFeature" />
</jaxrs:features>
</jaxrs:server>
</beans>

View File

@ -31,10 +31,14 @@
import org.junit.runners.JUnit4;
import org.springframework.mock.web.MockHttpServletRequest;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Created by Martin Stockhammer on 21.01.17.
@ -57,6 +61,10 @@ public void validateRequestWithoutHeader() throws UserConfigurationException, IO
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertTrue( ctx.isAborted() );
}
@ -74,6 +82,10 @@ public void validateRequestWithOrigin() throws UserConfigurationException, IOExc
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertFalse( ctx.isAborted() );
}
@ -91,6 +103,10 @@ public void validateRequestWithBadOrigin() throws UserConfigurationException, IO
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertTrue( ctx.isAborted() );
}
@ -108,6 +124,10 @@ public void validateRequestWithReferer() throws UserConfigurationException, IOEx
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertFalse( ctx.isAborted() );
}
@ -125,6 +145,10 @@ public void validateRequestWithBadReferer() throws UserConfigurationException, I
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertTrue( ctx.isAborted() );
}
@ -143,6 +167,10 @@ public void validateRequestWithOriginAndReferer() throws UserConfigurationExcept
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertFalse( ctx.isAborted() );
}
@ -162,6 +190,10 @@ public void validateRequestWithOriginAndRefererAndXForwarded() throws UserConfig
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertFalse( ctx.isAborted() );
}
@ -181,6 +213,9 @@ public void validateRequestWithOriginAndRefererAndWrongXForwarded() throws UserC
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertTrue( ctx.isAborted() );
}
@ -200,6 +235,10 @@ public void validateRequestWithOriginAndRefererAndXForwardedMultiple() throws Us
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertFalse( ctx.isAborted() );
}
@ -221,6 +260,10 @@ public void validateRequestWithOriginAndStaticUrl() throws UserConfigurationExce
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertFalse( ctx.isAborted() );
}
@ -241,6 +284,10 @@ public void validateRequestWithBadOriginAndStaticUrl() throws UserConfigurationE
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertTrue( ctx.isAborted() );
}
@ -264,6 +311,10 @@ public void validateRequestWithOriginListAndStaticUrl() throws UserConfiguration
interceptor.setHttpRequest( request );
interceptor.init();
MockContainerRequestContext ctx = new MockContainerRequestContext();
UriInfo uriInfo = mock( UriInfo.class );
when( uriInfo.getPath( ) ).thenReturn( "/api/v1/userService" );
ctx.setUriInfo( uriInfo );
interceptor.filter( ctx );
assertFalse( ctx.isAborted() );
}

View File

@ -43,6 +43,7 @@
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
@ -107,6 +108,9 @@ void filter() throws IOException, NoSuchMethodException, UserManagerException
doReturn( DefaultAuthenticationService.class ).when( resourceInfo ).getResourceClass( );
ContainerRequestContext context = mock( ContainerRequestContext.class );
when( context.getHeaderString( "Authorization" ) ).thenReturn( "Bearer " + token.getData( ) );
UriInfo uriInfo = mock( UriInfo.class );
when( context.getUriInfo( ) ).thenReturn( uriInfo );
when( uriInfo.getPath( ) ).thenReturn( "/api/v2/redback/auth/ping" );
User user = new SimpleUser( );
user.setUsername( "gandalf" );
when( userManager.findUser( "gandalf" ) ).thenReturn( user );
@ -127,6 +131,10 @@ void filterWithInvalidToken() throws IOException, NoSuchMethodException
doReturn( DefaultAuthenticationService.class ).when( resourceInfo ).getResourceClass( );
ContainerRequestContext context = mock( ContainerRequestContext.class );
when( context.getHeaderString( "Authorization" ) ).thenReturn( "Bearer xxxxx" );
UriInfo uriInfo = mock( UriInfo.class );
when( context.getUriInfo( ) ).thenReturn( uriInfo );
when( uriInfo.getPath( ) ).thenReturn( "/api/v2/redback/auth/ping/authenticated" );
interceptor.filter( context);
verify( context, times(1) ).abortWith( argThat( response -> response.getStatus() == 401 ) );
verify( httpServletResponse, times(1) ).setHeader( eq("WWW-Authenticate"), anyString( ) );
@ -143,6 +151,10 @@ void filterWithInvalidTokenUnrestrictedMethod() throws IOException, NoSuchMethod
doReturn( DefaultAuthenticationService.class ).when( resourceInfo ).getResourceClass( );
ContainerRequestContext context = mock( ContainerRequestContext.class );
when( context.getHeaderString( "Authorization" ) ).thenReturn( "Bearer xxxxx" );
UriInfo uriInfo = mock( UriInfo.class );
when( context.getUriInfo( ) ).thenReturn( uriInfo );
when( uriInfo.getPath( ) ).thenReturn( "/api/v2/redback/auth/ping" );
interceptor.filter( context);
RedbackRequestInformation info = RedbackAuthenticationThreadLocal.get( );
assertNull( info );

View File

@ -43,6 +43,8 @@ public class MockContainerRequestContext implements ContainerRequestContext {
private boolean aborted = false;
private UriInfo uriInfo;
@Override
public Object getProperty(String s) {
return null;
@ -65,7 +67,11 @@ public void removeProperty(String s) {
@Override
public UriInfo getUriInfo() {
return null;
return uriInfo;
}
public void setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
}
@Override