Skip to content

Commit

Permalink
Merge pull request #3 from jenkinsci/jenkins-29370-context
Browse files Browse the repository at this point in the history
[JENKINS-29370] Add the concept of an authentication token context
  • Loading branch information
stephenc committed Jul 20, 2015
2 parents ac2edca + 4e5848a commit 34165ee
Show file tree
Hide file tree
Showing 4 changed files with 478 additions and 5 deletions.
@@ -0,0 +1,204 @@
/*
* The MIT License
*
* Copyright (c) 2015, CloudBees, Inc., Stephen Connolly.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.authentication.tokens.api;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import net.jcip.annotations.NotThreadSafe;
import net.jcip.annotations.ThreadSafe;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* The context within which an authentication token will be used.
*
* @param <T> the type of token
* @since 1.2
*/
@ThreadSafe
public final class AuthenticationTokenContext<T> {
/**
* The base class
*/
@NonNull
private final Class<T> tokenClass;
/**
* The optional purposes for which the token will be used.
*/
@CheckForNull
private final Map<Object, Object> purposes;

/**
* Creates a basic context for any purpose.
*
* @param tokenClass the type of token.
*/
public AuthenticationTokenContext(@NonNull Class<T> tokenClass) {
this(tokenClass, null);
}

/**
* Creates a context with the specified purposes.
*
* @param tokenClass the type of token.
* @param purposes the purposes.
*/
private AuthenticationTokenContext(@NonNull Class<T> tokenClass, @CheckForNull Map<Object, Object> purposes) {
this.tokenClass = tokenClass;
this.purposes = purposes;
}

/**
* Creates a {@link Builder} for contexts of the specified token type.
*
* @param tokenClass the type of token.
* @param <T> the type of token.
* @return a {@link Builder} instance.
*/
public static <T> Builder<T> builder(@NonNull Class<T> tokenClass) {
return new Builder<T>(tokenClass);
}

/**
* Returns the type of token.
*
* @return the type of token.
*/
@NonNull
public Class<T> getTokenClass() {
return tokenClass;
}

/**
* Checks if the context specifies the supplied purpose and matches against the valid values.
*
* @param purpose the purpose.
* @param validValues the valid values that the purpose must match if specified.
* @return {@code true} if either the purpose is not specified or the purpose is specified and is equal
* to one of the specified values.
*/
public boolean canHave(@NonNull Object purpose, Object... validValues) {
if (purposes == null || !purposes.containsKey(purpose)) {
// we do not have a counter purpose
return true;
}
Object value = purposes.get(purpose);
for (Object valid : validValues) {
if (value == null ? valid == null : value.equals(valid)) {
return true;
}
}
return false;
}

/**
* Ensures the context specifies the supplied purpose matching against the valid values.
*
* @param purpose the purpose.
* @param validValues the valid values that the purpose must match.
* @return {@code true} if and only if the purpose is specified and is equal to one of the specified values.
*/
public boolean mustHave(@NonNull Object purpose, Object... validValues) {
if (purposes == null || !purposes.containsKey(purpose)) {
return false;
}
Object value = purposes.get(purpose);
for (Object valid : validValues) {
if (value == null ? valid == null : value.equals(valid)) {
return true;
}
}
return false;
}

/**
* A non-thread safe builder of {@link AuthenticationTokenContext} instances.
*
* @param <T> the token type.
* @since 1.2
*/
@NotThreadSafe
public static final class Builder<T> {

/**
* The token type.
*/
@NonNull
private final Class<T> tokenClass;
/**
* The purposes.
*/
@CheckForNull
private Map<Object, Object> purposes = null;

/**
* Constructs a new builder.
*
* @param tokenClass the token type.
*/
private Builder(@NonNull Class<T> tokenClass) {
this.tokenClass = tokenClass;
}

/**
* Specifies the supplied purpose (with value {@link Boolean#TRUE}).
*
* @param purpose the purpose.
* @return {@code this} for method chaining.
*/
@NonNull
public Builder<T> with(@NonNull Object purpose) {
return with(purpose, Boolean.TRUE);
}

/**
* Specifies the supplied purpose with the specified value.
*
* @param purpose the purpose.
* @param value the value.
* @return {@code this} for method chaining.
*/
@NonNull
public Builder<T> with(@NonNull Object purpose, @CheckForNull Object value) {
if (purposes == null) {
purposes = new LinkedHashMap<Object, Object>();
}
purposes.put(purpose, value);
return this;
}

/**
* Instantiates the {@link AuthenticationTokenContext}.
*
* @return the {@link AuthenticationTokenContext}.
*/
@NonNull
public AuthenticationTokenContext<T> build() {
return new AuthenticationTokenContext<T>(tokenClass,
purposes == null || purposes.isEmpty() ? null : purposes);
}
}

}
Expand Up @@ -121,6 +121,27 @@ public final boolean consumes(@NonNull Credentials credentials) {
return this.credentialsClass.isInstance(credentials) && matcher().matches(credentials);
}

