Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
xxadev
jenkins
提交
b3f16489
J
jenkins
项目概览
xxadev
/
jenkins
与 Fork 源项目一致
从无法访问的项目Fork
通知
3
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
J
jenkins
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
b3f16489
编写于
9月 29, 2015
作者:
O
Oleg Nenashev
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
[FIXED SECURITY-200] - Do not expose Api tokens to other users by default
System property can be used to restore the original behavior
上级
f53802bb
变更
3
显示空白变更内容
内联
并排
Showing
3 changed file
with
142 addition
and
26 deletion
+142
-26
core/src/main/java/jenkins/security/ApiTokenProperty.java
core/src/main/java/jenkins/security/ApiTokenProperty.java
+63
-2
core/src/main/resources/jenkins/security/Messages.properties
core/src/main/resources/jenkins/security/Messages.properties
+3
-1
test/src/test/java/jenkins/security/ApiTokenPropertyTest.java
.../src/test/java/jenkins/security/ApiTokenPropertyTest.java
+76
-23
未找到文件。
core/src/main/java/jenkins/security/ApiTokenProperty.java
浏览文件 @
b3f16489
...
@@ -29,6 +29,7 @@ import hudson.model.Descriptor.FormException;
...
@@ -29,6 +29,7 @@ import hudson.model.Descriptor.FormException;
import
hudson.model.User
;
import
hudson.model.User
;
import
hudson.model.UserProperty
;
import
hudson.model.UserProperty
;
import
hudson.model.UserPropertyDescriptor
;
import
hudson.model.UserPropertyDescriptor
;
import
hudson.security.ACL
;
import
hudson.util.HttpResponses
;
import
hudson.util.HttpResponses
;
import
hudson.util.Secret
;
import
hudson.util.Secret
;
import
jenkins.model.Jenkins
;
import
jenkins.model.Jenkins
;
...
@@ -41,6 +42,10 @@ import org.kohsuke.stapler.StaplerResponse;
...
@@ -41,6 +42,10 @@ import org.kohsuke.stapler.StaplerResponse;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.security.SecureRandom
;
import
java.security.SecureRandom
;
import
javax.annotation.Nonnull
;
import
org.apache.commons.lang.StringUtils
;
import
org.kohsuke.accmod.Restricted
;
import
org.kohsuke.accmod.restrictions.NoExternalUse
;
/**
/**
* Remembers the API token for this user, that can be used like a password to login.
* Remembers the API token for this user, that can be used like a password to login.
...
@@ -53,6 +58,16 @@ import java.security.SecureRandom;
...
@@ -53,6 +58,16 @@ import java.security.SecureRandom;
public
class
ApiTokenProperty
extends
UserProperty
{
public
class
ApiTokenProperty
extends
UserProperty
{
private
volatile
Secret
apiToken
;
private
volatile
Secret
apiToken
;
/**
* If enabled, shows API tokens to users with {@link Jenkins#ADMINISTER) permissions.
* Disabled by default due to the security reasons.
* If enabled, it restores the original Jenkins behavior (SECURITY-200).
* @since TODO
*/
private
static
final
boolean
SHOW_TOKEN_TO_ADMINS
=
Boolean
.
getBoolean
(
ApiTokenProperty
.
class
.
getName
()
+
".showTokenToAdmins"
);
@DataBoundConstructor
@DataBoundConstructor
public
ApiTokenProperty
()
{
public
ApiTokenProperty
()
{
_changeApiToken
();
_changeApiToken
();
...
@@ -66,7 +81,24 @@ public class ApiTokenProperty extends UserProperty {
...
@@ -66,7 +81,24 @@ public class ApiTokenProperty extends UserProperty {
apiToken
=
Secret
.
fromString
(
seed
);
apiToken
=
Secret
.
fromString
(
seed
);
}
}
/**
* Gets the API token.
* The method performs security checks. Only the current user and SYSTEM may see it.
* Users with {@link Jenkins#ADMINISTER} may be allowed to do it using {@link #SHOW_TOKEN_TO_ADMINS}.
*
* @return API Token. Never null, but may be {@link Messages#ApiTokenProperty_ChangeToken_TokenIsHidden()}
* if the user has no appropriate permissions.
* @since TODO: the method performs security checks
*/
@Nonnull
public
String
getApiToken
()
{
public
String
getApiToken
()
{
return
hasPermissionToSeeToken
()
?
getApiTokenInsecure
()
:
Messages
.
ApiTokenProperty_ChangeToken_TokenIsHidden
();
}
@Nonnull
@Restricted
(
NoExternalUse
.
class
)
/*package*/
String
getApiTokenInsecure
()
{
String
p
=
apiToken
.
getPlainText
();
String
p
=
apiToken
.
getPlainText
();
if
(
p
.
equals
(
Util
.
getDigestOf
(
Jenkins
.
getInstance
().
getSecretKey
()+
":"
+
user
.
getId
())))
{
if
(
p
.
equals
(
Util
.
getDigestOf
(
Jenkins
.
getInstance
().
getSecretKey
()+
":"
+
user
.
getId
())))
{
// if the current token is the initial value created by pre SECURITY-49 Jenkins, we can't use that.
// if the current token is the initial value created by pre SECURITY-49 Jenkins, we can't use that.
...
@@ -77,7 +109,34 @@ public class ApiTokenProperty extends UserProperty {
...
@@ -77,7 +109,34 @@ public class ApiTokenProperty extends UserProperty {
}
}
public
boolean
matchesPassword
(
String
password
)
{
public
boolean
matchesPassword
(
String
password
)
{
return
getApiToken
().
equals
(
password
);
return
getApiTokenInsecure
().
equals
(
password
);
}
private
boolean
hasPermissionToSeeToken
()
{
final
Jenkins
jenkins
=
Jenkins
.
getInstance
();
if
(
jenkins
==
null
)
{
return
false
;
// Should not happen - we don't display UIs in this stage
}
// Administrators can do whatever they want
if
(
SHOW_TOKEN_TO_ADMINS
&&
jenkins
.
hasPermission
(
Jenkins
.
ADMINISTER
))
{
return
true
;
}
final
User
current
=
User
.
current
();
if
(
current
==
null
)
{
// Anonymous
return
false
;
}
// SYSTEM user is always eligible to see tokens
if
(
Jenkins
.
getAuthentication
()
==
ACL
.
SYSTEM
)
{
return
true
;
}
//TODO: replace by IdStrategy in newer Jenkins versions
//return User.idStrategy().equals(user.getId(), current.getId());
return
StringUtils
.
equals
(
user
.
getId
(),
current
.
getId
());
}
}
public
void
changeApiToken
()
throws
IOException
{
public
void
changeApiToken
()
throws
IOException
{
...
@@ -125,7 +184,9 @@ public class ApiTokenProperty extends UserProperty {
...
@@ -125,7 +184,9 @@ public class ApiTokenProperty extends UserProperty {
p
.
changeApiToken
();
p
.
changeApiToken
();
}
}
rsp
.
setHeader
(
"script"
,
"document.getElementById('apiToken').value='"
+
p
.
getApiToken
()+
"'"
);
rsp
.
setHeader
(
"script"
,
"document.getElementById('apiToken').value='"
+
p
.
getApiToken
()+
"'"
);
return
HttpResponses
.
html
(
Messages
.
ApiTokenProperty_ChangeToken_Success
());
return
HttpResponses
.
html
(
p
.
hasPermissionToSeeToken
()
?
Messages
.
ApiTokenProperty_ChangeToken_Success
()
:
Messages
.
ApiTokenProperty_ChangeToken_SuccessHidden
());
}
}
}
}
...
...
core/src/main/resources/jenkins/security/Messages.properties
浏览文件 @
b3f16489
...
@@ -21,5 +21,7 @@
...
@@ -21,5 +21,7 @@
# THE SOFTWARE.
# THE SOFTWARE.
ApiTokenProperty.DisplayName
=
API Token
ApiTokenProperty.DisplayName
=
API Token
ApiTokenProperty.ChangeToken.Success
=
<div>Updated</div>
ApiTokenProperty.ChangeToken.TokenIsHidden
=
Token is hidden
ApiTokenProperty.ChangeToken.Success
=
<div>Updated. See the new token in the field above</div>
ApiTokenProperty.ChangeToken.SuccessHidden
=
<div>Updated. You need to login as the user to see the token</div>
RekeySecretAdminMonitor.DisplayName
=
Re-keying
RekeySecretAdminMonitor.DisplayName
=
Re-keying
\ No newline at end of file
test/src/test/java/jenkins/security/ApiTokenPropertyTest.java
浏览文件 @
b3f16489
...
@@ -5,6 +5,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm;
...
@@ -5,6 +5,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm;
import
com.gargoylesoftware.htmlunit.html.HtmlPage
;
import
com.gargoylesoftware.htmlunit.html.HtmlPage
;
import
hudson.Util
;
import
hudson.Util
;
import
hudson.model.User
;
import
hudson.model.User
;
import
hudson.security.ACL
;
import
jenkins.model.Jenkins
;
import
jenkins.model.Jenkins
;
import
org.apache.commons.httpclient.Credentials
;
import
org.apache.commons.httpclient.Credentials
;
import
org.apache.commons.httpclient.HttpClient
;
import
org.apache.commons.httpclient.HttpClient
;
...
@@ -16,6 +17,8 @@ import org.apache.commons.httpclient.auth.CredentialsProvider;
...
@@ -16,6 +17,8 @@ import org.apache.commons.httpclient.auth.CredentialsProvider;
import
org.jvnet.hudson.test.HudsonTestCase
;
import
org.jvnet.hudson.test.HudsonTestCase
;
import
java.util.concurrent.Callable
;
import
java.util.concurrent.Callable
;
import
javax.annotation.Nonnull
;
import
org.jvnet.hudson.test.Issue
;
/**
/**
* @author Kohsuke Kawaguchi
* @author Kohsuke Kawaguchi
...
@@ -27,40 +30,33 @@ public class ApiTokenPropertyTest extends HudsonTestCase {
...
@@ -27,40 +30,33 @@ public class ApiTokenPropertyTest extends HudsonTestCase {
public
void
testBasics
()
throws
Exception
{
public
void
testBasics
()
throws
Exception
{
jenkins
.
setSecurityRealm
(
createDummySecurityRealm
());
jenkins
.
setSecurityRealm
(
createDummySecurityRealm
());
User
u
=
User
.
get
(
"foo"
);
User
u
=
User
.
get
(
"foo"
);
ApiTokenProperty
t
=
u
.
getProperty
(
ApiTokenProperty
.
class
);
final
ApiTokenProperty
t
=
u
.
getProperty
(
ApiTokenProperty
.
class
);
final
String
token
=
t
.
getApiToken
();
final
String
token
=
t
.
getApiToken
();
// make sure the UI shows the token
// Make sure that user is able to get the token via the interface
HtmlPage
config
=
createWebClient
().
goTo
(
u
.
getUrl
()
+
"/configure"
);
ACL
.
impersonate
(
u
.
impersonate
(),
new
Runnable
()
{
HtmlForm
form
=
config
.
getFormByName
(
"config"
);
assertEquals
(
token
,
form
.
getInputByName
(
"_.apiToken"
).
getValueAttribute
());
// round-trip shouldn't change the API token
submit
(
form
);
assertSame
(
t
,
u
.
getProperty
(
ApiTokenProperty
.
class
));
WebClient
wc
=
createWebClient
();
wc
.
setCredentialsProvider
(
new
CredentialsProvider
()
{
public
Credentials
getCredentials
(
AuthScheme
scheme
,
String
host
,
int
port
,
boolean
proxy
)
throws
CredentialsNotAvailableException
{
return
new
UsernamePasswordCredentials
(
"foo"
,
token
);
}
});
wc
.
setWebConnection
(
new
HttpWebConnection
(
wc
)
{
@Override
@Override
protected
HttpClient
getHttpClient
()
{
public
void
run
()
{
HttpClient
c
=
super
.
getHttpClient
();
assertEquals
(
"User is unable to get its own token"
,
token
,
t
.
getApiToken
());
c
.
getParams
().
setAuthenticationPreemptive
(
true
);
c
.
getState
().
setCredentials
(
new
AuthScope
(
"localhost"
,
localPort
,
AuthScope
.
ANY_REALM
),
new
UsernamePasswordCredentials
(
"foo"
,
token
));
return
c
;
}
}
});
});
// test the authentication
// test the authentication via Token
WebClient
wc
=
createClientForUser
(
"foo"
);
assertEquals
(
u
,
wc
.
executeOnServer
(
new
Callable
<
User
>()
{
assertEquals
(
u
,
wc
.
executeOnServer
(
new
Callable
<
User
>()
{
public
User
call
()
throws
Exception
{
public
User
call
()
throws
Exception
{
return
User
.
current
();
return
User
.
current
();
}
}
}));
}));
// Make sure the UI shows the token to the user
HtmlPage
config
=
wc
.
goTo
(
u
.
getUrl
()
+
"/configure"
);
HtmlForm
form
=
config
.
getFormByName
(
"config"
);
assertEquals
(
token
,
form
.
getInputByName
(
"_.apiToken"
).
getValueAttribute
());
// round-trip shouldn't change the API token
submit
(
form
);
assertSame
(
t
,
u
.
getProperty
(
ApiTokenProperty
.
class
));
}
}
public
void
testSecurity49Upgrade
()
throws
Exception
{
public
void
testSecurity49Upgrade
()
throws
Exception
{
...
@@ -85,4 +81,61 @@ public class ApiTokenPropertyTest extends HudsonTestCase {
...
@@ -85,4 +81,61 @@ public class ApiTokenPropertyTest extends HudsonTestCase {
assertTrue
(
t
.
getApiToken
().
equals
(
Util
.
getDigestOf
(
historicalInitialValue
+
"somethingElse"
)));
assertTrue
(
t
.
getApiToken
().
equals
(
Util
.
getDigestOf
(
historicalInitialValue
+
"somethingElse"
)));
}
}
@Issue
(
"SECURITY-200"
)
public
void
testAdminsShouldBeUnableToSeeTokensByDefault
()
throws
Exception
{
jenkins
.
setSecurityRealm
(
createDummySecurityRealm
());
User
u
=
User
.
get
(
"foo"
);
final
ApiTokenProperty
t
=
u
.
getProperty
(
ApiTokenProperty
.
class
);
final
String
token
=
t
.
getApiToken
();
// Make sure the UI does not show the token to another user
WebClient
wc
=
createClientForUser
(
"bar"
);
HtmlPage
config
=
wc
.
goTo
(
u
.
getUrl
()
+
"/configure"
);
HtmlForm
form
=
config
.
getFormByName
(
"config"
);
assertEquals
(
Messages
.
ApiTokenProperty_ChangeToken_TokenIsHidden
(),
form
.
getInputByName
(
"_.apiToken"
).
getValueAttribute
());
}
@Issue
(
"SECURITY-200"
)
public
void
testAdminsShouldBeUnableToChangeTokensByDefault
()
throws
Exception
{
jenkins
.
setSecurityRealm
(
createDummySecurityRealm
());
User
foo
=
User
.
get
(
"foo"
);
User
bar
=
User
.
get
(
"bar"
);
final
ApiTokenProperty
t
=
foo
.
getProperty
(
ApiTokenProperty
.
class
);
final
ApiTokenProperty
.
DescriptorImpl
descriptor
=
(
ApiTokenProperty
.
DescriptorImpl
)
t
.
getDescriptor
();
// Make sure that Admin can reset a token of another user
WebClient
wc
=
createClientForUser
(
"bar"
);
HtmlPage
res
=
wc
.
goTo
(
foo
.
getUrl
()
+
"/"
+
descriptor
.
getDescriptorUrl
()+
"/changeToken"
);
assertEquals
(
"Update token response is incorrect"
,
Messages
.
ApiTokenProperty_ChangeToken_SuccessHidden
(),
"<div>"
+
res
.
getBody
().
asText
()
+
"</div>"
);
}
@Nonnull
private
WebClient
createClientForUser
(
final
String
username
)
{
User
u
=
User
.
get
(
username
);
final
ApiTokenProperty
t
=
u
.
getProperty
(
ApiTokenProperty
.
class
);
// Yes, we use the insecure call in the test stuff
final
String
token
=
t
.
getApiTokenInsecure
();
WebClient
wc
=
createWebClient
();
wc
.
setCredentialsProvider
(
new
CredentialsProvider
()
{
@Override
public
Credentials
getCredentials
(
AuthScheme
scheme
,
String
host
,
int
port
,
boolean
proxy
)
throws
CredentialsNotAvailableException
{
return
new
UsernamePasswordCredentials
(
username
,
token
);
}
});
wc
.
setWebConnection
(
new
HttpWebConnection
(
wc
)
{
@Override
protected
HttpClient
getHttpClient
()
{
HttpClient
c
=
super
.
getHttpClient
();
c
.
getParams
().
setAuthenticationPreemptive
(
true
);
c
.
getState
().
setCredentials
(
new
AuthScope
(
"localhost"
,
localPort
,
AuthScope
.
ANY_REALM
),
new
UsernamePasswordCredentials
(
username
,
token
));
return
c
;
}
});
return
wc
;
}
}
}
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录