diff --git a/pom.xml b/pom.xml index d3a2f06d..981de3d1 100644 --- a/pom.xml +++ b/pom.xml @@ -329,6 +329,12 @@ redback-common-configuration-api ${project.version} + + org.apache.archiva.redback + redback-authentication-jwt + ${project.version} + + commons-logging @@ -421,6 +427,20 @@ ${slf4j.version} test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + org.easymock easymock @@ -537,6 +557,12 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${jackson.version} + + org.apache.cxf diff --git a/redback-authentication/redback-authentication-api/pom.xml b/redback-authentication/redback-authentication-api/pom.xml index cf3629cd..dad8ad31 100644 --- a/redback-authentication/redback-authentication-api/pom.xml +++ b/redback-authentication/redback-authentication-api/pom.xml @@ -41,6 +41,10 @@ org.apache.archiva.redback redback-users-api + + org.apache.archiva.redback + redback-configuration + jakarta.inject diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationConstants.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationConstants.java index 836e7f2f..94c85966 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationConstants.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationConstants.java @@ -44,4 +44,5 @@ public class AuthenticationConstants */ public static final int AUTHN_MUST_CHANGE_PASSWORD_EXCEPTION = 4; + } diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationControl.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationControl.java new file mode 100644 index 00000000..6a90effe --- /dev/null +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationControl.java @@ -0,0 +1,63 @@ +package org.apache.archiva.redback.authentication; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +/** + * These have the same meaning as for PAM modules + * + *
+ *
required
+ *
If a ‘required’ module returns a status that is not ‘success’, + * the operation will ultimately fail, but only after the modules below + * it are invoked. This seems senseless at first glance I suppose, but + * it serves the purpose of always acting the same way from the point + * of view of the user trying to utilize the service. The net effect is + * that it becomes impossible for a potential cracker to determine + * which module caused the failure – and the less information a + * malicious user has about your system, the better. Important to note + * is that even if all of the modules in the stack succeed, failure of + * one ‘required’ module means the operation will ultimately fail. Of + * course, if a required module succeeds, the operation can still fail + * if a ‘required’ module later in the stack fails.
+ *
requisite
+ *
If a ‘requisite’ module fails, the operation not only fails, but + * the operation is immediately terminated with a failure without + * invoking any other modules: ‘do not pass go, do not collect $200’, + * so to speak.
+ *
sufficient
+ *
If a sufficient module succeeds, it is enough to satisfy the + * requirements of sufficient modules in that realm for use of the + * service, and modules below it that are also listed as ‘sufficient’ + * are not invoked. If it fails, the operation fails unless a module + * invoked after it succeeds. Important to note is that if a ‘required’ + * module fails before a ‘sufficient’ one succeeds, the operation will + * fail anyway, ignoring the status of any ‘sufficient’ modules.
+ *
optional
+ *
An ‘optional’ module, according to the pam(8) manpage, will only + * cause an operation to fail if it’s the only module in the stack for + * that facility.
+ *
+ * + * @author Martin Stockhammer + * @since 3.0 + */ +public enum AuthenticationControl +{ + SUFFICIENT, OPTIONAL, REQUIRED, REQUISITE +} diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationManager.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationManager.java index 96413aca..0780228a 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationManager.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationManager.java @@ -27,14 +27,50 @@ /** * AuthenticationManager: * - * @author: Jesse McConnell + * @author Jesse McConnell + * @author Martin Stockhammer */ public interface AuthenticationManager { + /** + * Returns the identifier of this authentication manager + * @return the identifier string + */ String getId(); + /** + * Returns the list of authenticators in the same order as they are called for authentication + * @return the list of authenticators. + */ List getAuthenticators(); + /** + * Authenticates by calling all authenticators in the defined order. + * + * @param source the authentication data + * @return the result that gives information, if the authentication was successful + * @throws AccountLockedException if the account is locked + * @throws AuthenticationException if something unexpected happend during authentication + * @throws MustChangePasswordException if the user has to change his password + */ AuthenticationResult authenticate( AuthenticationDataSource source ) throws AccountLockedException, AuthenticationException, MustChangePasswordException; + + /** + * Returns the authenticator controls that are used to control the order and actions during authentication. + * @return the list of controls + */ + List getControls(); + + /** + * Sets the list of authenticator controls + * @param controlList the list of control instances + */ + void setControls( List controlList); + + /** + * Modifies the control for a single authenticator + * @param control the authenticator control + */ + void modifyControl(AuthenticatorControl control); } \ No newline at end of file diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationResult.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationResult.java index 6c56f2f1..cc02a3d0 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationResult.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticationResult.java @@ -96,7 +96,7 @@ public List getAuthenticationFailureCauses() { if ( authenticationFailureCauses == null ) { - this.authenticationFailureCauses = new ArrayList(); + this.authenticationFailureCauses = new ArrayList<>( ); } return authenticationFailureCauses; } diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticatorControl.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticatorControl.java new file mode 100644 index 00000000..78e351ab --- /dev/null +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/AuthenticatorControl.java @@ -0,0 +1,76 @@ +package org.apache.archiva.redback.authentication; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +/** + * Gives a priority and what to do, if the authentication succeeds. + * + * @author Martin Stockhammer + * @since 3.0 + */ +public class AuthenticatorControl implements Comparable +{ + final String name; + final int priority; + final boolean active; + final AuthenticationControl control; + + public AuthenticatorControl( String name, int priority, AuthenticationControl control ) + { + this.name = name; + this.priority = priority; + this.control = control; + this.active = true; + } + + public AuthenticatorControl( String name, int priority, AuthenticationControl control, boolean active) + { + this.name = name; + this.priority = priority; + this.control = control; + this.active = active; + } + + + public String getName( ) + { + return name; + } + + public int getPriority( ) + { + return priority; + } + + public AuthenticationControl getControl( ) + { + return control; + } + + @Override + public int compareTo( AuthenticatorControl o ) + { + return this.getPriority()-o.getPriority(); + } + + public boolean isActive( ) + { + return active; + } +} diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/BearerTokenAuthenticationDataSource.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/BearerTokenAuthenticationDataSource.java new file mode 100644 index 00000000..f7139172 --- /dev/null +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/BearerTokenAuthenticationDataSource.java @@ -0,0 +1,64 @@ +package org.apache.archiva.redback.authentication; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +/** + * Datasource used for authentication by Bearer token (JWT) + * + * @author Martin Stockhammer + * @since 3.0 + */ +public class BearerTokenAuthenticationDataSource implements AuthenticationDataSource +{ + private String tokenData; + private String principal; + + public BearerTokenAuthenticationDataSource( ) + { + } + + public BearerTokenAuthenticationDataSource( String principal, String tokenData ) + { + this.tokenData = tokenData; + this.principal = principal; + } + + @Override + public String getUsername( ) + { + return principal; + } + + @Override + public boolean isEnforcePasswordChange( ) + { + return false; + } + + public String getTokenData( ) + { + return tokenData; + } + + public void setTokenData( String tokenData ) + { + this.tokenData = tokenData; + } + +} diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/DefaultAuthenticationManager.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/DefaultAuthenticationManager.java index 1bc47707..a8c09f2f 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/DefaultAuthenticationManager.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/DefaultAuthenticationManager.java @@ -19,6 +19,7 @@ * under the License. */ +import org.apache.archiva.redback.configuration.UserConfiguration; import org.apache.archiva.redback.policy.AccountLockedException; import org.apache.archiva.redback.policy.MustChangePasswordException; import org.apache.archiva.redback.users.User; @@ -33,9 +34,18 @@ import javax.inject.Inject; import javax.inject.Named; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; /** @@ -48,51 +58,118 @@ * * @author: Jesse McConnell */ -@Service("authenticationManager") +@Service( "authenticationManager" ) public class DefaultAuthenticationManager - implements AuthenticationManager { + implements AuthenticationManager +{ - private Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger( DefaultAuthenticationManager.class ); - private List authenticators; + final private AtomicReference> authenticators = new AtomicReference<>( ); + final private AtomicReference> controls = new AtomicReference<>( ); + final private AtomicReference> modfiedControls = new AtomicReference<>( ); + private Map availableAuthenticators; @Inject private ApplicationContext applicationContext; @Inject - @Named(value = "userManager#default") + @Named("userConfiguration#default") + private UserConfiguration userConfiguration; + + @Inject + @Named( "userManager#default" ) private UserManager userManager; - @SuppressWarnings("unchecked") + @SuppressWarnings( "unchecked" ) @PostConstruct - public void initialize() { - this.authenticators = - new ArrayList(applicationContext.getBeansOfType(Authenticator.class).values()); + public void initialize( ) + { + this.availableAuthenticators = + applicationContext.getBeansOfType( Authenticator.class ).values( ).stream( ).collect( Collectors.toMap( a -> a.getId( ), authenticator -> authenticator ) ); + this.modfiedControls.set( new HashMap<>( ) ); + initializeOrder( ); + } + + private void initializeOrder() { + Stream controlStream = initControls( ); + final List authenticators = new ArrayList<>( ); + final Map controls = new LinkedHashMap<>( ); + controlStream.forEachOrdered( control -> { + authenticators.add( this.availableAuthenticators.get( control.getName( ) ) ); + controls.put( control.getName( ), control ); + } ); + this.authenticators.set( authenticators ); + this.controls.set( controls ); + } + + Map getConfigControls( ) + { + return new HashMap<>( ); + } + + Map getAvailableAuthenticators() { + return this.availableAuthenticators; + } + + Map getModifiedControls() { + return this.modfiedControls.get(); + } + + void setModfiedControls(Map newControls) { + this.modfiedControls.set( newControls ); + } + + Map getControlMap() { + return controls.get(); } - public String getId() { - return "Default Authentication Manager - " + this.getClass().getName() + " : managed authenticators - " + - knownAuthenticators(); + private Stream initControls( ) + { + Collection authenticators = getAvailableAuthenticators().values(); + Map nondefault = getModifiedControls( ); + Map configControlMap = getConfigControls( ); + Spliterator spliterator = Spliterators.spliterator( authenticators, Spliterator.NONNULL ); + return StreamSupport.stream( spliterator, false ).map( authenticator -> { + final String id = authenticator.getId( ); + return nondefault.containsKey( id ) ? nondefault.get(id) : (configControlMap.containsKey( id ) ? configControlMap.get( id ) : new AuthenticatorControl( id, 100, AuthenticationControl.SUFFICIENT, true )); + } ).sorted( Collections.reverseOrder( ) ); } - public AuthenticationResult authenticate(AuthenticationDataSource source) - throws AccountLockedException, AuthenticationException, MustChangePasswordException { - if (authenticators == null || authenticators.size() == 0) { - return (new AuthenticationResult(false, null, new AuthenticationException( - "no valid authenticators, can't authenticate"))); + + public String getId( ) + { + return "Default Authentication Manager - " + this.getClass( ).getName( ) + " : managed authenticators - " + + knownAuthenticators( ); + } + + public AuthenticationResult authenticate( AuthenticationDataSource source ) + throws AccountLockedException, AuthenticationException, MustChangePasswordException + { + List authenticators = this.authenticators.get( ); + if ( authenticators == null || authenticators.size( ) == 0 ) + { + return ( new AuthenticationResult( false, null, new AuthenticationException( + "no valid authenticators, can't authenticate" ) ) ); } // put AuthenticationResult exceptions in a map - List authnResultErrors = new ArrayList(); - for (Authenticator authenticator : authenticators) { - if (authenticator.isValid()) { - if (authenticator.supportsDataSource(source)) { - AuthenticationResult authResult = authenticator.authenticate(source); + List authnResultErrors = new ArrayList( ); + for ( Authenticator authenticator : authenticators ) + { + final AuthenticatorControl control = getControlMap( ).get( authenticator.getId( ) ); + assert control != null; + if ( authenticator.isValid( ) && control.isActive()) + { + if ( authenticator.supportsDataSource( source ) ) + { + AuthenticationResult authResult = authenticator.authenticate( source ); List authenticationFailureCauses = - authResult.getAuthenticationFailureCauses(); + authResult.getAuthenticationFailureCauses( ); - if (authResult.isAuthenticated()) { + if ( authResult.isAuthenticated( ) ) + { //olamy: as we can chain various user managers with Archiva // user manager authenticator can lock accounts in the following case : // 2 user managers: ldap and jdo. @@ -101,17 +178,24 @@ public AuthenticationResult authenticate(AuthenticationDataSource source) // now ldap bind authenticator work but loginAttemptCount has been increased. // so we restore here loginAttemptCount to 0 if in authenticationFailureCauses - for (AuthenticationFailureCause authenticationFailureCause : authenticationFailureCauses) { - User user = authenticationFailureCause.getUser(); - if (user != null) { - if (user.getCountFailedLoginAttempts() > 0) { - user.setCountFailedLoginAttempts(0); - if (!userManager.isReadOnly()) { - try { - userManager.updateUser(user); - } catch (UserManagerException e) { - log.debug(e.getMessage(), e); - log.warn("skip error updating user: {}", e.getMessage()); + for ( AuthenticationFailureCause authenticationFailureCause : authenticationFailureCauses ) + { + User user = authenticationFailureCause.getUser( ); + if ( user != null ) + { + if ( user.getCountFailedLoginAttempts( ) > 0 ) + { + user.setCountFailedLoginAttempts( 0 ); + if ( !userManager.isReadOnly( ) ) + { + try + { + userManager.updateUser( user ); + } + catch ( UserManagerException e ) + { + log.debug( e.getMessage( ), e ); + log.warn( "skip error updating user: {}", e.getMessage( ) ); } } } @@ -120,38 +204,72 @@ public AuthenticationResult authenticate(AuthenticationDataSource source) return authResult; } - if (authenticationFailureCauses != null) { - authnResultErrors.addAll(authenticationFailureCauses); - } else { - if (authResult.getException() != null) { + if ( authenticationFailureCauses != null ) + { + authnResultErrors.addAll( authenticationFailureCauses ); + } + else + { + if ( authResult.getException( ) != null ) + { authnResultErrors.add( - new AuthenticationFailureCause(AuthenticationConstants.AUTHN_RUNTIME_EXCEPTION, - authResult.getException().getMessage())); + new AuthenticationFailureCause( AuthenticationConstants.AUTHN_RUNTIME_EXCEPTION, + authResult.getException( ).getMessage( ) ) ); } } } - } else { - log.warn("Invalid authenticator found: " + authenticator.getId()); + } + else + { + log.warn( "Invalid authenticator found: " + authenticator.getId( ) ); } } - return (new AuthenticationResult(false, null, new AuthenticationException( - "authentication failed on authenticators: " + knownAuthenticators()), authnResultErrors)); + return ( new AuthenticationResult( false, null, new AuthenticationException( + "authentication failed on authenticators: " + knownAuthenticators( ) ), authnResultErrors ) ); } - public List getAuthenticators() { - return authenticators; + @Override + public List getControls( ) + { + return getControlMap().values().stream().collect( Collectors.toList()); } - private String knownAuthenticators() { - StringBuilder strbuf = new StringBuilder(); + @Override + public void setControls( List controlList ) + { + setModfiedControls( controlList.stream( ).collect( Collectors.toMap( c -> c.getName( ), c -> c ) ) ); + initializeOrder( ); + } - for (Authenticator authenticator : authenticators) { - strbuf.append('(').append(authenticator.getId()).append(") "); + @Override + public void modifyControl( AuthenticatorControl control ) + { + Map myControls = getModifiedControls( ); + if (availableAuthenticators.containsKey( control.getName() )) { + myControls.put( control.getName( ), control ); + } else { + log.warn( "Cannot modify control for authenticator {}. It does not exist.", control.getName( ) ); + } + initializeOrder(); + } + + public List getAuthenticators( ) + { + return authenticators.get(); + } + + private String knownAuthenticators( ) + { + StringBuilder strbuf = new StringBuilder( ); + + for ( Authenticator authenticator : getAuthenticators() ) + { + strbuf.append( '(' ).append( authenticator.getId( ) ).append( ") " ); } - return strbuf.toString(); + return strbuf.toString( ); } } diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/BearerError.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/BearerError.java new file mode 100644 index 00000000..6bcecb91 --- /dev/null +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/BearerError.java @@ -0,0 +1,52 @@ +package org.apache.archiva.redback.authentication.jwt; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +/** + * @author Martin Stockhammer + */ +public enum BearerError +{ + INVALID_REQUEST("invalid_request",1024),INVALID_TOKEN("invalid_token", 1025), INSUFFICIENT_SCOPE( "insufficient_scope", 1026 ); + + protected String errorString; + protected int id; + BearerError(String errorString, int id) { + this.errorString = errorString; + this.id = id; + } + + public String getError() { + return this.errorString; + } + + public int getId() { + return this.id; + } + + public static BearerError get(int id) { + final BearerError[] values = BearerError.values(); + for ( int i = 0 ; i parseToken( String token) throws JwtException { * @return the subject of the JWT * @throws AuthenticationException if the verification failed */ - public String verify( String token ) throws AuthenticationException + public String verify( String token ) throws TokenAuthenticationException { try { @@ -709,14 +717,32 @@ public String verify( String token ) throws AuthenticationException String subject = signature.getBody( ).getSubject( ); if ( StringUtils.isEmpty( subject ) ) { - throw new AuthenticationException( "Subject in JWT is empty" ); + throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "contains no subject" ); } return subject; } - catch ( JwtException e ) + catch ( ExpiredJwtException e ) { - throw new AuthenticationException( e.getMessage( ), e ); + throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "token expired" ); } + catch ( SignatureException e ) { + throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "token signature does not match" ); + } + catch ( UnsupportedJwtException e) { + throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "jwt is unsupported" ); + } + catch ( MalformedJwtException e) + { + throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "malformed token content" ); + } + catch (JwtKeyIdNotFoundException e) { + throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "signer key does not exist" ); + } + catch ( JwtException e) { + log.debug( "Unknown JwtException {}, {}", e.getClass( ), e.getMessage( ) ); + throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "unknown error " + e.getMessage( ) ); + } + } /** diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtKeyIdNotFoundException.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtKeyIdNotFoundException.java new file mode 100644 index 00000000..a0b4e420 --- /dev/null +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtKeyIdNotFoundException.java @@ -0,0 +1,38 @@ +package org.apache.archiva.redback.authentication.jwt; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import io.jsonwebtoken.JwtException; + +/** + * Exception is thrown, when a key with the given id is not in the current list + * @author Martin Stockhammer + */ +public class JwtKeyIdNotFoundException extends JwtException +{ + public JwtKeyIdNotFoundException( String message ) + { + super( message ); + } + + public JwtKeyIdNotFoundException( String message, Throwable cause ) + { + super( message, cause ); + } +} diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/TokenAuthenticationException.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/TokenAuthenticationException.java new file mode 100644 index 00000000..9dd2b5d8 --- /dev/null +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/TokenAuthenticationException.java @@ -0,0 +1,61 @@ +package org.apache.archiva.redback.authentication.jwt; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +/** + * @author Martin Stockhammer + */ +public class TokenAuthenticationException extends Exception +{ + final private BearerError error; + + public TokenAuthenticationException( BearerError error) + { + this.error = error; + } + + public TokenAuthenticationException( BearerError error, String message ) + { + super( message ); + this.error = error; + } + + public TokenAuthenticationException( BearerError error, String message, Throwable cause ) + { + super( message, cause ); + this.error = error; + } + + public TokenAuthenticationException( BearerError error, Throwable cause ) + { + super( cause ); + this.error = error; + } + + public TokenAuthenticationException( BearerError error, String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace ) + { + super( message, cause, enableSuppression, writableStackTrace ); + this.error = error; + } + + public BearerError getError( ) + { + return error; + } +} diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java index f7b16c4d..6acf4a0a 100644 --- a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java @@ -21,22 +21,18 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; -import org.apache.archiva.components.registry.Registry; import org.apache.archiva.components.registry.RegistryException; import org.apache.archiva.components.registry.commons.CommonsConfigurationRegistry; import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.authentication.AuthenticationResult; +import org.apache.archiva.redback.authentication.BearerTokenAuthenticationDataSource; import org.apache.archiva.redback.authentication.PasswordBasedAuthenticationDataSource; import org.apache.archiva.redback.authentication.Token; import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource; import org.apache.archiva.redback.configuration.DefaultUserConfiguration; -import org.apache.archiva.redback.configuration.UserConfiguration; import org.apache.archiva.redback.configuration.UserConfigurationException; import org.apache.commons.configuration2.BaseConfiguration; -import org.apache.commons.configuration2.Configuration; -import org.apache.commons.configuration2.builder.BasicConfigurationBuilder; import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -56,7 +52,7 @@ public abstract class AbstractJwtTest protected CommonsConfigurationRegistry registry; protected BaseConfiguration saveConfig; - protected void init( Map parameters) throws UserConfigurationException, RegistryException + protected void init( Map parameters) throws UserConfigurationException, RegistryException, AuthenticationException { this.registry = new CommonsConfigurationRegistry( ); String baseDir = System.getProperty( "basedir", "" ); @@ -95,7 +91,8 @@ void getId( ) @Test void supportsDataSource( ) { - assertTrue( jwtAuthenticator.supportsDataSource( new TokenBasedAuthenticationDataSource( ) ) ); + assertTrue( jwtAuthenticator.supportsDataSource( new BearerTokenAuthenticationDataSource( ) ) ); + assertFalse( jwtAuthenticator.supportsDataSource( new TokenBasedAuthenticationDataSource( ) ) ); assertFalse( jwtAuthenticator.supportsDataSource( new PasswordBasedAuthenticationDataSource( ) ) ); } @@ -151,7 +148,7 @@ void renewSigningKey( ) } @Test - void verify( ) throws AuthenticationException + void verify( ) throws TokenAuthenticationException { Token token = jwtAuthenticator.generateToken( "frodo_baggins" ); assertEquals( "frodo_baggins", jwtAuthenticator.verify( token.getData( ) ) ); @@ -183,12 +180,12 @@ void getCurrentKeyListSize( ) } @Test - void invalidKeySignature() throws AuthenticationException + void invalidKeySignature() throws TokenAuthenticationException { Token token = jwtAuthenticator.generateToken( "samwise_gamgee" ); assertEquals( "samwise_gamgee", jwtAuthenticator.verify( token.getData( ) ) ); jwtAuthenticator.revokeSigningKeys( ); - assertThrows( AuthenticationException.class, ( ) -> { + assertThrows( TokenAuthenticationException.class, ( ) -> { jwtAuthenticator.verify( token.getData( ) ); } ); } @@ -202,7 +199,7 @@ void invalidKeyDate( ) { jwtAuthenticator.setTokenLifetime( Duration.ofNanos( 0 ) ); Token token = jwtAuthenticator.generateToken( "samwise_gamgee" ); - assertThrows( AuthenticationException.class, ( ) -> { + assertThrows( TokenAuthenticationException.class, ( ) -> { jwtAuthenticator.verify( token.getData( ) ); } ); } finally @@ -216,9 +213,8 @@ void invalidKeyDate( ) void validAuthenticate() throws AuthenticationException { Token token = jwtAuthenticator.generateToken( "bilbo_baggins" ); - TokenBasedAuthenticationDataSource source = new TokenBasedAuthenticationDataSource( ); - source.setPrincipal( "bilbo_baggins" ); - source.setToken( token.getData() ); + BearerTokenAuthenticationDataSource source = new BearerTokenAuthenticationDataSource( ); + source.setTokenData( token.getData() ); AuthenticationResult result = jwtAuthenticator.authenticate( source ); assertNotNull( result ); assertTrue( result.isAuthenticated( ) ); @@ -228,13 +224,11 @@ void validAuthenticate() throws AuthenticationException @Test void invalidAuthenticate() throws AuthenticationException { - TokenBasedAuthenticationDataSource source = new TokenBasedAuthenticationDataSource( ); - source.setPrincipal( "bilbo_baggins" ); - source.setToken( "invalidToken" ); + BearerTokenAuthenticationDataSource source = new BearerTokenAuthenticationDataSource( ); + source.setTokenData( "invalidToken" ); AuthenticationResult result = jwtAuthenticator.authenticate( source ); assertNotNull( result ); assertFalse( result.isAuthenticated( ) ); - assertEquals( "bilbo_baggins", result.getPrincipal( ) ); } diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedPublicKeyTest.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedPublicKeyTest.java index 63a0d853..9087d7a7 100644 --- a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedPublicKeyTest.java +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedPublicKeyTest.java @@ -19,6 +19,7 @@ */ import org.apache.archiva.components.registry.RegistryException; +import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.configuration.UserConfigurationException; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; @@ -46,7 +47,7 @@ class JwtAuthenticatorFilebasedPublicKeyTest extends AbstractJwtTest { @BeforeEach - void init() throws RegistryException, UserConfigurationException + void init() throws RegistryException, UserConfigurationException, AuthenticationException { Map params = new HashMap<>(); params.put( AUTHENTICATION_JWT_KEYSTORETYPE, AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE ); diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedTest.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedTest.java index ecbce686..47a2b065 100644 --- a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedTest.java +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedTest.java @@ -19,6 +19,7 @@ */ import org.apache.archiva.components.registry.RegistryException; +import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.configuration.UserConfigurationException; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; @@ -48,7 +49,7 @@ class JwtAuthenticatorFilebasedTest extends AbstractJwtTest { @BeforeEach - void init() throws RegistryException, UserConfigurationException + void init() throws RegistryException, UserConfigurationException, AuthenticationException { Map params = new HashMap<>(); params.put( AUTHENTICATION_JWT_KEYSTORETYPE, AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE ); diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorMemorybasedTest.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorMemorybasedTest.java index fa876f74..4b055853 100644 --- a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorMemorybasedTest.java +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorMemorybasedTest.java @@ -19,6 +19,7 @@ */ import org.apache.archiva.components.registry.RegistryException; +import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.configuration.UserConfigurationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; @@ -40,7 +41,7 @@ class JwtAuthenticatorMemorybasedTest extends AbstractJwtTest { @BeforeEach - void init() throws RegistryException, UserConfigurationException + void init() throws RegistryException, UserConfigurationException, AuthenticationException { Map params = new HashMap<>(); params.put( AUTHENTICATION_JWT_KEYSTORETYPE, AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY ); diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/Token.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/Token.java index 2baaa438..8bec5e07 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/Token.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/Token.java @@ -22,6 +22,8 @@ import javax.xml.bind.annotation.XmlRootElement; import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; import java.util.Date; /** @@ -33,8 +35,8 @@ public class Token { String key; - Instant created; - Instant expires; + OffsetDateTime created; + OffsetDateTime expires; String principal; String purpose; @@ -45,8 +47,8 @@ public Token( ) public static Token of( AuthenticationKey key ) { Token token = new Token( ); token.setKey( key.getKey() ); - token.setCreated( key.getDateCreated().toInstant() ); - token.setExpires( key.getDateExpires().toInstant() ); + token.setCreatedFromInstant( key.getDateCreated().toInstant() ); + token.setExpiresFromInstant( key.getDateExpires().toInstant() ); token.setPrincipal( key.getForPrincipal() ); token.setPurpose( key.getPurpose() ); return token; @@ -56,8 +58,8 @@ public static Token of( String key, Date created, Date expires, String principal { Token token = new Token( ); token.setKey( key ); - token.setCreated( created.toInstant( ) ); - token.setExpires( expires.toInstant( ) ); + token.setCreatedFromInstant( created.toInstant( ) ); + token.setExpiresFromInstant( expires.toInstant( ) ); token.setPrincipal( principal ); token.setPrincipal( purpose ); return token; @@ -67,8 +69,8 @@ public static Token of( String key, Instant created, Instant expires, String pri { Token token = new Token( ); token.setKey( key ); - token.setCreated( created ); - token.setExpires( expires ); + token.setCreatedFromInstant( created ); + token.setExpiresFromInstant( expires ); token.setPrincipal( principal ); token.setPrincipal( purpose ); return token; @@ -84,22 +86,31 @@ public void setKey( String key ) this.key = key; } - public Instant getCreated( ) + public OffsetDateTime getCreated( ) { return created; } - public void setCreated( Instant created ) + public void setCreatedFromInstant( Instant created ) + { + this.created = OffsetDateTime.ofInstant( created, ZoneId.of( "UTC" ) ); + } + + public void setCreated( OffsetDateTime created ) { this.created = created; } - - public Instant getExpires( ) + public OffsetDateTime getExpires( ) { return expires; } - public void setExpires( Instant expires ) + public void setExpiresFromInstant( Instant expires ) + { + this.expires = OffsetDateTime.ofInstant( expires, ZoneId.of( "UTC" ) ); + } + + public void setExpires( OffsetDateTime expires ) { this.expires = expires; } diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/RedbackServiceException.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/RedbackServiceException.java index 6c99322b..d8bc4fd0 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/RedbackServiceException.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/RedbackServiceException.java @@ -50,9 +50,9 @@ public RedbackServiceException( ErrorMessage errorMessage ) errorMessages.add( errorMessage ); } - public RedbackServiceException( ErrorMessage errorMessage, int httpErrorCode ) + public RedbackServiceException( ErrorMessage errorMessage, int httpResponseCode ) { - this.httpErrorCode = httpErrorCode; + this.httpErrorCode = httpResponseCode; errorMessages.add( errorMessage ); } @@ -61,6 +61,12 @@ public RedbackServiceException( List errorMessage ) errorMessages.addAll( errorMessage ); } + public RedbackServiceException( List errorMessage, int httpResponseCode ) + { + this.httpErrorCode = httpResponseCode; + errorMessages.addAll( errorMessage ); + } + public int getHttpErrorCode() { return httpErrorCode; diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java index c38b5a9a..88c8f4d4 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java @@ -19,17 +19,14 @@ * under the License. */ -import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.apache.archiva.redback.authorization.RedbackAuthorization; import org.apache.archiva.redback.rest.api.model.ActionStatus; -import org.apache.archiva.redback.rest.api.model.AuthenticationKeyResult; import org.apache.archiva.redback.rest.api.model.LoginRequest; import org.apache.archiva.redback.rest.api.model.PingResult; import org.apache.archiva.redback.rest.api.model.Token; import org.apache.archiva.redback.rest.api.model.User; -import org.apache.archiva.redback.rest.api.model.UserLogin; import org.apache.archiva.redback.rest.api.services.RedbackServiceException; import javax.ws.rs.GET; diff --git a/redback-integrations/redback-rest/redback-rest-services/pom.xml b/redback-integrations/redback-rest/redback-rest-services/pom.xml index ee4a0b8c..9d67f5b7 100644 --- a/redback-integrations/redback-rest/redback-rest-services/pom.xml +++ b/redback-integrations/redback-rest/redback-rest-services/pom.xml @@ -94,15 +94,24 @@ org.apache.archiva.redback redback-system
+ + org.apache.archiva.redback + redback-authentication-jwt + org.apache.archiva.components.cache archiva-components-spring-cache-api - - jakarta.servlet - jakarta.servlet-api + org.apache.archiva.redback + redback-common-integrations + + org.apache.archiva.redback + redback-rest-api + + + org.springframework @@ -113,25 +122,47 @@ ehcache + + jakarta.servlet + jakarta.servlet-api + + + jakarta.persistence + jakarta.persistence-api + + + jakarta.transaction + jakarta.transaction-api + + + jakarta.xml.bind + jakarta.xml.bind-api + + + jakarta.inject + jakarta.inject-api + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.mail + jakarta.mail-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + com.sun.mail + jakarta.mail + org.apache.commons commons-lang3 - - - org.apache.archiva.redback - redback-rest-api - ${project.version} - - - - - - org.apache.archiva.redback - redback-common-integrations - - commons-io commons-io @@ -148,6 +179,7 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + compile org.apache.cxf @@ -177,7 +209,6 @@ com.fasterxml.jackson.dataformat jackson-dataformat-xml - ${jackson.version} @@ -192,6 +223,11 @@ runtime + + org.apache.archiva.redback + redback-users-memory + test + org.apache.archiva.redback redback-rbac-jpa @@ -305,39 +341,19 @@ test + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + - - jakarta.persistence - jakarta.persistence-api - - - jakarta.transaction - jakarta.transaction-api - - - jakarta.xml.bind - jakarta.xml.bind-api - - - jakarta.inject - jakarta.inject-api - - - jakarta.annotation - jakarta.annotation-api - - - jakarta.mail - jakarta.mail-api - - - jakarta.ws.rs - jakarta.ws.rs-api - - - com.sun.mail - jakarta.mail - diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/RedbackRequestInformation.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/RedbackRequestInformation.java index 60bd5deb..1503a7ea 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/RedbackRequestInformation.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/RedbackRequestInformation.java @@ -18,14 +18,19 @@ * under the License. */ +import org.apache.archiva.redback.system.SecuritySession; import org.apache.archiva.redback.users.User; /** * @author Olivier Lamy + * @author Martin Stockhammer * @since 1.4 + * */ public class RedbackRequestInformation { + private SecuritySession securitySession; + private User user; private String remoteAddr; @@ -36,6 +41,13 @@ public RedbackRequestInformation( User user, String remoteAddr ) this.remoteAddr = remoteAddr; } + public RedbackRequestInformation( SecuritySession securitySession, User user, String remoteAddr ) + { + this.securitySession = securitySession; + this.user = user; + this.remoteAddr = remoteAddr; + } + public User getUser() { return user; @@ -55,4 +67,14 @@ public void setRemoteAddr( String remoteAddr ) { this.remoteAddr = remoteAddr; } + + public SecuritySession getSecuritySession( ) + { + return securitySession; + } + + public void setSecuritySession( SecuritySession securitySession ) + { + this.securitySession = securitySession; + } } diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/AbstractInterceptor.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/AbstractInterceptor.java index bc9aac4a..b8a220f4 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/AbstractInterceptor.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/AbstractInterceptor.java @@ -25,6 +25,9 @@ import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticator; import org.apache.archiva.redback.policy.AccountLockedException; import org.apache.archiva.redback.policy.MustChangePasswordException; +import org.apache.archiva.redback.rest.services.RedbackAuthenticationThreadLocal; +import org.apache.archiva.redback.rest.services.RedbackRequestInformation; +import org.apache.archiva.redback.system.SecuritySession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotationUtils; @@ -50,6 +53,7 @@ public abstract class AbstractInterceptor private Map authorizationCache = new HashMap<>( ); public static final String AUTHENTICATION_RESULT = "org.apache.archiva.authResult"; + public static final String SECURITY_SESSION = "org.apache.archiva.securitySession"; @Context private HttpServletRequest httpServletRequest; @@ -67,6 +71,15 @@ public HttpServletResponse getHttpServletResponse( ) return httpServletResponse; } + protected void setHttpServletRequest(HttpServletRequest request) { + this.httpServletRequest = request; + } + + protected void setHttpServletResponse(HttpServletResponse response) { + this.httpServletResponse = response; + } + + public RedbackAuthorization getRedbackAuthorization( ResourceInfo resourceInfo ) { Method method = resourceInfo.getResourceMethod( ); RedbackAuthorization redbackAuthorization = getAuthorizationForMethod( method ); @@ -87,6 +100,21 @@ private RedbackAuthorization getAuthorizationForMethod(Method method) { } } + protected SecuritySession getSecuritySession(ContainerRequestContext containerRequestContext, HttpAuthenticator httpAuthenticator, + HttpServletRequest request) { + if ( containerRequestContext.getProperty( SECURITY_SESSION ) != null ) { + return (SecuritySession) containerRequestContext.getProperty( SECURITY_SESSION ); + } + RedbackRequestInformation info = RedbackAuthenticationThreadLocal.get( ); + SecuritySession securitySession = info == null ? null : info.getSecuritySession( ); + if (securitySession!=null) { + return securitySession; + } else + { + return httpAuthenticator.getSecuritySession( request.getSession( true ) ); + } + } + protected AuthenticationResult getAuthenticationResult( ContainerRequestContext containerRequestContext, HttpAuthenticator httpAuthenticator, HttpServletRequest request ) { AuthenticationResult authenticationResult = null; diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/BearerAuthInterceptor.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/BearerAuthInterceptor.java new file mode 100644 index 00000000..71d77ec9 --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/BearerAuthInterceptor.java @@ -0,0 +1,231 @@ +package org.apache.archiva.redback.rest.services.interceptors; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import org.apache.archiva.redback.authentication.AuthenticationException; +import org.apache.archiva.redback.authentication.AuthenticationFailureCause; +import org.apache.archiva.redback.authentication.AuthenticationResult; +import org.apache.archiva.redback.authentication.BearerTokenAuthenticationDataSource; +import org.apache.archiva.redback.authentication.jwt.BearerError; +import org.apache.archiva.redback.authentication.jwt.JwtAuthenticator; +import org.apache.archiva.redback.authorization.RedbackAuthorization; +import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticationException; +import org.apache.archiva.redback.policy.AccountLockedException; +import org.apache.archiva.redback.policy.MustChangePasswordException; +import org.apache.archiva.redback.rest.services.RedbackAuthenticationThreadLocal; +import org.apache.archiva.redback.rest.services.RedbackRequestInformation; +import org.apache.archiva.redback.system.SecuritySession; +import org.apache.archiva.redback.system.SecuritySystem; +import org.apache.archiva.redback.users.User; +import org.apache.archiva.redback.users.UserManager; +import org.apache.archiva.redback.users.UserManagerException; +import org.apache.archiva.redback.users.UserNotFoundException; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.util.regex.Pattern; + +/** + * Interceptor that checks for the Bearer Header value and tries to verify the token. + * + * @author Martin Stockhammer + * @since 3.0 + */ +@Service( "bearerAuthInterceptor#rest" ) +@Provider +@Priority( Priorities.AUTHENTICATION ) +public class BearerAuthInterceptor extends AbstractInterceptor + implements ContainerRequestFilter +{ + + private static final Logger log = LoggerFactory.getLogger( BearerAuthInterceptor.class ); + + @Inject + @Named( value = "userManager#default" ) + private UserManager userManager; + + @Inject + @Named( value = "securitySystem" ) + SecuritySystem securitySystem; + + @Inject + JwtAuthenticator jwtAuthenticator; + + @Context + private ResourceInfo resourceInfo; + + protected void setUserManager( UserManager userManager ) + { + this.userManager = userManager; + } + + protected void setJwtAuthenticator( JwtAuthenticator jwtAuthenticator ) + { + this.jwtAuthenticator = jwtAuthenticator; + } + + protected void setResourceInfo( ResourceInfo resourceInfo ) + { + this.resourceInfo = resourceInfo; + } + + @Override + public void filter( ContainerRequestContext requestContext ) throws IOException + { + log.debug( "Intercepting request for bearer token" ); + // 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 ", + 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 + requestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build( ) ); + return; + } + String bearerHeader = StringUtils.defaultIfEmpty( requestContext.getHeaderString( "Authorization" ), "" ).trim( ); + if ( !"".equals( bearerHeader ) ) + { + log.debug( "Found token" ); + String bearerToken = bearerHeader.replaceFirst( "\\s*Bearer\\s+(\\S+)\\s*", "$1" ); + final HttpServletRequest request = getHttpServletRequest( ); + BearerTokenAuthenticationDataSource source = new BearerTokenAuthenticationDataSource( "", bearerToken ); + + if ( redbackAuthorization.noRestriction( ) ) + { + log.debug( "No restriction for method {}#{}", resourceInfo.getResourceClass( ), resourceInfo.getResourceMethod( ) ); + // maybe session exists so put it in threadLocal + // some services need the current user if logged + // maybe there is some authz in the request so try it but not fail so catch Exception ! + try + { + SecuritySession securitySession = securitySystem.authenticate( source ); + AuthenticationResult authenticationResult = securitySession.getAuthenticationResult( ); + + if ( ( authenticationResult == null ) || ( !authenticationResult.isAuthenticated( ) ) ) + { + return; + } + + User user = authenticationResult.getUser( ) == null ? userManager.findUser( + authenticationResult.getPrincipal( ) ) : authenticationResult.getUser( ); + RedbackRequestInformation redbackRequestInformation = + new RedbackRequestInformation( securitySession, user, request.getRemoteAddr( ) ); + + RedbackAuthenticationThreadLocal.set( redbackRequestInformation ); + // message.put( AuthenticationResult.class, authenticationResult ); + requestContext.setProperty( AUTHENTICATION_RESULT, authenticationResult ); + requestContext.setProperty( SECURITY_SESSION, securitySession ); + } + catch ( Exception e ) + { + log.debug( "Authentication failed {}", e.getMessage( ), e ); + // ignore here + } + return; + } + HttpServletResponse response = getHttpServletResponse( ); + try + { + SecuritySession securitySession = securitySystem.authenticate( source ); + AuthenticationResult authenticationResult = securitySession.getAuthenticationResult( ); + + if ( ( authenticationResult == null ) || ( !authenticationResult.isAuthenticated( ) ) ) + { + String error; + String message; + if ( authenticationResult.getAuthenticationFailureCauses( ).size( ) > 0 ) + { + AuthenticationFailureCause cause = authenticationResult.getAuthenticationFailureCauses( ).get( 0 ); + error = BearerError.get( cause.getCause( ) ).getError( ); + message = cause.getMessage( ); + } + else + { + error = "invalid_token"; + message = "Unknown error"; + } + response.setHeader( "WWW-Authenticate", "Bearer realm=\"" + request.getRemoteHost( ) + "\",error=\"" + + error + "\",error_description=\"" + message + "\"" ); + requestContext.abortWith( Response.status( Response.Status.UNAUTHORIZED ).build( ) ); + return; + } + + User user = authenticationResult.getUser( ) == null + ? userManager.findUser( authenticationResult.getPrincipal( ) ) + : authenticationResult.getUser( ); + + RedbackRequestInformation redbackRequestInformation = + new RedbackRequestInformation( user, request.getRemoteAddr( ) ); + redbackRequestInformation.setSecuritySession( securitySession ); + RedbackAuthenticationThreadLocal.set( redbackRequestInformation ); + // message.put( AuthenticationResult.class, authenticationResult ); + requestContext.setProperty( AUTHENTICATION_RESULT, authenticationResult ); + requestContext.setProperty( SECURITY_SESSION, securitySession ); + return; + } + catch ( AuthenticationException e ) + { + response.setHeader( "WWW-Authenticate", "Bearer realm=\"" + request.getRemoteHost( ) + + "\",error=\"invalid_token\",error_description=\"" + e.getMessage( ) + "\"" ); + requestContext.abortWith( Response.status( Response.Status.UNAUTHORIZED ).build( ) ); + } + catch ( UserNotFoundException e ) + { + response.setHeader( "WWW-Authenticate", "Bearer realm=\"" + request.getRemoteHost( ) + + "\",error=\"invalid_token\",error_description=\"user not found\"" ); + requestContext.abortWith( Response.status( Response.Status.UNAUTHORIZED ).build( ) ); + } + catch ( UserManagerException e ) + { + log.error( "Error from user manager " + e.getMessage( ) ); + requestContext.abortWith( Response.status( Response.Status.INTERNAL_SERVER_ERROR ).build( ) ); + } + catch ( AccountLockedException e ) + { + response.setHeader( "WWW-Authenticate", "Bearer realm=\"" + request.getRemoteHost( ) + + "\",error=\"invalid_token\",error_description=\"account locked\"" ); + requestContext.abortWith( Response.status( Response.Status.UNAUTHORIZED ).build( ) ); + } + catch ( MustChangePasswordException e ) + { + response.setHeader( "WWW-Authenticate", "Bearer realm=\"" + request.getRemoteHost( ) + + "\",error=\"invalid_token\",error_description=\"password change required\"" ); + requestContext.abortWith( Response.status( Response.Status.UNAUTHORIZED ).build( ) ); + } + + + } + } +} diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/JacksonJsonConfigurator.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/JacksonJsonConfigurator.java index 2551c5a6..b5d54a88 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/JacksonJsonConfigurator.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/JacksonJsonConfigurator.java @@ -50,6 +50,7 @@ public JacksonJsonConfigurator( @Named("redbackJacksonJsonMapper") ObjectMapper log.info( "configure jackson ObjectMapper" ); objectMapper.disable( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES ); objectMapper.setAnnotationIntrospector( new JaxbAnnotationIntrospector( objectMapper.getTypeFactory() ) ); + objectMapper.findAndRegisterModules( ); objectMapper.registerModule( new JavaTimeModule( ) ); objectMapper.setDateFormat( new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" ) ); diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/PermissionsInterceptor.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/PermissionsInterceptor.java index 2461f594..64d11d08 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/PermissionsInterceptor.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/PermissionsInterceptor.java @@ -24,13 +24,17 @@ import org.apache.archiva.redback.authorization.AuthorizationResult; import org.apache.archiva.redback.authorization.RedbackAuthorization; import org.apache.archiva.redback.integration.filter.authentication.basic.HttpBasicAuthentication; +import org.apache.archiva.redback.rest.services.RedbackAuthenticationThreadLocal; +import org.apache.archiva.redback.rest.services.RedbackRequestInformation; import org.apache.archiva.redback.system.SecuritySession; import org.apache.archiva.redback.system.SecuritySystem; +import org.apache.archiva.redback.users.User; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import javax.annotation.Priority; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; @@ -48,6 +52,7 @@ */ @Service( "permissionInterceptor#rest" ) @Provider +@Priority( Priorities.AUTHORIZATION ) public class PermissionsInterceptor extends AbstractInterceptor implements ContainerRequestFilter @@ -68,7 +73,7 @@ public class PermissionsInterceptor public void filter( ContainerRequestContext containerRequestContext ) { - + log.debug( "Filtering request" ); RedbackAuthorization redbackAuthorization = getRedbackAuthorization( resourceInfo ); if ( redbackAuthorization != null ) @@ -85,14 +90,14 @@ public void filter( ContainerRequestContext containerRequestContext ) && !( permissions.length == 1 && StringUtils.isEmpty( permissions[0] ) ) ) { HttpServletRequest request = getHttpServletRequest( ); - SecuritySession securitySession = httpAuthenticator.getSecuritySession( request.getSession() ); + SecuritySession securitySession = getSecuritySession( containerRequestContext, httpAuthenticator, request ); AuthenticationResult authenticationResult = getAuthenticationResult( containerRequestContext, httpAuthenticator, request ); - log.debug( "authenticationResult from message: {}", authenticationResult ); if ( authenticationResult != null && authenticationResult.isAuthenticated() ) { + User userObject = securitySession == null ? authenticationResult.getUser( ) : securitySession.getUser( ); for ( String permission : permissions ) { log.debug( "check permission: {} with securitySession {}", permission, securitySession ); @@ -108,10 +113,13 @@ public void filter( ContainerRequestContext containerRequestContext ) log.debug("Found resource from annotated parameter: {}",resource); } - AuthorizationResult authorizationResult = - securitySystem.authorize( authenticationResult.getUser(), permission, // - StringUtils.isBlank( resource ) // - ? null : resource ); + AuthorizationResult authorizationResult = null; + if (userObject!=null) + { + authorizationResult = securitySystem.authorize( userObject, permission, // + StringUtils.isBlank( resource ) // + ? null : resource ); + } if ( authenticationResult != null && authorizationResult.isAuthorized() ) { log.debug( "isAuthorized for permission {}", permission ); diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/Priorities.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/Priorities.java new file mode 100644 index 00000000..71e9fcce --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/Priorities.java @@ -0,0 +1,30 @@ +package org.apache.archiva.redback.rest.services.interceptors; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +/** + * @author Martin Stockhammer + */ +public final class Priorities +{ + public static final int INITIALIZE = 100; + public static final int PRECHECK = 1000; + public static final int AUTHENTICATION = 2000; + public static final int AUTHORIZATION = 3000; +} diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java index da2a44dc..c134d172 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java @@ -34,6 +34,7 @@ import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; +import javax.annotation.Priority; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; @@ -64,6 +65,7 @@ */ @Provider @Service( "requestValidationInterceptor#rest" ) +@Priority( Priorities.PRECHECK ) public class RequestValidationInterceptor extends AbstractInterceptor implements ContainerRequestFilter diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/ThreadLocalUserCleaner.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/ThreadLocalUserCleaner.java index dbce28f6..f43c5459 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/ThreadLocalUserCleaner.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/ThreadLocalUserCleaner.java @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import javax.annotation.Priority; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; @@ -39,6 +40,7 @@ @Service( "threadLocalUserCleaner#rest" ) @Provider @PreMatching +@Priority( Priorities.INITIALIZE ) public class ThreadLocalUserCleaner implements ContainerResponseFilter { diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java index 9537c3c3..c7c2af3d 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java @@ -22,8 +22,8 @@ import org.apache.archiva.redback.authentication.AuthenticationConstants; import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.authentication.AuthenticationFailureCause; -import org.apache.archiva.redback.authentication.EncryptionFailedException; import org.apache.archiva.redback.authentication.PasswordBasedAuthenticationDataSource; +import org.apache.archiva.redback.authentication.jwt.JwtAuthenticator; import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticator; import org.apache.archiva.redback.keys.AuthenticationKey; import org.apache.archiva.redback.keys.KeyManager; @@ -52,12 +52,12 @@ import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import java.time.Duration; import java.time.Instant; -import java.time.temporal.TemporalAmount; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -87,6 +87,12 @@ public class DefaultAuthenticationService @Context private HttpServletRequest httpServletRequest; + @Context + private HttpServletResponse response; + + @Inject + private JwtAuthenticator jwtAuthenticator; + // validation token lifetime: 3 hours long tokenLifetime = 1000*3600*3; @@ -101,7 +107,6 @@ public DefaultAuthenticationService( SecuritySystem securitySystem, @Override public Token requestOnetimeToken( String providedKey, String principal, String purpose, int expirationSeconds ) - throws RedbackServiceException { KeyManager keyManager = securitySystem.getKeyManager(); AuthenticationKey key; @@ -133,18 +138,20 @@ public Token requestOnetimeToken( String providedKey, String principal, String p @Override public PingResult ping() - throws RedbackServiceException { return new PingResult( true); } @Override public PingResult pingWithAutz() - throws RedbackServiceException { return new PingResult( true ); } + private Token getRestToken( org.apache.archiva.redback.authentication.Token token ) { + return Token.of( token.getData( ), token.getMetadata( ).created( ), token.getMetadata( ).validBefore( ), token.getMetadata( ).getUser( ), "rest-auth" ); + } + @Override public Token logIn( LoginRequest loginRequest ) throws RedbackServiceException @@ -157,32 +164,22 @@ public Token logIn( LoginRequest loginRequest ) { SecuritySession securitySession = securitySystem.authenticate( authDataSource ); log.debug("Security session {}", securitySession); - if ( securitySession.getAuthenticationResult().isAuthenticated() ) + if ( securitySession.getAuthenticationResult() != null + && securitySession.getAuthenticationResult().isAuthenticated() ) { org.apache.archiva.redback.users.User user = securitySession.getUser(); - log.debug("user {} authenticated", user.getUsername()); + org.apache.archiva.redback.authentication.Token token = jwtAuthenticator.generateToken( user.getUsername( ) ); + log.debug("User {} authenticated", user.getUsername()); if ( !user.isValidated() ) { log.info( "user {} not validated", user.getUsername() ); - return null; + throw new RedbackServiceException( "redback:user-not-validated", Response.Status.FORBIDDEN.getStatusCode() ); } - UserLogin restUser = buildRestUser( user ); - restUser.setReadOnly( securitySystem.userManagerReadOnly() ); - // validationToken only set during login - try { - String validationToken = securitySystem.getTokenManager().encryptToken(user.getUsername(), tokenLifetime); - restUser.setValidationToken(validationToken); - log.debug("Validation Token set {}",validationToken); - - } catch (EncryptionFailedException e) { - log.error("Validation token could not be created "+e.getMessage()); - } - - // here create an http session - httpAuthenticator.authenticate( authDataSource, httpServletRequest.getSession( true ) ); - return null; - } - if ( securitySession.getAuthenticationResult() != null + // Stateless services no session + // httpAuthenticator.authenticate( authDataSource, httpServletRequest.getSession( true ) ); + Token restToken = getRestToken( token ); + return restToken; + } else if ( securitySession.getAuthenticationResult() != null && securitySession.getAuthenticationResult().getAuthenticationFailureCauses() != null ) { List errorMessages = new ArrayList(); @@ -190,33 +187,42 @@ public Token logIn( LoginRequest loginRequest ) { if ( authenticationFailureCause.getCause() == AuthenticationConstants.AUTHN_NO_SUCH_USER ) { - errorMessages.add( new ErrorMessage( "incorrect.username.password" ) ); + errorMessages.add( new ErrorMessage( "redback:incorrect.username.password" ) ); } else { - errorMessages.add( new ErrorMessage().message( authenticationFailureCause.getMessage() ) ); + errorMessages.add( new ErrorMessage().message( "redback:"+authenticationFailureCause.getMessage() ) ); } } - - throw new RedbackServiceException( errorMessages ); + response.setHeader( "WWW-Authenticate", "redback-login realm="+httpServletRequest.getRemoteHost() ); + throw new RedbackServiceException( errorMessages , Response.Status.UNAUTHORIZED.getStatusCode()); } - return null; + response.setHeader( "WWW-Authenticate", "redback-login realm="+httpServletRequest.getRemoteHost() ); + throw new RedbackServiceException( "redback:login-failed", Response.Status.UNAUTHORIZED.getStatusCode() ); } + catch ( AuthenticationException e ) { - throw new RedbackServiceException( e.getMessage(), Response.Status.FORBIDDEN.getStatusCode() ); + log.debug( "Authentication error: {}", e.getMessage( ), e ); + throw new RedbackServiceException( "redback:login-failed", Response.Status.UNAUTHORIZED.getStatusCode() ); } - catch ( UserNotFoundException | AccountLockedException e ) + catch ( UserNotFoundException e ) { - throw new RedbackServiceException( e.getMessage() ); + log.debug( "User not found: {}", e.getMessage( ), e ); + throw new RedbackServiceException( "redback:login-failed", Response.Status.UNAUTHORIZED.getStatusCode() ); + } + catch (AccountLockedException e) { + log.info( "Account locked: {}", e.getMessage( ), e ); + throw new RedbackServiceException( "redback:account-locked", Response.Status.FORBIDDEN.getStatusCode() ); } catch ( MustChangePasswordException e ) { - return null; + log.debug( "Password change required: {}", e.getMessage( ), e ); + throw new RedbackServiceException( "redback:password-change-required", Response.Status.FORBIDDEN.getStatusCode( ) ); } catch ( UserManagerException e ) { - log.info( "UserManagerException: {}", e.getMessage() ); + log.warn( "UserManagerException: {}", e.getMessage() ); List errorMessages = Arrays.asList( new ErrorMessage().message( "UserManagerException: " + e.getMessage() ) ); throw new RedbackServiceException( errorMessages ); diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/resources/META-INF/spring-context.xml b/redback-integrations/redback-rest/redback-rest-services/src/main/resources/META-INF/spring-context.xml index 2fd63b1a..2b51cb53 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/resources/META-INF/spring-context.xml +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/resources/META-INF/spring-context.xml @@ -88,7 +88,7 @@ - + diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/AbstractRestServicesTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/AbstractRestServicesTest.java index fd3d8920..ff9a69f7 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/AbstractRestServicesTest.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/AbstractRestServicesTest.java @@ -19,6 +19,8 @@ * under the License. */ +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import junit.framework.TestCase; import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants; @@ -83,6 +85,14 @@ public int getServerPort() { } } + JacksonJaxbJsonProvider getJsonProvider() { + JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider( ); + ObjectMapper mapper = new ObjectMapper( ); + mapper.registerModule( new JavaTimeModule( ) ); + provider.setMapper( mapper ); + return provider; + } + /** * Returns true, if the server does exist and is running. * @return true, if server does exist and is running. @@ -173,7 +183,7 @@ protected FakeCreateAdminService getFakeCreateAdminService() { return JAXRSClientFactory.create( "http://localhost:" + getServerPort()+ "/" + getRestServicesPath() + "/fakeCreateAdminService/", - FakeCreateAdminService.class, Collections.singletonList( new JacksonJaxbJsonProvider() ) ); + FakeCreateAdminService.class, Collections.singletonList( getJsonProvider() ) ); } @After @@ -241,7 +251,7 @@ protected LoginService getLoginService( String authzHeader ) { LoginService service = JAXRSClientFactory.create( "http://localhost:" + getServerPort() + "/" + getRestServicesPath() + "/redbackServices/", - LoginService.class, Collections.singletonList( new JacksonJaxbJsonProvider() ) ); + LoginService.class, Collections.singletonList( getJsonProvider() ) ); // for debuging purpose WebClient.getConfig( service ).getHttpConduit().getClient().setReceiveTimeout( getTimeout() ); @@ -262,7 +272,7 @@ protected AuthenticationService getLoginServiceV2( String authzHeader ) { AuthenticationService service = JAXRSClientFactory.create( "http://localhost:" + getServerPort() + "/" + getRestServicesPath() + "/v2/redback/", - AuthenticationService.class, Collections.singletonList( new JacksonJaxbJsonProvider() ) ); + AuthenticationService.class, Collections.singletonList( getJsonProvider() ) ); // for debuging purpose WebClient.getConfig( service ).getHttpConduit().getClient().setReceiveTimeout( getTimeout() ); @@ -285,7 +295,7 @@ protected LdapGroupMappingService getLdapGroupMappingService( String authzHeader LdapGroupMappingService service = JAXRSClientFactory.create( "http://localhost:" + getServerPort() + "/" + getRestServicesPath() + "/redbackServices/", LdapGroupMappingService.class, - Collections.singletonList( new JacksonJaxbJsonProvider() ) ); + Collections.singletonList( getJsonProvider() ) ); // for debuging purpose WebClient.getConfig( service ).getHttpConduit().getClient().setReceiveTimeout( getTimeout() ); diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/interceptors/BearerAuthInterceptorTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/interceptors/BearerAuthInterceptorTest.java new file mode 100644 index 00000000..bc7519fc --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/interceptors/BearerAuthInterceptorTest.java @@ -0,0 +1,151 @@ +package org.apache.archiva.redback.rest.services.interceptors; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import org.apache.archiva.redback.authentication.AuthenticationException; +import org.apache.archiva.redback.authentication.Token; +import org.apache.archiva.redback.authentication.jwt.JwtAuthenticator; +import org.apache.archiva.redback.configuration.UserConfiguration; +import org.apache.archiva.redback.policy.AccountLockedException; +import org.apache.archiva.redback.policy.MustChangePasswordException; +import org.apache.archiva.redback.rest.services.RedbackAuthenticationThreadLocal; +import org.apache.archiva.redback.rest.services.RedbackRequestInformation; +import org.apache.archiva.redback.rest.services.v2.DefaultAuthenticationService; +import org.apache.archiva.redback.system.DefaultSecuritySession; +import org.apache.archiva.redback.system.SecuritySystem; +import org.apache.archiva.redback.users.User; +import org.apache.archiva.redback.users.UserManager; +import org.apache.archiva.redback.users.UserManagerException; +import org.apache.archiva.redback.users.memory.SimpleUser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ResourceInfo; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * @author Martin Stockhammer + */ +@ExtendWith( MockitoExtension.class ) +class BearerAuthInterceptorTest +{ + + @Mock + UserConfiguration userConfiguration; + + @Mock + UserManager userManager; + + @Mock + private ResourceInfo resourceInfo; + + @Mock + private HttpServletRequest httpServletRequest; + + @Mock + private HttpServletResponse httpServletResponse; + + @Mock + private SecuritySystem securitySystem; + + private JwtAuthenticator jwtAuthenticator; + + BearerAuthInterceptor interceptor; + + @BeforeEach + void setup() throws AuthenticationException, UserManagerException, AccountLockedException, MustChangePasswordException + { + // when( userConfiguration.getString( anyString( ) ) ).thenReturn( null ); + doAnswer( invocation -> invocation.getArgument( 1 ).toString() ).when( userConfiguration ).getString( anyString( ), anyString( ) ); + doAnswer( invocation -> (int)invocation.getArgument( 1 ) ).when( userConfiguration ).getInt( anyString( ), anyInt() ); + + interceptor = new BearerAuthInterceptor( ); + interceptor.setHttpServletRequest( httpServletRequest ); + interceptor.setHttpServletResponse( httpServletResponse ); + interceptor.setResourceInfo( resourceInfo ); + interceptor.setUserManager( userManager ); + interceptor.securitySystem = securitySystem; + this.jwtAuthenticator = new JwtAuthenticator( ); + jwtAuthenticator.setUserConfiguration( userConfiguration ); + jwtAuthenticator.init(); + interceptor.setJwtAuthenticator( jwtAuthenticator ); + doAnswer( invocation -> new DefaultSecuritySession( jwtAuthenticator.authenticate( invocation.getArgument( 0 ) )) ) + .when( securitySystem ).authenticate( any( ) ); + + } + + @Test + void filter() throws IOException, NoSuchMethodException, UserManagerException + { + Token token = jwtAuthenticator.generateToken( "gandalf" ); + when( resourceInfo.getResourceMethod( ) ).thenReturn( DefaultAuthenticationService.class.getDeclaredMethod( "ping" ) ); + doReturn( DefaultAuthenticationService.class ).when( resourceInfo ).getResourceClass( ); + ContainerRequestContext context = mock( ContainerRequestContext.class ); + when( context.getHeaderString( "Authorization" ) ).thenReturn( "Bearer " + token.getData( ) ); + User user = new SimpleUser( ); + user.setUsername( "gandalf" ); + when( userManager.findUser( "gandalf" ) ).thenReturn( user ); + interceptor.filter( context); + verify( context, never() ).abortWith( any() ); + RedbackRequestInformation info = RedbackAuthenticationThreadLocal.get( ); + assertNotNull( info ); + assertEquals( "gandalf", info.getUser( ).getUsername( ) ); + } + + + @Test + void filterWithInvalidToken() throws IOException, NoSuchMethodException + { + RedbackAuthenticationThreadLocal.set( null ); + Token token = jwtAuthenticator.generateToken( "gandalf" ); + when( resourceInfo.getResourceMethod( ) ).thenReturn( DefaultAuthenticationService.class.getDeclaredMethod( "pingWithAutz") ); + doReturn( DefaultAuthenticationService.class ).when( resourceInfo ).getResourceClass( ); + ContainerRequestContext context = mock( ContainerRequestContext.class ); + when( context.getHeaderString( "Authorization" ) ).thenReturn( "Bearer xxxxx" ); + interceptor.filter( context); + verify( context, times(1) ).abortWith( argThat( response -> response.getStatus() == 401 ) ); + verify( httpServletResponse, times(1) ).setHeader( eq("WWW-Authenticate"), anyString( ) ); + RedbackRequestInformation info = RedbackAuthenticationThreadLocal.get( ); + assertNull( info ); + } + + @Test + void filterWithInvalidTokenUnrestrictedMethod() throws IOException, NoSuchMethodException + { + RedbackAuthenticationThreadLocal.set( null ); + Token token = jwtAuthenticator.generateToken( "gandalf" ); + when( resourceInfo.getResourceMethod( ) ).thenReturn( DefaultAuthenticationService.class.getDeclaredMethod( "ping") ); + doReturn( DefaultAuthenticationService.class ).when( resourceInfo ).getResourceClass( ); + ContainerRequestContext context = mock( ContainerRequestContext.class ); + when( context.getHeaderString( "Authorization" ) ).thenReturn( "Bearer xxxxx" ); + interceptor.filter( context); + RedbackRequestInformation info = RedbackAuthenticationThreadLocal.get( ); + assertNull( info ); + } + +} \ No newline at end of file diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractRestServicesTestV2.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractRestServicesTestV2.java new file mode 100644 index 00000000..ccc9abe8 --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractRestServicesTestV2.java @@ -0,0 +1,332 @@ +package org.apache.archiva.redback.rest.services.v2; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import org.apache.archiva.redback.authentication.Token; +import org.apache.archiva.redback.authentication.jwt.JwtAuthenticator; +import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants; +import org.apache.archiva.redback.rest.api.services.RoleManagementService; +import org.apache.archiva.redback.rest.api.services.v2.AuthenticationService; +import org.apache.archiva.redback.rest.services.FakeCreateAdminService; +import org.apache.archiva.redback.rest.services.FakeCreateAdminServiceImpl; +import org.apache.archiva.redback.role.RoleManager; +import org.apache.archiva.redback.users.User; +import org.apache.archiva.redback.users.UserManager; +import org.apache.archiva.redback.users.UserManagerException; +import org.apache.archiva.redback.users.UserNotFoundException; +import org.apache.commons.lang3.SystemUtils; +import org.apache.cxf.common.util.Base64Utility; +import org.apache.cxf.jaxrs.client.JAXRSClientFactory; +import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.cxf.transport.servlet.CXFServlet; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.context.ContextLoaderListener; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.ws.rs.core.MediaType; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Olivier Lamy + */ +@ExtendWith( SpringExtension.class ) +@ContextConfiguration( locations = { "classpath*:/META-INF/spring-context.xml", "classpath*:/spring-context.xml" } ) +public abstract class AbstractRestServicesTestV2 +{ + + private JwtAuthenticator jwtAuthenticator; + private UserManager userManager; + + protected Logger log = LoggerFactory.getLogger( getClass() ); + + private static AtomicReference server = new AtomicReference<>(); + private static AtomicReference serverConnector = new AtomicReference<>(); + private RoleManager roleManager; + + protected void init() { + } + + protected void destroy() { + this.jwtAuthenticator = null; + this.userManager = null; + } + + /** + * Returns the server that was started, or null if not initialized before. + * @return + */ + public Server getServer() { + return this.server.get(); + } + + public int getServerPort() { + ServerConnector connector = serverConnector.get(); + if (connector!=null) { + return connector.getLocalPort(); + } else { + return 0; + } + } + + public JwtAuthenticator getJwtAuthenticator() { + if (this.jwtAuthenticator == null) { + JwtAuthenticator auth = ContextLoaderListener.getCurrentWebApplicationContext( ) + .getBean( JwtAuthenticator.class ); + assertNotNull( auth ); + this.jwtAuthenticator = auth; + } + return this.jwtAuthenticator; + } + + public UserManager getUserManager() { + if (this.userManager==null) { + UserManager userManager = ContextLoaderListener.getCurrentWebApplicationContext( ) + .getBean( "userManager#default", UserManager.class ); + assertNotNull( userManager ); + this.userManager = userManager; + } + return this.userManager; + } + + public RoleManager getRoleManager() { + if (this.roleManager==null) { + RoleManager roleManager = ContextLoaderListener.getCurrentWebApplicationContext( ) + .getBean( "roleManager", RoleManager.class ); + assertNotNull( roleManager ); + this.roleManager = roleManager; + } + return this.roleManager; + } + + JacksonJaxbJsonProvider getJsonProvider() { + JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider( ); + ObjectMapper mapper = new ObjectMapper( ); + mapper.registerModule( new JavaTimeModule( ) ); + provider.setMapper( mapper ); + return provider; + } + + protected boolean exists( DirContext context, String dn ) + { + Object result = null; + try { + result = context.lookup( dn ); + } + catch ( NameNotFoundException e ) { + return false; + } + catch ( NamingException e ) + { + log.error( "Unknown error during lookup: {}", e.getMessage( ) ); + } + return result != null; + } + + protected void deleteUser(User user) { + if (user!=null) + { + deleteUser( user.getUsername( ) ); + } + } + + protected void deleteUser(String userName) { + if (userName!=null) + { + try + { + getUserManager( ).deleteUser( userName ); + } + catch ( UserNotFoundException e ) + { + // ignore + } + catch ( UserManagerException e ) + { + log.error( "Could not delete user {}", userName ); + } + } + } + + /** + * Returns true, if the server does exist and is running. + * @return true, if server does exist and is running. + */ + public boolean isServerRunning() { + return this.server.get() != null && this.server.get().isRunning(); + } + + /** + * Returns the timeout in ms for rest requests. The timeout can be set by + * the system property rest.test.timeout. + * @return The timeout value in ms. + */ + public long getTimeout() + { + return Long.getLong( "rest.test.timeout", 1000000 ); + } + + public static String encode( String uid, String password ) + { + return "Basic " + Base64Utility.encode( ( uid + ":" + password ).getBytes() ); + } + + public String getAdminAuthzHeader() + { + assertNotNull( getJwtAuthenticator()); + String adminUser = RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME; + Token token = getJwtAuthenticator().generateToken( adminUser ); + return "Bearer " + token.getData( ); + } + + public String getAuthHeader(String userId) { + assertNotNull( getJwtAuthenticator() ); + Token token = getJwtAuthenticator().generateToken( userId ); + return "Bearer " + token.getData( ); + } + + protected String getSpringConfigLocation() + { + return "classpath*:spring-context.xml,classpath*:META-INF/spring-context.xml"; + } + + + protected String getRestServicesPath() + { + return "restServices"; + } + + public void startServer() + throws Exception + { + log.info("Starting server"); + Server myServer = new Server(); + this.server.set(myServer); + this.serverConnector.set(new ServerConnector( myServer, new HttpConnectionFactory())); + myServer.addConnector(serverConnector.get()); + + ServletHolder servletHolder = new ServletHolder( new CXFServlet() ); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setResourceBase( SystemUtils.JAVA_IO_TMPDIR ); + context.setSessionHandler( new SessionHandler( ) ); + context.addServlet( servletHolder, "/" + getRestServicesPath() + "/*" ); + context.setInitParameter( "contextConfigLocation", getSpringConfigLocation() ); + context.addEventListener(new ContextLoaderListener()); + + getServer().setHandler( context ); + getServer().start(); + + if (log.isDebugEnabled()) + { + log.debug( "Jetty dump: {}", getServer().dump() ); + } + + log.info( "Started server on port {}", getServerPort() ); + + UserManager um = getUserManager( ); + + User adminUser = null; + try + { + adminUser = um.findUser( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME ); + } catch ( UserNotFoundException e ) { + // ignore + } + if (adminUser==null) + { + adminUser = um.createUser( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME, "Administrator", "admin@local.home" ); + adminUser.setUsername( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME ); + adminUser.setPassword( FakeCreateAdminServiceImpl.ADMIN_TEST_PWD ); + adminUser.setFullName( "the admin user" ); + adminUser.setEmail( "toto@toto.fr" ); + adminUser.setPermanent( true ); + adminUser.setValidated( true ); + adminUser.setLocked( false ); + adminUser.setPasswordChangeRequired( false ); + um.addUser( adminUser ); + + getRoleManager( ).assignRole( "system-administrator", adminUser.getUsername( ) ); + } + + FakeCreateAdminService fakeCreateAdminService = getFakeCreateAdminService(); + this.jwtAuthenticator = null; + + //assertTrue( res.booleanValue() ); + + } + + protected FakeCreateAdminService getFakeCreateAdminService() + { + return JAXRSClientFactory.create( + "http://localhost:" + getServerPort()+ "/" + getRestServicesPath() + "/fakeCreateAdminService/", + FakeCreateAdminService.class, Collections.singletonList( getJsonProvider() ) ); + } + + public void stopServer() + throws Exception + { + if ( getServer() != null ) + { + log.info("Stopping server"); + getServer().stop(); + } + } + + protected AuthenticationService getLoginServiceV2( String authzHeader ) + { + AuthenticationService service = + JAXRSClientFactory.create( "http://localhost:" + getServerPort() + "/" + getRestServicesPath() + "/v2/redback/", + AuthenticationService.class, Collections.singletonList( getJsonProvider() ) ); + + // for debuging purpose + WebClient.getConfig( service ).getHttpConduit().getClient().setReceiveTimeout( getTimeout() ); + + if ( authzHeader != null ) + { + WebClient.client( service ).header( "Authorization", authzHeader ); + } + WebClient.client(service).header("Referer","http://localhost:"+getServerPort()); + + WebClient.client( service ).accept( MediaType.APPLICATION_JSON_TYPE ); + WebClient.client( service ).type( MediaType.APPLICATION_JSON_TYPE ); + + return service; + } + + + +} diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AuthenticationServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AuthenticationServiceTest.java index b494e592..9069a032 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AuthenticationServiceTest.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AuthenticationServiceTest.java @@ -21,30 +21,50 @@ import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants; import org.apache.archiva.redback.rest.api.model.LoginRequest; import org.apache.archiva.redback.rest.api.model.Token; -import org.apache.archiva.redback.rest.api.model.User; import org.apache.archiva.redback.rest.api.services.RedbackServiceException; import org.apache.archiva.redback.rest.api.services.UserService; -import org.apache.archiva.redback.rest.services.AbstractRestServicesTest; import org.apache.archiva.redback.rest.services.FakeCreateAdminService; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.apache.archiva.redback.users.User; +import org.apache.archiva.redback.users.UserManager; +import org.apache.archiva.redback.users.UserManagerException; +import org.apache.archiva.redback.users.memory.SimpleUser; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.*; /** * @author Olivier Lamy */ -@RunWith( SpringJUnit4ClassRunner.class ) +@ExtendWith( SpringExtension.class ) @ContextConfiguration( locations = { "classpath:/spring-context.xml" } ) public class AuthenticationServiceTest - extends AbstractRestServicesTest + extends AbstractRestServicesTestV2 { + @BeforeEach + void setup() throws Exception + { + super.init(); + super.startServer(); + } + + @AfterEach + void stop() throws Exception + { + super.stopServer(); + super.destroy(); + } + @Test public void loginAdmin() throws Exception { - assertNotNull( getLoginService( null ).logIn( new LoginRequest( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME, + assertNotNull( getLoginServiceV2( null ).logIn( new LoginRequest( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME, FakeCreateAdminService.ADMIN_TEST_PWD ) ) ); } @@ -56,44 +76,46 @@ public void createUserThenLog() { // START SNIPPET: create-user - User user = new User( "toto", "toto the king", "toto@toto.fr", false, false ); + UserManager um = getUserManager( ); + User user = um.createUser( "toto", "toto the king", "toto@toto.fr" ); + user.setValidated( true ); + user.setLocked( false ); user.setPassword( "foo123" ); user.setPermanent( false ); user.setPasswordChangeRequired( false ); user.setLocked( false ); - user.setValidated( true ); - UserService userService = getUserService( authorizationHeader ); - userService.createUser( user ); + user = um.addUser( user ); // END SNIPPET: create-user - user = userService.getUser( "toto" ); assertNotNull( user ); assertEquals( "toto the king", user.getFullName() ); assertEquals( "toto@toto.fr", user.getEmail() ); - getLoginServiceV2( encode( "toto", "foo123" ) ).pingWithAutz(); + getLoginServiceV2( getAuthHeader( "toto" ) ).pingWithAutz(); } finally { - getUserService( authorizationHeader ).deleteUser( "toto" ); - getUserService( authorizationHeader ).removeFromCache( "toto" ); - assertNull( getUserService( authorizationHeader ).getUser( "toto" ) ); + deleteUser( "toto" ); } } @Test - public void simpleLogin() throws RedbackServiceException + public void simpleLogin() throws RedbackServiceException, UserManagerException { + String authorizationHeader = getAdminAuthzHeader( ); try { // START SNIPPET: create-user - User user = new User( "toto", "toto the king", "toto@toto.fr", false, false ); + UserManager um = getUserManager( ); + User user = um.createUser( "toto", "toto the king", "toto@toto.fr" ); user.setPassword( "foo123" ); user.setPermanent( false ); user.setPasswordChangeRequired( false ); user.setLocked( false ); user.setValidated( true ); - UserService userService = getUserService( authorizationHeader ); - userService.createUser( user ); + user = um.addUser( user ); + // We need this additional round, because new users have the password change flag set to true + user.setPasswordChangeRequired( false ); + um.updateUser( user ); // END SNIPPET: create-user LoginRequest request = new LoginRequest( "toto", "foo123" ); Token result = getLoginServiceV2( "" ).logIn( request ); @@ -103,9 +125,7 @@ public void simpleLogin() throws RedbackServiceException } finally { - getUserService( authorizationHeader ).deleteUser( "toto" ); - getUserService( authorizationHeader ).removeFromCache( "toto" ); - assertNull( getUserService( authorizationHeader ).getUser( "toto" ) ); + deleteUser( "toto" ); } } diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/GroupServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/GroupServiceTest.java index 7fcdc181..70e412d1 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/GroupServiceTest.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/GroupServiceTest.java @@ -22,19 +22,23 @@ import org.apache.archiva.components.apacheds.ApacheDs; import org.apache.archiva.redback.rest.api.model.GroupMapping; import org.apache.archiva.redback.rest.api.services.v2.GroupService; -import org.apache.archiva.redback.rest.services.AbstractRestServicesTest; import org.apache.commons.lang3.StringUtils; import org.apache.cxf.jaxrs.client.JAXRSClientFactory; import org.apache.cxf.jaxrs.client.WebClient; import org.assertj.core.api.Condition; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.test.annotation.DirtiesContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import javax.inject.Inject; import javax.inject.Named; +import javax.naming.NamingException; import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttribute; import javax.naming.directory.BasicAttributes; @@ -51,12 +55,12 @@ /** * @author Olivier Lamy */ -@RunWith( SpringJUnit4ClassRunner.class ) +@ExtendWith( SpringExtension.class ) @ContextConfiguration( - locations = { "classpath:/ldap-spring-test.xml" } ) -@DirtiesContext( classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD ) + locations = {"classpath:/ldap-spring-test.xml"} ) +@TestInstance( TestInstance.Lifecycle.PER_CLASS ) public class GroupServiceTest - extends AbstractRestServicesTest + extends AbstractRestServicesTestV2 { @Inject @@ -73,18 +77,18 @@ public class GroupServiceTest protected GroupService getGroupService( String authzHeader ) { GroupService service = - JAXRSClientFactory.create( "http://localhost:" + getServerPort() + "/" + getRestServicesPath() + "/v2/redback/", + JAXRSClientFactory.create( "http://localhost:" + getServerPort( ) + "/" + getRestServicesPath( ) + "/v2/redback/", GroupService.class, - Collections.singletonList( new JacksonJaxbJsonProvider() ) ); + Collections.singletonList( new JacksonJaxbJsonProvider( ) ) ); // for debuging purpose - WebClient.getConfig( service ).getHttpConduit().getClient().setReceiveTimeout( getTimeout() ); + WebClient.getConfig( service ).getHttpConduit( ).getClient( ).setReceiveTimeout( getTimeout( ) ); if ( authzHeader != null ) { WebClient.client( service ).header( "Authorization", authzHeader ); } - WebClient.client(service).header("Referer","http://localhost:"+getServerPort()); + WebClient.client( service ).header( "Referer", "http://localhost:" + getServerPort( ) ); WebClient.client( service ).accept( MediaType.APPLICATION_JSON_TYPE ); WebClient.client( service ).type( MediaType.APPLICATION_JSON_TYPE ); @@ -93,70 +97,158 @@ protected GroupService getGroupService( String authzHeader ) } @Override - protected String getSpringConfigLocation() + protected String getSpringConfigLocation( ) { return "classpath*:spring-context.xml,classpath*:META-INF/spring-context.xml,classpath:/ldap-spring-test.xml"; } - @Override - public void startServer() + @BeforeAll + public void startup( ) throws Exception { - super.startServer(); - - groupSuffix = apacheDs.addSimplePartition( "test", new String[]{ "archiva", "apache", "org" } ).getSuffix(); - - log.info( "groupSuffix: {}", groupSuffix ); + super.init( ); + super.startServer( ); suffix = "ou=People,dc=archiva,dc=apache,dc=org"; - log.info( "DN Suffix: {}", suffix ); - - apacheDs.startServer(); - - BasicAttribute objectClass = new BasicAttribute( "objectClass" ); - objectClass.add( "top" ); - objectClass.add( "organizationalUnit" ); - - Attributes attributes = new BasicAttributes( true ); - attributes.put( objectClass ); - attributes.put( "organizationalUnitName", "foo" ); - - apacheDs.getAdminContext().createSubcontext( suffix, attributes ); - - createGroups(); - } - - @Override - public void stopServer() - throws Exception - { - - // cleanup ldap entries - InitialDirContext context = apacheDs.getAdminContext(); - - for ( String group : this.groups ) + if ( apacheDs.isStopped( ) ) { - context.unbind( createGroupDn( group ) ); + groupSuffix = apacheDs.addSimplePartition( "test", new String[]{"archiva", "apache", "org"} ).getSuffix( ); + + log.info( "groupSuffix: {}", groupSuffix ); + apacheDs.startServer( ); + if ( !exists( apacheDs.getAdminContext( ), suffix ) ) + { + BasicAttribute objectClass = new BasicAttribute( "objectClass" ); + objectClass.add( "top" ); + objectClass.add( "organizationalUnit" ); + + Attributes attributes = new BasicAttributes( true ); + attributes.put( objectClass ); + attributes.put( "organizationalUnitName", "foo" ); + + apacheDs.getAdminContext( ).createSubcontext( suffix, attributes ); + } } - - context.unbind( suffix ); - - context.close(); - - apacheDs.stopServer(); - - super.stopServer(); } - private void createGroups() + @BeforeEach + public void initLdap( ) throws Exception + { + removeAllGroups( ); + createGroups( ); + } + + @AfterEach + public void cleanupLdap( ) throws NamingException + { + removeAllGroups( ); + } + + private void removeAllGroups( ) + { + if (!apacheDs.isStopped()) + { + InitialDirContext context = null; + try + { + context = apacheDs.getAdminContext( ); + for ( String group : this.groups ) + { + try + { + context.unbind( createGroupDn( group ) ); + } + catch ( NamingException e ) + { + // Ignore + } + } + + } + catch ( NamingException e ) + { + log.error( "Could not remove groups {}", e.getMessage( ), e ); + } + finally + { + try + { + if ( context != null ) context.close( ); + } + catch ( Exception e ) + { + log.error( "Error during context close {}", e.getMessage( ) ); + } + } + } + } + + @AfterAll + public void stop( ) throws Exception + + { + + removeAllGroups( ); + // cleanup ldap entries + try + { + InitialDirContext context = null; + try + { + context = apacheDs.getAdminContext( ); + context.unbind( suffix ); + } + finally + { + try + { + if ( context != null ) context.close( ); + } + catch ( Exception e ) + { + log.error( "Error during context close {}", e.getMessage( ) ); + } + try + { + apacheDs.stopServer( ); + } + catch ( Exception e ) + { + log.error( "Could not stop apacheds {}", e.getMessage( ) ); + } + } + } + catch ( Exception e ) + { + log.error( "Could not stop ldap {}", e.getMessage( ) ); + } + finally + { + super.stopServer( ); + super.destroy( ); + } + } + + private void createGroups( ) throws Exception { - InitialDirContext context = apacheDs.getAdminContext(); - - for ( String group : groups ) + InitialDirContext context = null; + try { - createGroup( context, group, createGroupDn( group ) ); + context = apacheDs.getAdminContext( ); + + for ( String group : groups ) + { + createGroup( context, group, createGroupDn( group ) ); + } + } + finally + { + if ( context != null ) + { + context.close( ); + } } } @@ -164,19 +256,26 @@ private void createGroups() private void createGroup( DirContext context, String groupName, String dn ) throws Exception { + if ( !exists( context, dn ) ) + { + Attributes attributes = new BasicAttributes( true ); + BasicAttribute objectClass = new BasicAttribute( "objectClass" ); + objectClass.add( "top" ); + objectClass.add( "groupOfUniqueNames" ); + attributes.put( objectClass ); + attributes.put( "cn", groupName ); + BasicAttribute basicAttribute = new BasicAttribute( "uniquemember" ); - Attributes attributes = new BasicAttributes( true ); - BasicAttribute objectClass = new BasicAttribute( "objectClass" ); - objectClass.add( "top" ); - objectClass.add( "groupOfUniqueNames" ); - attributes.put( objectClass ); - attributes.put( "cn", groupName ); - BasicAttribute basicAttribute = new BasicAttribute( "uniquemember" ); + basicAttribute.add( "uid=admin," + suffix ); - basicAttribute.add( "uid=admin," + suffix ); + attributes.put( basicAttribute ); - attributes.put( basicAttribute ); - context.createSubcontext( dn, attributes ); + context.createSubcontext( dn, attributes ); + } + else + { + log.error( "Group {} exists already", dn ); + } } private String createGroupDn( String cn ) @@ -185,71 +284,76 @@ private String createGroupDn( String cn ) } @Test - public void getAllGroups() + public void getAllGroups( ) throws Exception { + String authorizationHeader = getAdminAuthzHeader( ); try { GroupService service = getGroupService( authorizationHeader ); - List allGroups = service.getGroups( Integer.valueOf( 0 ), Integer.valueOf( Integer.MAX_VALUE ) ).getData().stream( ).map( group -> group.getName( ) ).collect( Collectors.toList( ) ); + List allGroups = service.getGroups( Integer.valueOf( 0 ), Integer.valueOf( Integer.MAX_VALUE ) ).getData( ).stream( ).map( group -> group.getName( ) ).collect( Collectors.toList( ) ); - assertThat( allGroups ).isNotNull().isNotEmpty().hasSize( 3 ).containsAll( groups ); + assertThat( allGroups ).isNotNull( ).isNotEmpty( ).hasSize( 3 ).containsAll( groups ); } catch ( Exception e ) { - log.error( e.getMessage(), e ); + log.error( e.getMessage( ), e ); throw e; } } @Test - public void getGroupMappings() + public void getGroupMappings( ) throws Exception { + + String authorizationHeader = getAdminAuthzHeader( ); try { GroupService service = getGroupService( authorizationHeader ); - List mappings = service.getGroupMappings(); + List mappings = service.getGroupMappings( ); - assertThat( mappings ).isNotNull().isNotEmpty().hasSize( 3 ); + assertThat( mappings ).isNotNull( ).isNotEmpty( ).hasSize( 3 ); } catch ( Exception e ) { - log.error( e.getMessage(), e ); + log.error( e.getMessage( ), e ); throw e; } } @Test - public void addThenRemove() + public void addThenRemove( ) throws Exception { + String authorizationHeader = getAdminAuthzHeader( ); + try { GroupService service = getGroupService( authorizationHeader ); - List mappings = service.getGroupMappings(); + List mappings = service.getGroupMappings( ); - assertThat( mappings ).isNotNull().isNotEmpty().hasSize( 3 ); + assertThat( mappings ).isNotNull( ).isNotEmpty( ).hasSize( 3 ); GroupMapping groupMapping = new GroupMapping( "ldap group", Arrays.asList( "redback role" ) ); service.addGroupMapping( groupMapping ); - mappings = service.getGroupMappings(); + mappings = service.getGroupMappings( ); - assertThat( mappings ).isNotNull().isNotEmpty().hasSize( 4 ).are( - new Condition() + assertThat( mappings ).isNotNull( ).isNotEmpty( ).hasSize( 4 ).are( + new Condition( ) { @Override public boolean matches( GroupMapping mapping ) { - if ( StringUtils.equals( "ldap group", mapping.getGroup() ) ) + if ( StringUtils.equals( "ldap group", mapping.getGroup( ) ) ) { - assertThat( mapping.getRoleNames() ).isNotNull().isNotEmpty().containsOnly( + assertThat( mapping.getRoleNames( ) ).isNotNull( ).isNotEmpty( ).containsOnly( "redback role" ); return true; } @@ -260,13 +364,13 @@ public boolean matches( GroupMapping mapping ) service.removeGroupMapping( "ldap group" ); - mappings = service.getGroupMappings(); + mappings = service.getGroupMappings( ); - assertThat( mappings ).isNotNull().isNotEmpty().hasSize( 3 ); + assertThat( mappings ).isNotNull( ).isNotEmpty( ).hasSize( 3 ); } catch ( Exception e ) { - log.error( e.getMessage(), e ); + log.error( e.getMessage( ), e ); throw e; } } diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/resources/spring-context.xml b/redback-integrations/redback-rest/redback-rest-services/src/test/resources/spring-context.xml index e2b584d4..a9f90c86 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/resources/spring-context.xml +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/resources/spring-context.xml @@ -47,6 +47,8 @@ + +