/**
* Checks if this source fits the specified context.
* @param context the context that an authentication token is required in.
* @return {@code true} if and only if this source fits the specified context.
* @since 1.2
*/
@SuppressWarnings("unchecked")
public final boolean fits(AuthenticationTokenContext<?> context) {
return produces(context.getTokenClass()) && isFit((AuthenticationTokenContext<? super T>) context);
}

/**
* Checks if this source fits the specified context, override this method
* @param context the context that an authentication token is required in.
* @return {@code true} if and only if this source fits the specified context.
* @since 1.2
*/
protected boolean isFit(AuthenticationTokenContext<? super T> context) {
return true;
}

/**
* Score the goodness of match.
* @param tokenClass the token class.
Expand Down Expand Up @@ -156,4 +177,17 @@ public final boolean consumes(@NonNull Credentials credentials) {
}
return ((int)producerScore) << 16 | (int)consumerScore;
}

/**
* Score the goodness of match.
*
* @param context the context that an authentication token is required in.
* @param credentials the credentials instance.
* @return the match score (higher the better) or {@code null} if not a match.
* @since 1.2
*/
/*package*/
final Integer score(AuthenticationTokenContext<?> context, Credentials credentials) {
return fits(context) ? score(context.getTokenClass(), credentials) : null;
}
}
Expand Up @@ -32,7 +32,6 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
Expand Down Expand Up @@ -67,10 +66,22 @@ private AuthenticationTokens() {
* @return a matcher for the type of token
*/
public static <T> CredentialsMatcher matcher(Class<T> tokenClass) {
return matcher(new AuthenticationTokenContext<T>(tokenClass));
}

/**
* Builds a matcher for credentials that can be converted into the supplied token type.
*
* @param context the context that an authentication token is required in.
* @param <T> the type of token.
* @return a matcher for the type of token.
* @since 1.2
*/
public static <T> CredentialsMatcher matcher(AuthenticationTokenContext<T> context) {
List<CredentialsMatcher> matchers = new ArrayList<CredentialsMatcher>();
for (AuthenticationTokenSource<?, ?> source : Jenkins.getInstance()
.getExtensionList(AuthenticationTokenSource.class)) {
if (source.produces(tokenClass)) {
if (source.fits(context)) {
matchers.add(source.matcher());
}
}
Expand All @@ -91,6 +102,25 @@ public static <T> CredentialsMatcher matcher(Class<T> tokenClass) {
@SuppressWarnings("unchecked")
@CheckForNull
public static <T, C extends Credentials> T convert(@NonNull Class<T> type, @CheckForNull C credentials) {
if (credentials == null) {
return null;
}
return convert(new AuthenticationTokenContext<T>(type), credentials);
}

/**
* Converts the supplied credentials into the specified token.
*
* @param context the context that an authentication token is required in.
* @param credentials the credentials instance to convert.
* @param <T> the type of token to convert to.
* @param <C> the type of credentials to convert,
* @return the token or {@code null} if the credentials could not be converted.
* @since 1.2
*/
@SuppressWarnings("unchecked")
@CheckForNull
public static <T, C extends Credentials> T convert(@NonNull AuthenticationTokenContext<T> context, @CheckForNull C credentials) {
if (credentials == null) {
return null;
}
Expand All @@ -99,7 +129,7 @@ public static <T, C extends Credentials> T convert(@NonNull Class<T> type, @Chec
Collections.reverseOrder());
for (AuthenticationTokenSource<?, ?> source : Jenkins.getInstance()
.getExtensionList(AuthenticationTokenSource.class)) {
Integer score = source.score(type, credentials);
Integer score = source.score(context, credentials);
if (score != null && !matches.containsKey(score)) {
// if there are two extensions with the same score,
// then the first (i.e. highest Extension.ordinal should win)
Expand All @@ -108,7 +138,7 @@ public static <T, C extends Credentials> T convert(@NonNull Class<T> type, @Chec
}
// now try all the matches (form best to worst) until we get a conversion
for (AuthenticationTokenSource<?,?> source: matches.values()) {
if (source.produces(type) && source.consumes(credentials)) { // redundant test, but for safety
if (source.produces(context.getTokenClass()) && source.consumes(credentials)) { // redundant test, but for safety
AuthenticationTokenSource<? extends T, ? super C> s =
(AuthenticationTokenSource<? extends T, ? super C>) source;
T token = null;
Expand All @@ -118,7 +148,7 @@ public static <T, C extends Credentials> T convert(@NonNull Class<T> type, @Chec
LogRecord lr = new LogRecord(Level.FINE,
"Could not convert credentials {0} into token of type {1} using source {2}: {3}");
lr.setThrown(e);
lr.setParameters(new Object[]{credentials, type, s, e.getMessage()});
lr.setParameters(new Object[]{credentials, context.getTokenClass(), s, e.getMessage()});
LOGGER.log(lr);
}
if (token != null) {
Expand All @@ -129,4 +159,5 @@ public static <T, C extends Credentials> T convert(@NonNull Class<T> type, @Chec

return null;
}

}

0 comments on commit 34165ee

Please sign in to comment.