Compare commits
4 Commits
master
...
story/JENK
Author | SHA1 | Date |
---|---|---|
Vivek Pandey | 07bced4a2d | |
Vivek Pandey | 24e0b1d46b | |
Vivek Pandey | 225ab9a854 | |
Vivek Pandey | ab78663c9c |
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
##
|
||||
#
|
||||
# Usage
|
||||
#
|
||||
# jwtcurl [-u username:password] [-b BASE_URL] "[-X GET|POST|PUT|DELETE] BO_API_URL"
|
||||
#
|
||||
# Options:
|
||||
# -v: verbose output
|
||||
# -u: basic auth parameter in username:password format
|
||||
# -b: base url of jenkins without trailing slash. e.g. http://localhost:8080/jenkins or https://blueocean.io
|
||||
#
|
||||
# Note: You need to enclose last argument in double quotes if you are passing arguments to curl.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# Anonymous user:
|
||||
#
|
||||
# jwtcurl http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/p1/
|
||||
#
|
||||
# User with credentials:
|
||||
#
|
||||
# jwtcurl -u admin:admin http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/p1/
|
||||
#
|
||||
# Use base url other than http://localhost:8080/jenkins
|
||||
#
|
||||
# jwtcurl -u admin:admin -b https://myjenkinshost http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/p1/
|
||||
#
|
||||
# Author: Vivek Pandey
|
||||
#
|
||||
##
|
||||
if [ $# -eq 0 ]
|
||||
then
|
||||
echo "Usage: jwtcurl [-v] [-u username:password] [-b BASE_URL] \"-X [GET|POST|PUT|DELETE] BO_API_URL\""
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
while [[ $# -gt 1 ]]
|
||||
do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
-u)
|
||||
CREDENTIAL="-u $2"
|
||||
shift
|
||||
;;
|
||||
-b)
|
||||
BASE_URL="$2"
|
||||
shift
|
||||
;;
|
||||
-v)
|
||||
VERBOSE="$2"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
# unknown option
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ ! -z "$VERBOSE" ]; then
|
||||
SETX="set -x"
|
||||
CURL_VERBOSE="-v"
|
||||
fi
|
||||
|
||||
if [ -z "${BASE_URL}" ]; then
|
||||
BASE_URL=http://localhost:8080/jenkins
|
||||
fi
|
||||
|
||||
${SETX}
|
||||
|
||||
TOKEN=$(curl ${CURL_VERBOSE} -s -X GET ${CREDENTIAL} -I ${BASE_URL}/jwt-auth/token | awk 'BEGIN {FS=": "}/^X-BLUEOCEAN-JWT/{print $2}'|sed $'s/\r//')
|
||||
|
||||
if [ -z "${TOKEN}" ]; then
|
||||
echo "Failed to get JWT token"
|
||||
echo $?
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl ${CURL_VERBOSE} -H "Authorization: Bearer ${TOKEN}" $@
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package io.jenkins.blueocean.analyticstools;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.Plugin;
|
||||
import io.jenkins.blueocean.BluePageDecorator;
|
||||
import jenkins.model.Jenkins;
|
||||
|
||||
|
@ -17,6 +18,7 @@ public class AnalyticsTools extends BluePageDecorator {
|
|||
|
||||
/** gives Blueocean plugin version. blueocean-web being core module is looked at to determine the version */
|
||||
public String getBlueOceanPluginVersion(){
|
||||
return Jenkins.getInstance().getPlugin("blueocean-web").getWrapper().getVersion();
|
||||
Plugin plugin = Jenkins.getInstance().getPlugin("blueocean-web");
|
||||
return (plugin != null) ? plugin.getWrapper().getVersion() : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License
|
||||
|
||||
Copyright (c) 2016 CloudBees Inc and a number of other of contributors
|
||||
|
||||
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.
|
|
@ -0,0 +1,88 @@
|
|||
# BlueOcean JWT Plugin
|
||||
|
||||
This plugin provides JWT authenticated related APIs. JWT token is signed using RSA256 algorithm. This is asymmetric
|
||||
algorithm, this means the token is signed using the private key and Client must use corresponding public key to verify
|
||||
the claims.
|
||||
|
||||
# APIs
|
||||
|
||||
## JWT Token API
|
||||
|
||||
JWT token is generated for the user in session. In Jenkins there is always a user in context, that is if there is no
|
||||
logged in user then the generated token will carry the claim for anonymous user.
|
||||
|
||||
Default expiry time of token is 30 minutes.
|
||||
|
||||
JWT token is return as X-BLUEOCEAN-JWT HTTP header.
|
||||
|
||||
|
||||
GET /jwt-auth/token
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
X-BLUEOCEAN-JWT: eyJraWQiOiI2M2ZhMTY0ZWRhMDk0NjNjOGZlZTI2Njg4ZjgxOTZmZCIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJqdGkiOiJiMGVmMjJiNDliNWM0N2JjODU4YTg2MDdkM2Y0NGQzMyIsImlzcyI6ImJsdWVvY2Vhbi1qd3Q6Iiwic3ViIjoiYWxpY2UiLCJuYW1lIjoiQWxpY2UgQ29vcGVyIiwiaWF0IjoxNDcwMzMxNjA1LCJleHAiOjE0NzAzMzM0MDUsIm5iZiI6MTQ3MDMzMTU3NSwiY29udGV4dCI6eyJ1c2VyIjp7ImlkIjoiYWxpY2UiLCJmdWxsTmFtZSI6IkFsaWNlIENvb3BlciIsImVtYWlsIjoiYWxpY2VAamVua2lucy1jaS5vcmcifX19.H1iZAR2ajMeWRhh1VDdbqOtD7Wo0e0FZx8JDDNzphLu2DaLlxVRzBbhZ5TllvPx787kbNeK2tymFu_2Y_59qkq7YxZkrJctZTeiHVlTlHIxf2woBBggkIgoSvzNSsCcX73vjH5A5e54T5e8rUjF56XP05d5-WDvvheLo_Sqn4j19_lXkogCC2-JhDfc7sb8Xnw5PwYNZs29JYSSLOuUWm8UnD3AnBeFBhPfY2bR8-BjPXxdRWAyrZ-bz1CITfOm1xHZ-8NCGsfsUUGlcB_ijPVBt5T_29JWWFnougM1qZ_CEO56xu1572LMUmBYi8ynl75frzoSL_PvZYMXF47zcdg
|
||||
|
||||
JSON presentation of this token:
|
||||
|
||||
Header:
|
||||
|
||||
{"kid":"63fa164eda09463c8fee26688f8196fd","alg":"RS256","typ":"JWT"}
|
||||
|
||||
Claims:
|
||||
|
||||
{
|
||||
"name" : "Alice Cooper",
|
||||
"iss" : "blueocean-jwt:",
|
||||
"sub" : "alice",
|
||||
"exp" : 1470333405,
|
||||
"nbf" : 1470331575,
|
||||
"context" : {
|
||||
"user" : {
|
||||
"id" : "alice",
|
||||
"fullName" : "Alice Cooper",
|
||||
"email" : "alice@jenkins-ci.org"
|
||||
}
|
||||
},
|
||||
"jti" : "b0ef22b49b5c47bc858a8607d3f44d33",
|
||||
"iat" : 1470331605
|
||||
}
|
||||
|
||||
### Change expiry time
|
||||
|
||||
JWT tokens expires after 30 minutes (Default). exp claim header gives the time at which token expires. It is unix time
|
||||
in seconds. Default 30 minutes can be changed by sending expiryTimeInMins query parameter. This parameter value must be
|
||||
less than maximum expiry time allowed (8 hours or 480 minutes).
|
||||
|
||||
This parameter must be used carefully, it has security implications.
|
||||
|
||||
GET /jwt-auth/token?expiryTimeInMins=15
|
||||
|
||||
## Change maximum allowed expiry time
|
||||
|
||||
Use query maxExpiryTimeInMins to change default 8 hours maximum allowed expiry time.
|
||||
|
||||
This parameter must be used carefully, it has security implications.
|
||||
|
||||
GET /jwt-auth/token?maxExpiryTimeInMins=15
|
||||
|
||||
## Json web key (jwk) API
|
||||
|
||||
Client can call this API to get public key using the key id received as part of JWT header field 'kid'. This public key
|
||||
must be used to verify the JWT token.
|
||||
|
||||
GET /jwt-auth/jwks/bab71d7b184548a6b93480721d352ba1
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-type: application/json
|
||||
{
|
||||
"alg" : "RS256",
|
||||
"e" : "AQAB",
|
||||
"kty" : "RSA",
|
||||
"n" : "AMmWNNrmWzJXik7K7gmDkPumxqPzxc/JnxWsZ3CrhJGSO8hIgfsN6M5UHWSwkAoBHyNIaaPXhubWpcWCRewiI0U2Aw4jO3vzxNndRB9YaDPrrWDjvKBaqMC08IePPxmxXCj3ZS0QoEpf6rczdm2f9Of6Fro0TufXf2EYjLndBH7ep6iDQ4/TG7FkD7o39/GXuHAin0sz7atrPun3tlkuxllu5XNV+yW6WusrNIz3txyvKKEyQX950eW/6mMD0hS6yT7TbAwfrxkTnq4SiagCTllV+ct4wfnONDrao3WYgZnNgohsX/nEnYMHYq592n2WZW/i2+PNaFZlL2+3QgWO4qc=",
|
||||
"use" : "sig",
|
||||
"key_ops" : [
|
||||
"verify"
|
||||
],
|
||||
"kid" : "bab71d7b184548a6b93480721d352ba1"
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>io.jenkins.blueocean</groupId>
|
||||
<artifactId>blueocean-parent</artifactId>
|
||||
<version>1.0-alpha-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-jwt</artifactId>
|
||||
<packaging>hpi</packaging>
|
||||
|
||||
<name>BlueOcean :: JWT module</name>
|
||||
<url>https://wiki.jenkins-ci.org/display/JENKINS/Blue+Ocean+Plugin</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bitbucket.b_c</groupId>
|
||||
<artifactId>jose4j</artifactId>
|
||||
<version>0.5.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci.plugins</groupId>
|
||||
<artifactId>mailer</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,21 @@
|
|||
package io.jenkins.blueocean.auth.jwt;
|
||||
|
||||
import net.sf.json.JSONObject;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
|
||||
/**
|
||||
* Issuer of JSON Web Key.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @author Vivek Pandey
|
||||
* @see JwtAuthenticationService#getJwks(String)
|
||||
*/
|
||||
public abstract class JwkService {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return Gives JWK JSONObject
|
||||
*/
|
||||
@WebMethod(name = "")
|
||||
public abstract JSONObject getJwk();
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package io.jenkins.blueocean.auth.jwt;
|
||||
|
||||
import hudson.ExtensionPoint;
|
||||
import hudson.model.RootAction;
|
||||
import org.kohsuke.stapler.QueryParameter;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
import org.kohsuke.stapler.verb.GET;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* JWT endpoint resource. Provides functionality to get JWT token and also provides JWK endpoint to get
|
||||
* public key using keyId.
|
||||
*
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public abstract class JwtAuthenticationService implements RootAction, ExtensionPoint{
|
||||
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "jwt-auth";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gives JWT token for authenticated user. See https://tools.ietf.org/html/rfc7519.
|
||||
*
|
||||
* @param expiryTimeInMins token expiry time. Default 30 min.
|
||||
* @param maxExpiryTimeInMins max token expiry time. Default expiry time is 8 hours (480 mins)
|
||||
*
|
||||
* @return JWT if there is authenticated user or if anonymous user has at least READ permission, otherwise 401
|
||||
* error code is returned
|
||||
*
|
||||
* @see JwtToken
|
||||
*/
|
||||
@GET
|
||||
@WebMethod(name = "token")
|
||||
public abstract JwtToken getToken(@Nullable @QueryParameter("expiryTimeInMins") Integer expiryTimeInMins,
|
||||
@Nullable @QueryParameter("maxExpiryTimeInMins") Integer maxExpiryTimeInMins);
|
||||
|
||||
/**
|
||||
* Gives Json web key. See https://tools.ietf.org/html/rfc7517
|
||||
*
|
||||
* @param keyId keyId of the key
|
||||
*
|
||||
* @return JWK reponse
|
||||
*/
|
||||
@GET
|
||||
public abstract JwkService getJwks(String keyId);
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package io.jenkins.blueocean.auth.jwt;
|
||||
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import jenkins.security.RSADigitalSignatureConfidentialKey;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.jose4j.jws.AlgorithmIdentifiers;
|
||||
import org.jose4j.jws.JsonWebSignature;
|
||||
import org.jose4j.jwx.HeaderParameterNames;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.StaplerResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* JWT token
|
||||
*
|
||||
* Generates JWT token
|
||||
*
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public final class JwtToken implements HttpResponse{
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtToken.class);
|
||||
|
||||
public static final String X_BLUEOCEAN_JWT="X-BLUEOCEAN-JWT";
|
||||
private static final String DEFAULT_KEY_ID = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
/**
|
||||
* JWT header
|
||||
*/
|
||||
public final JSONObject header = new JSONObject();
|
||||
|
||||
|
||||
/**
|
||||
* JWT Claim
|
||||
*/
|
||||
public final JSONObject claim = new JSONObject();
|
||||
|
||||
|
||||
/**
|
||||
* Generates base64 representation of JWT token sign using "RS256" algorithm
|
||||
*
|
||||
* getHeader().toBase64UrlEncode() + "." + getClaim().toBase64UrlEncode() + "." + sign
|
||||
*
|
||||
*
|
||||
* @return base64 representation of JWT token
|
||||
*/
|
||||
public String sign(){
|
||||
for(JwtTokenDecorator decorator: JwtTokenDecorator.all()){
|
||||
decorator.decorate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* kid might have been set already by using {@link #header} or {@link JwtTokenDecorator}, if present use it
|
||||
* otherwise use the default kid
|
||||
*/
|
||||
String keyId = (String)header.get(HeaderParameterNames.KEY_ID);
|
||||
if(keyId == null){
|
||||
keyId = DEFAULT_KEY_ID;
|
||||
}
|
||||
|
||||
JwtRsaDigitalSignatureKey rsaDigitalSignatureConfidentialKey = new JwtRsaDigitalSignatureKey(keyId);
|
||||
|
||||
try {
|
||||
return rsaDigitalSignatureConfidentialKey.sign(claim);
|
||||
} catch (JoseException e) {
|
||||
String msg = "Failed to sign JWT token: "+e.getMessage();
|
||||
logger.error(msg);
|
||||
throw new ServiceException.UnexpectedErrorException(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
|
||||
rsp.setStatus(200);
|
||||
rsp.addHeader(X_BLUEOCEAN_JWT, sign());
|
||||
}
|
||||
|
||||
public final static class JwtRsaDigitalSignatureKey extends RSADigitalSignatureConfidentialKey{
|
||||
private final String id;
|
||||
|
||||
public JwtRsaDigitalSignatureKey(String id) {
|
||||
super("blueoceanJwt-"+id);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String sign(JSONObject claim) throws JoseException {
|
||||
JsonWebSignature jsonWebSignature = new JsonWebSignature();
|
||||
jsonWebSignature.setPayload(claim.toString());
|
||||
jsonWebSignature.setKey(getPrivateKey());
|
||||
jsonWebSignature.setKeyIdHeaderValue(id);
|
||||
jsonWebSignature.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
|
||||
jsonWebSignature.setHeader(HeaderParameterNames.TYPE, "JWT");
|
||||
|
||||
return jsonWebSignature.getCompactSerialization();
|
||||
}
|
||||
|
||||
public boolean exists() throws IOException {
|
||||
return super.load()!=null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package io.jenkins.blueocean.auth.jwt;
|
||||
|
||||
import hudson.ExtensionList;
|
||||
import hudson.ExtensionPoint;
|
||||
|
||||
/**
|
||||
* Participates in the creation of JwtToken
|
||||
*
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public abstract class JwtTokenDecorator implements ExtensionPoint {
|
||||
|
||||
|
||||
/** Decorates {@link JwtToken}
|
||||
*
|
||||
* @param token token to be decorated
|
||||
*
|
||||
* @return returns decorated token
|
||||
*/
|
||||
public abstract JwtToken decorate(JwtToken token);
|
||||
|
||||
/**
|
||||
* Returns all the registered {@link JwtTokenDecorator}s
|
||||
*/
|
||||
public static ExtensionList<JwtTokenDecorator> all() {
|
||||
return ExtensionList.lookup(JwtTokenDecorator.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package io.jenkins.blueocean.auth.jwt.impl;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import hudson.Extension;
|
||||
import hudson.Plugin;
|
||||
import hudson.model.User;
|
||||
import hudson.remoting.Base64;
|
||||
import hudson.tasks.Mailer;
|
||||
import io.jenkins.blueocean.auth.jwt.JwkService;
|
||||
import io.jenkins.blueocean.auth.jwt.JwtAuthenticationService;
|
||||
import io.jenkins.blueocean.auth.jwt.JwtToken;
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import jenkins.model.Jenkins;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.acegisecurity.Authentication;
|
||||
import org.kohsuke.stapler.QueryParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension
|
||||
public class JwtImpl extends JwtAuthenticationService {
|
||||
|
||||
private static int DEFAULT_EXPIRY_IN_SEC = 1800;
|
||||
private static int DEFAULT_MAX_EXPIRY_TIME_IN_MIN = 480;
|
||||
private static int DEFAULT_NOT_BEFORE_IN_SEC = 30;
|
||||
|
||||
@Override
|
||||
public JwtToken getToken(@Nullable @QueryParameter("expiryTimeInMins") Integer expiryTimeInMins, @Nullable @QueryParameter("maxExpiryTimeInMins") Integer maxExpiryTimeInMins) {
|
||||
String t = System.getProperty("EXPIRY_TIME_IN_MINS");
|
||||
long expiryTime=DEFAULT_EXPIRY_IN_SEC;
|
||||
if(t!= null){
|
||||
expiryTime = Integer.parseInt(t);
|
||||
}
|
||||
|
||||
int maxExpiryTime = DEFAULT_MAX_EXPIRY_TIME_IN_MIN;
|
||||
|
||||
t = System.getProperty("MAX_EXPIRY_TIME_IN_MINS");
|
||||
if(t!= null){
|
||||
maxExpiryTime = Integer.parseInt(t);
|
||||
}
|
||||
|
||||
if(maxExpiryTimeInMins != null){
|
||||
maxExpiryTime = maxExpiryTimeInMins;
|
||||
}
|
||||
if(expiryTimeInMins != null){
|
||||
if(expiryTimeInMins > maxExpiryTime) {
|
||||
throw new ServiceException.BadRequestExpception(
|
||||
String.format("expiryTimeInMins %s can't be greated than %s", expiryTimeInMins, maxExpiryTime));
|
||||
}
|
||||
expiryTime = expiryTimeInMins * 60;
|
||||
}
|
||||
|
||||
Authentication authentication = Jenkins.getInstance().getAuthentication();
|
||||
|
||||
if(authentication == null){
|
||||
throw new ServiceException.UnauthorizedException("Unauthorized: No login session found");
|
||||
}
|
||||
String userId = authentication.getName();
|
||||
|
||||
User user = User.get(userId, false, Collections.emptyMap());
|
||||
String email = null;
|
||||
String fullName = null;
|
||||
if(user != null) {
|
||||
fullName = user.getFullName();
|
||||
userId = user.getId();
|
||||
Mailer.UserProperty p = user.getProperty(Mailer.UserProperty.class);
|
||||
if(p!=null)
|
||||
email = p.getAddress();
|
||||
}
|
||||
Plugin plugin = Jenkins.getInstance().getPlugin("blueocean-jwt");
|
||||
String issuer = "blueocean-jwt:"+ ((plugin!=null) ? plugin.getWrapper().getVersion() : "");
|
||||
|
||||
JwtToken jwtToken = new JwtToken();
|
||||
jwtToken.claim.put("jti", UUID.randomUUID().toString().replace("-",""));
|
||||
jwtToken.claim.put("iss", issuer);
|
||||
jwtToken.claim.put("sub", userId);
|
||||
jwtToken.claim.put("name", fullName);
|
||||
long currentTime = System.currentTimeMillis()/1000;
|
||||
jwtToken.claim.put("iat", currentTime);
|
||||
jwtToken.claim.put("exp", currentTime+expiryTime);
|
||||
jwtToken.claim.put("nbf", currentTime - DEFAULT_NOT_BEFORE_IN_SEC);
|
||||
|
||||
//set claim
|
||||
JSONObject context = new JSONObject();
|
||||
JSONObject userObject = new JSONObject();
|
||||
userObject.put("id", userId);
|
||||
userObject.put("fullName", fullName);
|
||||
userObject.put("email", email);
|
||||
context.put("user", userObject);
|
||||
jwtToken.claim.put("context", context);
|
||||
|
||||
return jwtToken;
|
||||
}
|
||||
|
||||
public JwkFactory getJwks(String name) {
|
||||
if(name == null){
|
||||
throw new ServiceException.BadRequestExpception("keyId is required");
|
||||
}
|
||||
|
||||
return new JwkFactory(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIconFileName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "BlueOcean Jwt endpoint";
|
||||
}
|
||||
|
||||
public class JwkFactory extends JwkService {
|
||||
private final String keyId;
|
||||
|
||||
public JwkFactory(String keyId) {
|
||||
this.keyId = keyId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject getJwk() {
|
||||
JwtToken.JwtRsaDigitalSignatureKey key = new JwtToken.JwtRsaDigitalSignatureKey(keyId);
|
||||
try {
|
||||
if(!key.exists()){
|
||||
throw new ServiceException.NotFoundException(String.format("kid %s not found", keyId));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException.UnexpectedErrorException("Unexpected error: "+e.getMessage(), e);
|
||||
}
|
||||
RSAPublicKey publicKey = key.getPublicKey();
|
||||
JSONObject jwk = new JSONObject();
|
||||
jwk.put("kty", "RSA");
|
||||
jwk.put("alg","RS256");
|
||||
jwk.put("kid",keyId);
|
||||
jwk.put("use", "sig");
|
||||
jwk.put("key_ops", ImmutableList.of("verify"));
|
||||
jwk.put("n", Base64.encode(publicKey.getModulus().toByteArray()));
|
||||
jwk.put("e", Base64.encode(publicKey.getPublicExponent().toByteArray()));
|
||||
return jwk;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?jelly escape-by-default='true'?>
|
||||
<div>
|
||||
BlueOcean JWT plugin: Enables JWT based BlueOcean API authentication
|
||||
</div>
|
|
@ -0,0 +1,135 @@
|
|||
package io.jenkins.blueocean.auth.jwt.impl;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import hudson.model.User;
|
||||
import hudson.tasks.Mailer;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.jose4j.jwk.RsaJsonWebKey;
|
||||
import org.jose4j.jws.JsonWebSignature;
|
||||
import org.jose4j.jwt.JwtClaims;
|
||||
import org.jose4j.jwt.consumer.JwtConsumer;
|
||||
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
|
||||
import org.jose4j.jwx.JsonWebStructure;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public class JwtImplTest {
|
||||
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@Test
|
||||
public void getToken() throws Exception {
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
|
||||
User user = j.jenkins.getUser("alice");
|
||||
user.setFullName("Alice Cooper");
|
||||
user.addProperty(new Mailer.UserProperty("alice@jenkins-ci.org"));
|
||||
|
||||
JenkinsRule.WebClient webClient = j.createWebClient();
|
||||
|
||||
webClient.login("alice");
|
||||
|
||||
Page page = webClient.goTo("jwt-auth/token/", null);
|
||||
String token = page.getWebResponse().getResponseHeaderValue("X-BLUEOCEAN-JWT");
|
||||
|
||||
Assert.assertNotNull(token);
|
||||
|
||||
JsonWebStructure jsonWebStructure = JsonWebStructure.fromCompactSerialization(token);
|
||||
|
||||
Assert.assertTrue(jsonWebStructure instanceof JsonWebSignature);
|
||||
|
||||
JsonWebSignature jsw = (JsonWebSignature) jsonWebStructure;
|
||||
|
||||
System.out.println(token);
|
||||
System.out.println(jsw.toString());
|
||||
|
||||
|
||||
String kid = jsw.getHeader("kid");
|
||||
|
||||
Assert.assertNotNull(kid);
|
||||
|
||||
page = webClient.goTo("jwt-auth/jwks/"+kid+"/", "application/json");
|
||||
|
||||
// for(NameValuePair valuePair: page.getWebResponse().getResponseHeaders()){
|
||||
// System.out.println(valuePair);
|
||||
// }
|
||||
|
||||
JSONObject jsonObject = JSONObject.fromObject(page.getWebResponse().getContentAsString());
|
||||
System.out.println(jsonObject.toString());
|
||||
RsaJsonWebKey rsaJsonWebKey = new RsaJsonWebKey(jsonObject,null);
|
||||
|
||||
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
|
||||
.setRequireExpirationTime() // the JWT must have an expiration time
|
||||
.setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
|
||||
.setRequireSubject() // the JWT must have a subject claim
|
||||
.setVerificationKey(rsaJsonWebKey.getKey()) // verify the sign with the public key
|
||||
.build(); // create the JwtConsumer instance
|
||||
|
||||
JwtClaims claims = jwtConsumer.processToClaims(token);
|
||||
Assert.assertEquals("alice",claims.getSubject());
|
||||
|
||||
Map<String,Object> claimMap = claims.getClaimsMap();
|
||||
|
||||
Map<String,Object> context = (Map<String, Object>) claimMap.get("context");
|
||||
Map<String,String> userContext = (Map<String, String>) context.get("user");
|
||||
Assert.assertEquals("alice", userContext.get("id"));
|
||||
Assert.assertEquals("Alice Cooper", userContext.get("fullName"));
|
||||
Assert.assertEquals("alice@jenkins-ci.org", userContext.get("email"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymousUserToken() throws Exception{
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
|
||||
JenkinsRule.WebClient webClient = j.createWebClient();
|
||||
Page page = webClient.goTo("jwt-auth/token/", null);
|
||||
String token = page.getWebResponse().getResponseHeaderValue("X-BLUEOCEAN-JWT");
|
||||
|
||||
Assert.assertNotNull(token);
|
||||
|
||||
|
||||
JsonWebStructure jsonWebStructure = JsonWebStructure.fromCompactSerialization(token);
|
||||
|
||||
Assert.assertTrue(jsonWebStructure instanceof JsonWebSignature);
|
||||
|
||||
JsonWebSignature jsw = (JsonWebSignature) jsonWebStructure;
|
||||
|
||||
|
||||
String kid = jsw.getHeader("kid");
|
||||
|
||||
Assert.assertNotNull(kid);
|
||||
|
||||
page = webClient.goTo("jwt-auth/jwks/"+kid+"/", "application/json");
|
||||
|
||||
// for(NameValuePair valuePair: page.getWebResponse().getResponseHeaders()){
|
||||
// System.out.println(valuePair);
|
||||
// }
|
||||
|
||||
JSONObject jsonObject = JSONObject.fromObject(page.getWebResponse().getContentAsString());
|
||||
RsaJsonWebKey rsaJsonWebKey = new RsaJsonWebKey(jsonObject,null);
|
||||
|
||||
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
|
||||
.setRequireExpirationTime() // the JWT must have an expiration time
|
||||
.setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
|
||||
.setRequireSubject() // the JWT must have a subject claim
|
||||
.setVerificationKey(rsaJsonWebKey.getKey()) // verify the sign with the public key
|
||||
.build(); // create the JwtConsumer instance
|
||||
|
||||
JwtClaims claims = jwtConsumer.processToClaims(token);
|
||||
Assert.assertEquals("anonymous",claims.getSubject());
|
||||
|
||||
Map<String,Object> claimMap = claims.getClaimsMap();
|
||||
|
||||
Map<String,Object> context = (Map<String, Object>) claimMap.get("context");
|
||||
Map<String,String> userContext = (Map<String, String>) context.get("user");
|
||||
Assert.assertEquals("anonymous", userContext.get("id"));
|
||||
}
|
||||
}
|
|
@ -429,9 +429,10 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
WorkflowJob p = scheduleAndFindBranchProject(mp, "master");
|
||||
j.waitUntilNoActivity();
|
||||
|
||||
String token = getJwtToken(j.jenkins, "alice", "alice");
|
||||
Map m = new RequestBuilder(baseUrl)
|
||||
.put("/organizations/jenkins/pipelines/p/favorite")
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", true))
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -443,7 +444,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
List l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(1,l.size());
|
||||
|
@ -460,7 +461,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
m = new RequestBuilder(baseUrl)
|
||||
.put(getUrlFromHref(ref))
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", false))
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -472,7 +473,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(0,l.size());
|
||||
|
@ -480,7 +481,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("bob","bob")
|
||||
.jwtToken(getJwtToken(j.jenkins,"bob","bob"))
|
||||
.status(403)
|
||||
.build(String.class);
|
||||
}
|
||||
|
@ -503,9 +504,10 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
WorkflowJob p1 = scheduleAndFindBranchProject(mp, "feature2");
|
||||
|
||||
String token = getJwtToken(j.jenkins,"alice","alice");
|
||||
Map map = new RequestBuilder(baseUrl)
|
||||
.put("/organizations/jenkins/pipelines/p/branches/feature2/favorite")
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", true))
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -516,7 +518,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
List l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(1, l.size());
|
||||
|
@ -533,7 +535,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
map = new RequestBuilder(baseUrl)
|
||||
.put(getUrlFromHref(getHrefFromLinks((Map)l.get(0), "self")))
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", false))
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -544,7 +546,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(0, l.size());
|
||||
|
@ -552,7 +554,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("bob","bob")
|
||||
.jwtToken(getJwtToken(j.jenkins,"bob","bob"))
|
||||
.status(403)
|
||||
.build(String.class);
|
||||
|
||||
|
@ -582,9 +584,11 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
fup.toggleFavorite(mp.getFullName());
|
||||
user.save();
|
||||
|
||||
String token = getJwtToken(j.jenkins,"alice", "alice");
|
||||
|
||||
List l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(1, l.size());
|
||||
|
@ -603,7 +607,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
Map m = new RequestBuilder(baseUrl)
|
||||
.put(getUrlFromHref(getUrlFromHref(href)))
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", false))
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -614,7 +618,7 @@ public class MultiBranchTest extends PipelineBaseTest {
|
|||
|
||||
l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(0,l.size());
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.mashape.unirest.http.HttpResponse;
|
|||
import com.mashape.unirest.http.ObjectMapper;
|
||||
import com.mashape.unirest.http.Unirest;
|
||||
import com.mashape.unirest.http.exceptions.UnirestException;
|
||||
import com.mashape.unirest.request.GetRequest;
|
||||
import com.mashape.unirest.request.HttpRequest;
|
||||
import com.mashape.unirest.request.HttpRequestWithBody;
|
||||
import hudson.Util;
|
||||
|
@ -13,6 +14,7 @@ import hudson.model.Job;
|
|||
import hudson.model.Run;
|
||||
import io.jenkins.blueocean.commons.JsonConverter;
|
||||
import jenkins.branch.MultiBranchProject;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.jenkinsci.plugins.workflow.actions.ThreadNameAction;
|
||||
import org.jenkinsci.plugins.workflow.graph.FlowNode;
|
||||
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
|
||||
|
@ -34,6 +36,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.logging.LogManager;
|
||||
|
||||
import static io.jenkins.blueocean.auth.jwt.JwtToken.X_BLUEOCEAN_JWT;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
|
@ -45,6 +49,8 @@ public abstract class PipelineBaseTest{
|
|||
|
||||
protected String baseUrl;
|
||||
|
||||
protected String jwtToken;
|
||||
|
||||
protected String getContextPath(){
|
||||
return "blue/rest";
|
||||
}
|
||||
|
@ -56,6 +62,7 @@ public abstract class PipelineBaseTest{
|
|||
LogManager.getLogManager().readConfiguration(is);
|
||||
}
|
||||
this.baseUrl = j.jenkins.getRootUrl() + getContextPath();
|
||||
this.jwtToken = getJwtToken(j.jenkins);
|
||||
Unirest.setObjectMapper(new ObjectMapper() {
|
||||
public <T> T readValue(String value, Class<T> valueType) {
|
||||
try {
|
||||
|
@ -109,12 +116,14 @@ public abstract class PipelineBaseTest{
|
|||
if(HttpResponse.class.isAssignableFrom(type)){
|
||||
HttpResponse<String> response = Unirest.get(getBaseUrl(path)).header("Accept", accept)
|
||||
.header("Accept-Encoding","")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.asString();
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return (T) response;
|
||||
}
|
||||
|
||||
HttpResponse<T> response = Unirest.get(getBaseUrl(path)).header("Accept", accept).asObject(type);
|
||||
HttpResponse<T> response = Unirest.get(getBaseUrl(path)).header("Accept", accept)
|
||||
.header("Authorization", "Bearer "+jwtToken).asObject(type);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
} catch (UnirestException e) {
|
||||
|
@ -145,6 +154,7 @@ public abstract class PipelineBaseTest{
|
|||
try {
|
||||
HttpResponse<Map> response = Unirest.post(getBaseUrl(path))
|
||||
.header("Content-Type","application/json")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(Map.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -159,6 +169,7 @@ public abstract class PipelineBaseTest{
|
|||
HttpResponse<String> response = Unirest.post(getBaseUrl(path))
|
||||
.header("Content-Type",contentType)
|
||||
.header("Accept-Encoding","")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(String.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -179,6 +190,7 @@ public abstract class PipelineBaseTest{
|
|||
HttpResponse<Map> response = Unirest.put(getBaseUrl(path))
|
||||
.header("Content-Type","application/json")
|
||||
.header("Accept","application/json")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
//Unirest by default sets accept-encoding to gzip but stapler is sending malformed gzip value if
|
||||
// the response length is small (in this case its 20 chars).
|
||||
// Needs investigation in stapler to see whats going on there.
|
||||
|
@ -198,6 +210,7 @@ public abstract class PipelineBaseTest{
|
|||
try {
|
||||
HttpResponse<String> response = Unirest.put(getBaseUrl(path))
|
||||
.header("Content-Type",contentType)
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(String.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -215,6 +228,7 @@ public abstract class PipelineBaseTest{
|
|||
try {
|
||||
HttpResponse<Map> response = Unirest.patch(getBaseUrl(path))
|
||||
.header("Content-Type","application/json")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(Map.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -229,6 +243,7 @@ public abstract class PipelineBaseTest{
|
|||
try {
|
||||
HttpResponse<String> response = Unirest.patch(getBaseUrl(path))
|
||||
.header("Content-Type",contentType)
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(String.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -353,7 +368,7 @@ public abstract class PipelineBaseTest{
|
|||
public RequestBuilder request() {
|
||||
return new RequestBuilder(baseUrl);
|
||||
}
|
||||
public static class RequestBuilder {
|
||||
public class RequestBuilder {
|
||||
private String url;
|
||||
private String username;
|
||||
private String method;
|
||||
|
@ -362,6 +377,7 @@ public abstract class PipelineBaseTest{
|
|||
private String contentType = "application/json";
|
||||
private String baseUrl;
|
||||
private int expectedStatus = 200;
|
||||
private String token;
|
||||
|
||||
|
||||
private String getBaseUrl(String path){
|
||||
|
@ -394,6 +410,10 @@ public abstract class PipelineBaseTest{
|
|||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder jwtToken(String token){
|
||||
this.token = token;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder data(Map data) {
|
||||
this.data = data;
|
||||
|
@ -454,6 +474,11 @@ public abstract class PipelineBaseTest{
|
|||
|
||||
}
|
||||
request.header("Accept-Encoding","");
|
||||
if(token == null) {
|
||||
request.header("Authorization", "Bearer " + PipelineBaseTest.this.jwtToken);
|
||||
}else{
|
||||
request.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
|
||||
request.header("Content-Type", contentType);
|
||||
if(!Strings.isNullOrEmpty(username) && !Strings.isNullOrEmpty(password)){
|
||||
|
@ -471,4 +496,30 @@ public abstract class PipelineBaseTest{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getJwtToken(Jenkins jenkins) throws UnirestException {
|
||||
HttpResponse<String> response = Unirest.get(jenkins.getRootUrl()+"jwt-auth/token/").header("Accept", "*/*")
|
||||
.header("Accept-Encoding","").asString();
|
||||
|
||||
String token = response.getHeaders().getFirst(X_BLUEOCEAN_JWT);
|
||||
Assert.assertNotNull(token);
|
||||
//we do not validate it for test optimization and for the fact that there are separate
|
||||
// tests that test token generation and validation
|
||||
return token;
|
||||
}
|
||||
|
||||
public static String getJwtToken(Jenkins jenkins, String username, String password) throws UnirestException {
|
||||
GetRequest request = Unirest.get(jenkins.getRootUrl()+"jwt-auth/token/").header("Accept", "*/*")
|
||||
.header("Accept-Encoding","");
|
||||
if(username!= null && password!= null){
|
||||
request.basicAuth(username,password);
|
||||
}
|
||||
|
||||
HttpResponse<String> response = request.asString();
|
||||
String token = response.getHeaders().getFirst(X_BLUEOCEAN_JWT);
|
||||
Assert.assertNotNull(token);
|
||||
//we do not validate it for test optimization and for the fact that there are separate
|
||||
// tests that test token generation and validation
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,10 @@
|
|||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-jwt</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-rest-impl</artifactId>
|
||||
|
@ -107,12 +111,14 @@
|
|||
//
|
||||
linkHPI('blueocean-web');
|
||||
linkHPI('blueocean-dashboard');
|
||||
linkHPI('blueocean-personalization')
|
||||
linkHPI('blueocean-personalization');
|
||||
linkHPI('blueocean-rest');
|
||||
linkHPI('blueocean-commons');
|
||||
linkHPI('blueocean-jwt');
|
||||
linkHPI('blueocean-rest-impl');
|
||||
linkHPI('blueocean-pipeline-api-impl')
|
||||
linkHPI('blueocean-analytics-tools')
|
||||
linkHPI('blueocean-pipeline-api-impl');
|
||||
linkHPI('blueocean-analytics-tools');
|
||||
|
||||
</source>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
<artifactId>scm-api</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-jwt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test plugins -->
|
||||
<dependency>
|
||||
|
|
|
@ -4,17 +4,23 @@ import com.google.inject.Binder;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.Module;
|
||||
import hudson.Extension;
|
||||
import hudson.model.RootAction;
|
||||
import hudson.model.UnprotectedRootAction;
|
||||
import io.jenkins.blueocean.BlueOceanUI;
|
||||
import org.acegisecurity.context.SecurityContext;
|
||||
import org.acegisecurity.context.SecurityContextHolder;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerProxy;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
/**
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
@Extension
|
||||
public class BlueOceanRootAction implements RootAction, StaplerProxy {
|
||||
public class BlueOceanRootAction implements UnprotectedRootAction, StaplerProxy {
|
||||
private static final String URL_BASE="blue";
|
||||
|
||||
private final boolean disableJWT = Boolean.getBoolean("DISABLE_BLUEOCEAN_JWT_AUTHENTICATION");
|
||||
|
||||
@Inject
|
||||
private BlueOceanUI app;
|
||||
|
||||
|
@ -38,6 +44,14 @@ public class BlueOceanRootAction implements RootAction, StaplerProxy {
|
|||
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
|
||||
StaplerRequest request = Stapler.getCurrentRequest();
|
||||
|
||||
if(!disableJWT && request.getOriginalRestOfPath().startsWith("/rest/")) {
|
||||
JwtAuthenticationToken tokenAuthentication = new JwtAuthenticationToken(request);
|
||||
SecurityContext holder = SecurityContextHolder.getContext();
|
||||
holder.setAuthentication(tokenAuthentication);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
package io.jenkins.blueocean.service.embedded;
|
||||
|
||||
import hudson.model.User;
|
||||
import io.jenkins.blueocean.auth.jwt.JwtToken;
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.acegisecurity.GrantedAuthority;
|
||||
import org.acegisecurity.GrantedAuthorityImpl;
|
||||
import org.acegisecurity.providers.AbstractAuthenticationToken;
|
||||
import org.acegisecurity.userdetails.UserDetails;
|
||||
import org.jose4j.jwt.JwtClaims;
|
||||
import org.jose4j.jwt.MalformedClaimException;
|
||||
import org.jose4j.jwt.NumericDate;
|
||||
import org.jose4j.jwt.consumer.InvalidJwtException;
|
||||
import org.jose4j.jwt.consumer.JwtConsumer;
|
||||
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
|
||||
import org.jose4j.jwt.consumer.JwtContext;
|
||||
import org.jose4j.jwx.JsonWebStructure;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA256;
|
||||
|
||||
/**
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
public final class JwtAuthenticationToken extends AbstractAuthenticationToken{
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationToken.class);
|
||||
|
||||
|
||||
private final String name;
|
||||
private final GrantedAuthority[] grantedAuthorities;
|
||||
|
||||
public JwtAuthenticationToken(StaplerRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if(authHeader == null || !authHeader.startsWith("Bearer")){
|
||||
throw new ServiceException.UnauthorizedException("JWT token not found");
|
||||
}
|
||||
String token = authHeader.substring("Bearer ".length());
|
||||
try {
|
||||
JsonWebStructure jws = JsonWebStructure.fromCompactSerialization(token);
|
||||
String alg = jws.getAlgorithmHeaderValue();
|
||||
if(alg == null || !alg.equals(RSA_USING_SHA256)){
|
||||
logger.error(String.format("Invalid JWT token: unsupported algorithm in header, found %s, expected %s", alg, RSA_USING_SHA256));
|
||||
throw new ServiceException.UnauthorizedException("Invalid JWT token");
|
||||
}
|
||||
|
||||
String kid = jws.getKeyIdHeaderValue();
|
||||
|
||||
if(kid == null){
|
||||
logger.error("Invalid JWT token: missing kid");
|
||||
throw new ServiceException.UnauthorizedException("Invalid JWT token");
|
||||
}
|
||||
|
||||
JwtToken.JwtRsaDigitalSignatureKey key = new JwtToken.JwtRsaDigitalSignatureKey(kid);
|
||||
try {
|
||||
if(!key.exists()){
|
||||
throw new ServiceException.NotFoundException(String.format("kid %s not found", kid));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error(String.format("Error reading RSA key for id %s: %s",kid,e.getMessage()),e);
|
||||
throw new ServiceException.UnexpectedErrorException("Unexpected error: "+e.getMessage(), e);
|
||||
}
|
||||
|
||||
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
|
||||
.setRequireExpirationTime() // the JWT must have an expiration time
|
||||
.setRequireJwtId()
|
||||
.setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
|
||||
.setRequireSubject() // the JWT must have a subject claim
|
||||
.setVerificationKey(key.getPublicKey()) // verify the sign with the public key
|
||||
.build(); // create the JwtConsumer instance
|
||||
|
||||
try {
|
||||
JwtContext context = jwtConsumer.process(token);
|
||||
JwtClaims claims = context.getJwtClaims();
|
||||
|
||||
//check if token expired
|
||||
NumericDate expirationTime = claims.getExpirationTime();
|
||||
if (expirationTime.isBefore(NumericDate.now())){
|
||||
throw new ServiceException.UnauthorizedException("Invalid JWT token: expired");
|
||||
}
|
||||
String subject = claims.getSubject();
|
||||
|
||||
if(!subject.equals("anonymous")) { //if anonymous, we don't look in user db
|
||||
User user = User.get(subject, false, Collections.emptyMap());
|
||||
if (user == null) {
|
||||
throw new ServiceException.UnauthorizedException("Invalid JWT token: subject " + subject + " not found");
|
||||
}
|
||||
UserDetails d = Jenkins.getInstance().getSecurityRealm().loadUserByUsername(user.getId());
|
||||
this.grantedAuthorities = d.getAuthorities();
|
||||
}else{
|
||||
this.grantedAuthorities = new GrantedAuthority[] {new GrantedAuthorityImpl("anonymous")};
|
||||
}
|
||||
this.name = subject;
|
||||
|
||||
} catch (InvalidJwtException e) {
|
||||
logger.error("Invalid JWT token: "+e.getMessage(), e);
|
||||
throw new ServiceException.UnauthorizedException("Invalid JWT token");
|
||||
} catch (MalformedClaimException e) {
|
||||
logger.error(String.format("Error reading sub header for token %s",jws.getPayload()),e);
|
||||
throw new ServiceException.UnauthorizedException("Invalid JWT token: malformed claim");
|
||||
}
|
||||
} catch (JoseException e) {
|
||||
logger.error("Error parsing JWT token: "+e.getMessage(), e);
|
||||
throw new ServiceException.UnexpectedErrorException("Unexpected error");
|
||||
}
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrantedAuthority[] getAuthorities() {
|
||||
return grantedAuthorities;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ import io.jenkins.blueocean.rest.model.BlueFavoriteContainer;
|
|||
import io.jenkins.blueocean.rest.model.BlueUser;
|
||||
import jenkins.model.Jenkins;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* {@link BlueUser} implementation backed by in-memory {@link User}
|
||||
*
|
||||
|
@ -43,7 +45,16 @@ public class UserImpl extends BlueUser {
|
|||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
if (!user.hasPermission(Jenkins.ADMINISTER)) return null;
|
||||
String name = Jenkins.getAuthentication().getName();
|
||||
if(name.equals("anonymous") || user.getId().equals("anonymous")){
|
||||
return null;
|
||||
}else{
|
||||
User user = User.get(name, false, Collections.EMPTY_MAP);
|
||||
if(user == null){
|
||||
return null;
|
||||
}
|
||||
if (!user.hasPermission(Jenkins.ADMINISTER)) return null;
|
||||
}
|
||||
|
||||
Mailer.UserProperty p = user.getProperty(Mailer.UserProperty.class);
|
||||
return p != null ? p.getAddress() : null;
|
||||
|
|
|
@ -6,11 +6,14 @@ import com.mashape.unirest.http.HttpResponse;
|
|||
import com.mashape.unirest.http.ObjectMapper;
|
||||
import com.mashape.unirest.http.Unirest;
|
||||
import com.mashape.unirest.http.exceptions.UnirestException;
|
||||
import com.mashape.unirest.request.GetRequest;
|
||||
import com.mashape.unirest.request.HttpRequest;
|
||||
import com.mashape.unirest.request.HttpRequestWithBody;
|
||||
import hudson.model.Job;
|
||||
import hudson.model.Run;
|
||||
import io.jenkins.blueocean.commons.JsonConverter;
|
||||
import jenkins.model.Jenkins;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
|
@ -26,6 +29,8 @@ import java.util.Date;
|
|||
import java.util.Map;
|
||||
import java.util.logging.LogManager;
|
||||
|
||||
import static io.jenkins.blueocean.auth.jwt.JwtToken.X_BLUEOCEAN_JWT;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
|
@ -41,6 +46,8 @@ public abstract class BaseTest {
|
|||
return "blue/rest";
|
||||
}
|
||||
|
||||
protected String jwtToken;
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
if(System.getProperty("DISABLE_HTTP_HEADER_TRACE") == null) {
|
||||
|
@ -48,6 +55,7 @@ public abstract class BaseTest {
|
|||
LogManager.getLogManager().readConfiguration(is);
|
||||
}
|
||||
this.baseUrl = j.jenkins.getRootUrl() + getContextPath();
|
||||
this.jwtToken = getJwtToken(j.jenkins);
|
||||
Unirest.setObjectMapper(new ObjectMapper() {
|
||||
public <T> T readValue(String value, Class<T> valueType) {
|
||||
try {
|
||||
|
@ -101,12 +109,13 @@ public abstract class BaseTest {
|
|||
if(HttpResponse.class.isAssignableFrom(type)){
|
||||
HttpResponse<String> response = Unirest.get(getBaseUrl(path)).header("Accept", accept)
|
||||
.header("Accept-Encoding","")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.asString();
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return (T) response;
|
||||
}
|
||||
|
||||
HttpResponse<T> response = Unirest.get(getBaseUrl(path)).header("Accept", accept).asObject(type);
|
||||
HttpResponse<T> response = Unirest.get(getBaseUrl(path)).header("Accept", accept).header("Authorization", "Bearer "+jwtToken).asObject(type);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
} catch (UnirestException e) {
|
||||
|
@ -118,7 +127,8 @@ public abstract class BaseTest {
|
|||
protected Map delete(String path){
|
||||
assert path.startsWith("/");
|
||||
try {
|
||||
HttpResponse<Map> response = Unirest.delete(getBaseUrl(path)).asObject(Map.class);
|
||||
HttpResponse<Map> response = Unirest.delete(getBaseUrl(path))
|
||||
.header("Authorization", "Bearer "+jwtToken).asObject(Map.class);
|
||||
Assert.assertEquals(200, response.getStatus());
|
||||
return response.getBody();
|
||||
} catch (UnirestException e) {
|
||||
|
@ -137,6 +147,7 @@ public abstract class BaseTest {
|
|||
try {
|
||||
HttpResponse<Map> response = Unirest.post(getBaseUrl(path))
|
||||
.header("Content-Type","application/json")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(Map.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -151,6 +162,7 @@ public abstract class BaseTest {
|
|||
HttpResponse<String> response = Unirest.post(getBaseUrl(path))
|
||||
.header("Content-Type",contentType)
|
||||
.header("Accept-Encoding","")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(String.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -171,6 +183,7 @@ public abstract class BaseTest {
|
|||
HttpResponse<Map> response = Unirest.put(getBaseUrl(path))
|
||||
.header("Content-Type","application/json")
|
||||
.header("Accept","application/json")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
//Unirest by default sets accept-encoding to gzip but stapler is sending malformed gzip value if
|
||||
// the response length is small (in this case its 20 chars).
|
||||
// Needs investigation in stapler to see whats going on there.
|
||||
|
@ -190,6 +203,7 @@ public abstract class BaseTest {
|
|||
try {
|
||||
HttpResponse<String> response = Unirest.put(getBaseUrl(path))
|
||||
.header("Content-Type",contentType)
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(String.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -207,6 +221,7 @@ public abstract class BaseTest {
|
|||
try {
|
||||
HttpResponse<Map> response = Unirest.patch(getBaseUrl(path))
|
||||
.header("Content-Type","application/json")
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(Map.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -221,6 +236,7 @@ public abstract class BaseTest {
|
|||
try {
|
||||
HttpResponse<String> response = Unirest.patch(getBaseUrl(path))
|
||||
.header("Content-Type",contentType)
|
||||
.header("Authorization", "Bearer "+jwtToken)
|
||||
.body(body).asObject(String.class);
|
||||
Assert.assertEquals(expectedStatus, response.getStatus());
|
||||
return response.getBody();
|
||||
|
@ -300,7 +316,9 @@ public abstract class BaseTest {
|
|||
public RequestBuilder request() {
|
||||
return new RequestBuilder(baseUrl);
|
||||
}
|
||||
public static class RequestBuilder {
|
||||
|
||||
|
||||
public class RequestBuilder {
|
||||
private String url;
|
||||
private String username;
|
||||
private String method;
|
||||
|
@ -310,6 +328,7 @@ public abstract class BaseTest {
|
|||
private String baseUrl;
|
||||
private int expectedStatus = 200;
|
||||
|
||||
private String token;
|
||||
|
||||
private String getBaseUrl(String path){
|
||||
return baseUrl + path;
|
||||
|
@ -341,6 +360,11 @@ public abstract class BaseTest {
|
|||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder jwtToken(String token){
|
||||
this.token = token;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public RequestBuilder data(Map data) {
|
||||
this.data = data;
|
||||
|
@ -358,6 +382,11 @@ public abstract class BaseTest {
|
|||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder patch(String url) {
|
||||
this.url = url;
|
||||
this.method = "PATCH";
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder get(String url) {
|
||||
this.url = url;
|
||||
|
@ -387,6 +416,9 @@ public abstract class BaseTest {
|
|||
case "PUT":
|
||||
request = Unirest.put(getBaseUrl(url));
|
||||
break;
|
||||
case "PATCH":
|
||||
request = Unirest.patch(getBaseUrl(url));
|
||||
break;
|
||||
case "POST":
|
||||
request = Unirest.post(getBaseUrl(url));
|
||||
break;
|
||||
|
@ -407,6 +439,12 @@ public abstract class BaseTest {
|
|||
request.basicAuth(username, password);
|
||||
}
|
||||
|
||||
if(token == null) {
|
||||
request.header("Authorization", "Bearer " + BaseTest.this.jwtToken);
|
||||
}else{
|
||||
request.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
|
||||
if(request instanceof HttpRequestWithBody && data != null) {
|
||||
((HttpRequestWithBody)request).body(data);
|
||||
}
|
||||
|
@ -418,4 +456,39 @@ public abstract class BaseTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getJwtToken(Jenkins jenkins) throws UnirestException {
|
||||
HttpResponse<String> response = Unirest.get(jenkins.getRootUrl()+"jwt-auth/token/").header("Accept", "*/*")
|
||||
.header("Accept-Encoding","").asString();
|
||||
|
||||
String token = response.getHeaders().getFirst(X_BLUEOCEAN_JWT);
|
||||
Assert.assertNotNull(token);
|
||||
//we do not validate it for test optimization and for the fact that there are separate
|
||||
// tests that test token generation and validation
|
||||
return token;
|
||||
}
|
||||
|
||||
public static String getJwtToken(Jenkins jenkins, String username, String password) throws UnirestException {
|
||||
GetRequest request = Unirest.get(jenkins.getRootUrl()+"jwt-auth/token/").header("Accept", "*/*")
|
||||
.header("Accept-Encoding","");
|
||||
if(username!= null && password!= null){
|
||||
request.basicAuth(username,password);
|
||||
}
|
||||
|
||||
HttpResponse<String> response = request.asString();
|
||||
String token = response.getHeaders().getFirst(X_BLUEOCEAN_JWT);
|
||||
Assert.assertNotNull(token);
|
||||
//we do not validate it for test optimization and for the fact that there are separate
|
||||
// tests that test token generation and validation
|
||||
int i = token.indexOf('.');
|
||||
Assert.assertTrue(i > 0);
|
||||
|
||||
int j = token.lastIndexOf(".");
|
||||
Assert.assertTrue(j > 0);
|
||||
String claim = new String(org.jose4j.base64url.Base64.decode(token.substring(i+1, j)));
|
||||
Map u = JSONObject.fromObject(claim);
|
||||
Assert.assertEquals(username,u.get("sub"));
|
||||
return token;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.jenkins.blueocean.service.embedded;
|
||||
|
||||
import com.mashape.unirest.http.exceptions.UnirestException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
|
@ -11,12 +12,12 @@ import java.util.Map;
|
|||
*/
|
||||
public class OrganizationApiTest extends BaseTest {
|
||||
@Test
|
||||
public void organizationUsers() {
|
||||
public void organizationUsers() throws UnirestException {
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
hudson.model.User alice = j.jenkins.getUser("alice");
|
||||
alice.setFullName("Alice Cooper");
|
||||
|
||||
List users = request().authAlice().get("/organizations/jenkins/users/").build(List.class);
|
||||
List users = request().jwtToken(getJwtToken(j.jenkins,"alice", "alice")).get("/organizations/jenkins/users/").build(List.class);
|
||||
|
||||
Assert.assertEquals(users.size(), 1);
|
||||
Assert.assertEquals(((Map)users.get(0)).get("id"), "alice");
|
||||
|
|
|
@ -6,6 +6,7 @@ import hudson.model.FreeStyleProject;
|
|||
import hudson.model.Project;
|
||||
import hudson.model.User;
|
||||
import hudson.tasks.Mailer;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.MockFolder;
|
||||
|
@ -72,19 +73,52 @@ public class ProfileApiTest extends BaseTest{
|
|||
@Test
|
||||
public void patchMimeFailTest() throws Exception {
|
||||
User system = j.jenkins.getUser("SYSTEM");
|
||||
patch("/users/"+system.getId(), "","text/plain", 415);
|
||||
|
||||
new RequestBuilder(baseUrl)
|
||||
.contentType("text/plain")
|
||||
.status(415)
|
||||
.patch("/users/"+system.getId())
|
||||
.build(Map.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUserDetailsTest() throws Exception {
|
||||
hudson.model.User user = j.jenkins.getUser("alice");
|
||||
user.setFullName("Alice Cooper");
|
||||
user.addProperty(new Mailer.UserProperty("alice@jenkins-ci.org"));
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
hudson.model.User alice = j.jenkins.getUser("alice");
|
||||
alice.setFullName("Alice Cooper");
|
||||
alice.addProperty(new Mailer.UserProperty("alice@jenkins-ci.org"));
|
||||
|
||||
Map response = get("/users/"+user.getId());
|
||||
Assert.assertEquals(user.getId(), response.get("id"));
|
||||
Assert.assertEquals(user.getFullName(), response.get("fullName"));
|
||||
Assert.assertEquals("alice@jenkins-ci.org", response.get("email"));
|
||||
hudson.model.User bob = j.jenkins.getUser("bob");
|
||||
|
||||
bob.setFullName("Bob Smith");
|
||||
bob.addProperty(new Mailer.UserProperty("bob@jenkins-ci.org"));
|
||||
|
||||
//Call is made as anonymous user, email should be null
|
||||
Map response = get("/users/"+alice.getId());
|
||||
Assert.assertEquals(alice.getId(), response.get("id"));
|
||||
Assert.assertEquals(alice.getFullName(), response.get("fullName"));
|
||||
Assert.assertNull(response.get("email"));
|
||||
|
||||
//make a request on bob's behalf to get alice's user details, should get null email
|
||||
Map r = new RequestBuilder(baseUrl)
|
||||
.status(200)
|
||||
.jwtToken(getJwtToken(j.jenkins,"bob", "bob"))
|
||||
.get("/users/"+alice.getId()).build(Map.class);
|
||||
|
||||
Assert.assertEquals(alice.getId(), r.get("id"));
|
||||
Assert.assertEquals(alice.getFullName(), r.get("fullName"));
|
||||
Assert.assertTrue(bob.hasPermission(Jenkins.ADMINISTER));
|
||||
//bob is admin so can see alice email
|
||||
Assert.assertEquals("alice@jenkins-ci.org",r.get("email"));
|
||||
|
||||
r = new RequestBuilder(baseUrl)
|
||||
.status(200)
|
||||
.jwtToken(getJwtToken(j.jenkins,"alice", "alice"))
|
||||
.get("/users/"+alice.getId()).build(Map.class);
|
||||
|
||||
Assert.assertEquals(alice.getId(), r.get("id"));
|
||||
Assert.assertEquals(alice.getFullName(), r.get("fullName"));
|
||||
Assert.assertEquals("alice@jenkins-ci.org",r.get("email"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -95,17 +129,17 @@ public class ProfileApiTest extends BaseTest{
|
|||
|
||||
Project p = j.createFreeStyleProject("pipeline1");
|
||||
|
||||
|
||||
String token = getJwtToken(j.jenkins,"alice", "alice");
|
||||
Map map = new RequestBuilder(baseUrl)
|
||||
.put("/organizations/jenkins/pipelines/pipeline1/favorite")
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", true))
|
||||
.build(Map.class);
|
||||
|
||||
validatePipeline(p, (Map) map.get("item"));
|
||||
List l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(1, l.size());
|
||||
|
@ -117,7 +151,7 @@ public class ProfileApiTest extends BaseTest{
|
|||
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/pipeline1/favorite/", href);
|
||||
map = new RequestBuilder(baseUrl)
|
||||
.put(href.substring("/blue/rest".length()))
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", false))
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -125,15 +159,14 @@ public class ProfileApiTest extends BaseTest{
|
|||
|
||||
l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(0, l.size());
|
||||
|
||||
|
||||
new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("bob","bob")
|
||||
.jwtToken(getJwtToken(j.jenkins,"bob","bob"))
|
||||
.status(403)
|
||||
.build(String.class);
|
||||
|
||||
|
@ -148,16 +181,17 @@ public class ProfileApiTest extends BaseTest{
|
|||
MockFolder folder1 = j.createFolder("folder1");
|
||||
Project p = folder1.createProject(FreeStyleProject.class, "pipeline1");
|
||||
|
||||
String token = getJwtToken(j.jenkins,"alice", "alice");
|
||||
Map map = new RequestBuilder(baseUrl)
|
||||
.put("/organizations/jenkins/pipelines/folder1/pipelines/pipeline1/favorite/")
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", true))
|
||||
.build(Map.class);
|
||||
|
||||
validatePipeline(p, (Map) map.get("item"));
|
||||
List l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(1, l.size());
|
||||
|
@ -171,7 +205,7 @@ public class ProfileApiTest extends BaseTest{
|
|||
|
||||
map = new RequestBuilder(baseUrl)
|
||||
.put(href.substring("/blue/rest".length()))
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", false))
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -179,7 +213,7 @@ public class ProfileApiTest extends BaseTest{
|
|||
|
||||
l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(0, l.size());
|
||||
|
@ -187,14 +221,14 @@ public class ProfileApiTest extends BaseTest{
|
|||
|
||||
map = new RequestBuilder(baseUrl)
|
||||
.put("/organizations/jenkins/pipelines/folder1/favorite/")
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", true))
|
||||
.build(Map.class);
|
||||
|
||||
validateFolder(folder1, (Map) map.get("item"));
|
||||
l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(1, l.size());
|
||||
|
@ -208,7 +242,7 @@ public class ProfileApiTest extends BaseTest{
|
|||
|
||||
map = new RequestBuilder(baseUrl)
|
||||
.put(href.substring("/blue/rest".length()))
|
||||
.auth("alice", "alice")
|
||||
.jwtToken(token)
|
||||
.data(ImmutableMap.of("favorite", false))
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -216,7 +250,7 @@ public class ProfileApiTest extends BaseTest{
|
|||
|
||||
l = new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.build(List.class);
|
||||
|
||||
Assert.assertEquals(0, l.size());
|
||||
|
@ -225,7 +259,7 @@ public class ProfileApiTest extends BaseTest{
|
|||
|
||||
new RequestBuilder(baseUrl)
|
||||
.get("/users/"+user.getId()+"/favorites/")
|
||||
.auth("bob","bob")
|
||||
.jwtToken(getJwtToken(j.jenkins,"bob","bob"))
|
||||
.status(403)
|
||||
.build(String.class);
|
||||
|
||||
|
@ -262,9 +296,11 @@ public class ProfileApiTest extends BaseTest{
|
|||
user.setFullName("Alice Cooper");
|
||||
user.addProperty(new Mailer.UserProperty("alice@jenkins-ci.org"));
|
||||
|
||||
String token = getJwtToken(j.jenkins,"alice", "alice");
|
||||
|
||||
Map u = new RequestBuilder(baseUrl)
|
||||
.get("/organizations/jenkins/user/")
|
||||
.auth("alice","alice")
|
||||
.jwtToken(token)
|
||||
.status(200)
|
||||
.build(Map.class);
|
||||
|
||||
|
@ -285,10 +321,11 @@ public class ProfileApiTest extends BaseTest{
|
|||
user1.setFullName("Bob Cooper");
|
||||
user1.addProperty(new Mailer.UserProperty("bob@jenkins-ci.org"));
|
||||
|
||||
new RequestBuilder(baseUrl)
|
||||
Map u = new RequestBuilder(baseUrl)
|
||||
.get("/organizations/jenkins/user/")
|
||||
.status(404)
|
||||
.build(Map.class);
|
||||
|
||||
Assert.assertEquals("anonymous",u.get("id"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
- [Media Type](#media-type)
|
||||
- [Date Format](#date-format)
|
||||
- [Crumbs](#crumbs)
|
||||
- [Security](#security)
|
||||
- [Navigability](#navigability)
|
||||
- [Links](#links)
|
||||
- [Resource discovery](#resource-discovery)
|
||||
|
@ -103,7 +104,20 @@ All date formats are in ISO 8601 format
|
|||
|
||||
Jenkins usually requires a "crumb" with posted requests to prevent request forgery and other shenanigans.
|
||||
To avoid needing a crumb to POST data, the header `Content-Type: application/json` *must* be used.
|
||||
|
||||
|
||||
# Security
|
||||
|
||||
BlueOcean REST APIs requires JWT token for authentication. JWT APIs are provided by blueocean-jwt plugin. See
|
||||
[JWT APIs](../blueocean-jwt/README.md) to get JWT token and to get public key needed to verify the claims.
|
||||
|
||||
JWT token must be sent as bearer token as value of HTTP 'Authorization' header:
|
||||
|
||||
curl -H 'Authorization: Bearer eyJraWQ...' http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/
|
||||
|
||||
To disable JWT authentication use DISABLE_BLUEOCEAN_JWT_AUTHENTICATION=true system property.
|
||||
|
||||
mvn hpi:run -DDISABLE_BLUEOCEAN_JWT_AUTHENTICATION=true
|
||||
|
||||
# Navigability
|
||||
|
||||
## Links
|
||||
|
|
8
pom.xml
8
pom.xml
|
@ -106,6 +106,7 @@
|
|||
<module>blueocean-personalization</module>
|
||||
<module>blueocean-plugin</module>
|
||||
<module>blueocean-analytics-tools</module>
|
||||
<module>blueocean-jwt</module>
|
||||
</modules>
|
||||
|
||||
<repositories>
|
||||
|
@ -185,6 +186,13 @@
|
|||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-jwt</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- 3rd party dependencies -->
|
||||
|
||||
<dependency>
|
||||
|
|
Loading…
Reference in New Issue