Skip to content

Commit

Permalink
Refactor PersistentCredentials
Browse files Browse the repository at this point in the history
Try to improve code readability by introducing a callback method for
the interaction between the SingleUseCredentialsProvider and the
PersistentCredentials class.

Improve thread safety by making the getUsernameAndPassword method
syncronized. This avoids to show multiple dialogues in parallel to
the user in parallel.
  • Loading branch information
maarzt committed Nov 9, 2023
1 parent d6779be commit 1bec310
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,36 @@

import net.miginfocom.swing.MigLayout;

import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;

/**
* This class is meant to be used with JGIT for a comfortable way to ask the
* user for credentials. The user is only asked once for username and password.
* The credentials are stored in memory and reused for all subsequent requests.
* It also tries to detect if the user entered wrong credentials,
* and asks for new credentials.
*/
public class PersistentCredentials
{

private String username = null;

private String password = null;

private boolean missingCredentials()
/**
* This method simply returns the username and password if they are already
* known. It asks the user for credentials if they are not known yet, or if
* the previous attempt to use the credentials failed.
*/
private synchronized Pair< String, String > getUsernameAndPassword( URIish uri, boolean authenticationFailure )
{
return password == null || username == null;
boolean missingCredentials = password == null || username == null;
if ( missingCredentials || authenticationFailure )
if ( !queryPassword( uri.toString(), authenticationFailure ) )
return null;
return Pair.of( username, password );
}

private boolean queryPassword( String url, boolean previousAuthenticationFailed )
Expand Down Expand Up @@ -53,69 +68,6 @@ private boolean queryPassword( String url, boolean previousAuthenticationFailed

public CredentialsProvider getSingleUseCredentialsProvider()
{
return new SingleUseCredentialsProvider();
}

/**
* The JGIT api does not tell a CredentialsProvider if the credentials
* where correct. It simply asks for them again if they were wrong.
* <p>
* We can exploit this behavior by counting the number of times the
* CredentialsProvider was asked for credentials. If it was asked more than
* once, we assume that the credentials were wrong.
* <p>
* This only works if the CredentialsProvider is only used once.
*/
private class SingleUseCredentialsProvider extends CredentialsProvider
{
private int counter = 0;

@Override
public boolean isInteractive()
{
return true;
}

@Override
public boolean supports( CredentialItem... items )
{
for ( CredentialItem item : items )
if ( !isUsernameOrPassword( item ) )
return false;
return true;
}

private boolean isUsernameOrPassword( CredentialItem item )
{
return ( item instanceof CredentialItem.Username ) || ( item instanceof CredentialItem.Password );
}

@Override
public boolean get( URIish uri, CredentialItem... items ) throws UnsupportedCredentialItem
{
if ( !supports( items ) )
return false;
counter++;
boolean previousAuthenticationFailed = counter > 1;
if ( previousAuthenticationFailed || missingCredentials() )
if ( !queryPassword( uri.toString(), previousAuthenticationFailed ) )
return false;
fillUsernameAndPassword( items );
return true;
}

private void fillUsernameAndPassword( CredentialItem[] items )
{
for ( CredentialItem item : items )
fillItem( item );
}

private void fillItem( CredentialItem item )
{
if ( item instanceof CredentialItem.Username )
( ( CredentialItem.Username ) item ).setValue( username );
else if ( item instanceof CredentialItem.Password )
( ( CredentialItem.Password ) item ).setValue( password.toCharArray() );
}
return new SingleUseCredentialsProvider( this::getUsernameAndPassword );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.mastodon.mamut.tomancak.collaboration.credentials;

import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;

/**
* The JGIT api does not tell a CredentialsProvider if the credentials
* where correct. It simply asks for them again if they were wrong.
* <p>
* We can exploit this behavior by counting the number of times the
* CredentialsProvider was asked for credentials. If it was asked more than
* once, we assume that the credentials were wrong.
* <p>
* This only works if the CredentialsProvider is only used once.
*/
class SingleUseCredentialsProvider extends CredentialsProvider
{
private final Callback callback;

private int counter = 0;

public SingleUseCredentialsProvider( Callback callback )
{
this.callback = callback;
}

@Override
public boolean isInteractive()
{
return true;
}

@Override
public boolean supports( CredentialItem... items )
{
for ( CredentialItem item : items )
if ( !isUsernameOrPassword( item ) )
return false;
return true;
}

private boolean isUsernameOrPassword( CredentialItem item )
{
return ( item instanceof CredentialItem.Username ) || ( item instanceof CredentialItem.Password );
}

@Override
public boolean get( URIish uri, CredentialItem... items ) throws UnsupportedCredentialItem
{
if ( !supports( items ) )
throw new UnsupportedCredentialItem( uri, "" );
counter++;
boolean previousAuthenticationFailed = counter > 1;
Pair< String, String > usernameAndPassword = callback.getUsernameAndPassword( uri, previousAuthenticationFailed );
if ( usernameAndPassword == null )
return false;
fillUsernameAndPassword( items, usernameAndPassword.getLeft(), usernameAndPassword.getRight() );
return true;
}

private void fillUsernameAndPassword( CredentialItem[] items, String username, String password )
{
for ( CredentialItem item : items )
fillItem( item, username, password );
}

private void fillItem( CredentialItem item, String username, String password )
{
if ( item instanceof CredentialItem.Username )
( ( CredentialItem.Username ) item ).setValue( username );
else if ( item instanceof CredentialItem.Password )
( ( CredentialItem.Password ) item ).setValue( password.toCharArray() );
}

/**
* Callback interface for {@link SingleUseCredentialsProvider}.
*/
interface Callback
{

/**
* The {@link SingleUseCredentialsProvider} calls this method to get
* username and password for the given {@link URIish}.
*
* @param uri the URI for which credentials are requested.
* @param authenticationFailure true if the SingleUseCredentialsProvider
* thinks that the credentials were wrong.
* @return username and password or null if the user canceled the
* request.
* @see SingleUseCredentialsProvider
*/
Pair< String, String > getUsernameAndPassword( URIish uri, boolean authenticationFailure );
}
}

0 comments on commit 1bec310

Please sign in to comment.