提交 65c9f442 编写于 作者: M Marc Boorshtein 提交者: Kubernetes Prow Robot

impersonation support for the dashboard (#4082)

* Added user impersonation and username to ui

* groups working

* Added unit tests, extras for impersonation

* added docs

* fixed formatting to be consistent

* updates per pr review

* ran npm frontend:fix per travis ci
上级 1fd48683
......@@ -10,5 +10,21 @@
* [Integrations](integrations.md)
* [Labels](labels.md)
## User Impersonation
Impersonation uses a reverse proxy to inject a user's identifying information (username, groups and extra scopes) as headers in each request to the API server. The Dashboard can pass these headers to the API server if your reverse proxy will inject them in the requests.
![Impersonation Architecture](images/dashboard-impersonation.png "Impersonation Architecture")
Impersonation is useful in situations where using a user's token isn't available, such as cloud-hosted Kubernetes services. To use impersonation a reverse proxy must:
1. Have a Kubernetes service account that [has RBAC permissions to impersonate other users](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation)
2. Generate the `Impersonate-User` header with a unique name identifying the user
3. *Optional* Generate the `Impersonate-Group` header(s) with the impersonated user's group data
4. *Optional* Generate the `Impersonate-Extra` header(s) with additional authorization data
Impersonation will only work when the reverse proxy provides the `Authorization` header with a valid service account. It will not work with any other method of authenticating to the dashboard.
----
_Copyright 2019 [The Kubernetes Dashboard Authors](https://github.com/kubernetes/dashboard/graphs/contributors)_
......@@ -2597,7 +2597,7 @@
<target>Compte de service par défaut</target>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">26</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="f798949a4cdc74591fbf665c178513e4219c0898" datatype="html">
......@@ -2606,7 +2606,7 @@
<target>Connexion</target>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="6426cc90184df1cdb238f45fce5220df8438ed62" datatype="html">
......@@ -2615,7 +2615,7 @@
<target>Déconnexion</target>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">39</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="bef0c68b6a7d96b56a8879ecfb9de80a83c151a2" datatype="html">
......
......@@ -2423,7 +2423,7 @@
<target>デフォルトのサービスアカウント</target>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">26</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="f798949a4cdc74591fbf665c178513e4219c0898" datatype="html">
......@@ -2432,7 +2432,7 @@
<target>サインイン</target>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="6426cc90184df1cdb238f45fce5220df8438ed62" datatype="html">
......@@ -2441,7 +2441,7 @@
<target>サインアウト</target>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">39</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
......
......@@ -2224,7 +2224,7 @@
<source>Default service account</source>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">26</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="f798949a4cdc74591fbf665c178513e4219c0898" datatype="html">
......@@ -2232,7 +2232,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="6426cc90184df1cdb238f45fce5220df8438ed62" datatype="html">
......@@ -2240,7 +2240,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">../src/app/frontend/chrome/userpanel/template.html</context>
<context context-type="linenumber">39</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="f4d1dd59b039ad818d9da7e29a773e10e41d9821" datatype="html">
......
......@@ -50,6 +50,8 @@ const (
JWETokenHeader = "jweToken"
// Default http header for user-agent
DefaultUserAgent = "dashboard"
//Impersonation Extra header
ImpersonateUserExtraHeader = "Impersonate-Extra-"
)
// VERSION of this binary
......@@ -311,12 +313,37 @@ func (self *clientManager) buildCmdConfig(authInfo *api.AuthInfo, cfg *rest.Conf
// Extracts authorization information from the request header
func (self *clientManager) extractAuthInfo(req *restful.Request) (*api.AuthInfo, error) {
authHeader := req.HeaderParameter("Authorization")
impersonationHeader := req.HeaderParameter("Impersonate-User")
jweToken := req.HeaderParameter(JWETokenHeader)
// Authorization header will be more important than our token
token := self.extractTokenFromHeader(authHeader)
if len(token) > 0 {
return &api.AuthInfo{Token: token}, nil
authInfo := &api.AuthInfo{Token: token}
if len(impersonationHeader) > 0 {
//there's an impersonation header, lets make sure to add it
authInfo.Impersonate = impersonationHeader
//Check for impersonated groups
if groupsImpersonationHeader := req.Request.Header["Impersonate-Group"]; len(groupsImpersonationHeader) > 0 {
authInfo.ImpersonateGroups = groupsImpersonationHeader
}
//check for extra fields
for headerName, headerValues := range req.Request.Header {
if strings.HasPrefix(headerName, ImpersonateUserExtraHeader) {
extraName := headerName[len(ImpersonateUserExtraHeader):]
if authInfo.ImpersonateUserExtra == nil {
authInfo.ImpersonateUserExtra = make(map[string][]string)
}
authInfo.ImpersonateUserExtra[extraName] = headerValues
}
}
}
return authInfo, nil
}
if self.tokenManager != nil && len(jweToken) > 0 {
......
......@@ -305,3 +305,289 @@ func TestClientManager_InsecureAPIExtensionsClient(t *testing.T) {
t.Fatalf("InsecureClient(): Expected insecure client not to be nil")
}
}
func TestImpersonationUserClient(t *testing.T) {
args.GetHolderBuilder().SetEnableSkipLogin(true)
cases := []struct {
request *restful.Request
expected string
expectedImpersonationUser string
}{
{
&restful.Request{
Request: &http.Request{
Header: http.Header(map[string][]string{
"Authorization": {"Bearer test-token"},
"Impersonate-User": {"impersonatedUser"},
}),
TLS: &tls.ConnectionState{},
},
},
"test-token",
"impersonatedUser",
},
}
for _, c := range cases {
manager := NewClientManager("", "https://localhost:8080")
cfg, err := manager.Config(c.request)
//authInfo := manager.extractAuthInfo(c.request)
if err != nil {
t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+
" %s",
c.request, err.Error())
}
if cfg.BearerToken != c.expected {
t.Fatalf("Config(%v): Expected token to be %s but got %s",
c.request, c.expected, cfg.BearerToken)
}
if cfg.Impersonate.UserName != c.expectedImpersonationUser {
t.Fatalf("Config(%v): Expected impersonated user to be %s but got %s",
c.request, c.expectedImpersonationUser, cfg.Impersonate.UserName)
}
}
}
func TestNoImpersonationUserWithNoBearerClient(t *testing.T) {
args.GetHolderBuilder().SetEnableSkipLogin(true)
cases := []struct {
request *restful.Request
}{
{
&restful.Request{
Request: &http.Request{
Header: http.Header(map[string][]string{}),
TLS: &tls.ConnectionState{},
},
},
},
}
for _, c := range cases {
manager := NewClientManager("", "https://localhost:8080")
cfg, err := manager.Config(c.request)
//authInfo := manager.extractAuthInfo(c.request)
if err != nil {
t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+
" %s",
c.request, err.Error())
}
if len(cfg.BearerToken) > 0 {
t.Fatalf("Config(%v): Expected no token but got %s",
c.request, cfg.BearerToken)
}
if len(cfg.Impersonate.UserName) > 0 {
t.Fatalf("Config(%v): Expected no impersonated user but got %s",
c.request, cfg.Impersonate.UserName)
}
}
}
func TestImpersonationOneGroupClient(t *testing.T) {
args.GetHolderBuilder().SetEnableSkipLogin(true)
cases := []struct {
request *restful.Request
expected string
expectedImpersonationUser string
expectedImpersonationGroups []string
}{
{
&restful.Request{
Request: &http.Request{
Header: http.Header(map[string][]string{
"Authorization": {"Bearer test-token"},
"Impersonate-User": {"impersonatedUser"},
"Impersonate-Group": {"group1"},
}),
TLS: &tls.ConnectionState{},
},
},
"test-token",
"impersonatedUser",
[]string{"group1"},
},
}
for _, c := range cases {
manager := NewClientManager("", "https://localhost:8080")
cfg, err := manager.Config(c.request)
//authInfo := manager.extractAuthInfo(c.request)
if err != nil {
t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+
" %s",
c.request, err.Error())
}
if cfg.BearerToken != c.expected {
t.Fatalf("Config(%v): Expected token to be %s but got %s",
c.request, c.expected, cfg.BearerToken)
}
if cfg.Impersonate.UserName != c.expectedImpersonationUser {
t.Fatalf("Config(%v): Expected impersonated user to be %s but got %s",
c.request, c.expectedImpersonationUser, cfg.Impersonate.UserName)
}
if len(cfg.Impersonate.Groups) != 1 {
t.Fatalf("Config(%v): Expected one impersonated group but got %d",
c.request, len(cfg.Impersonate.Groups))
}
if cfg.Impersonate.Groups[0] != c.expectedImpersonationGroups[0] {
t.Fatalf("Config(%v): Expected impersonated group to be %s but got %s",
c.request, cfg.Impersonate.Groups[0], c.expectedImpersonationGroups[0])
}
}
}
func TestImpersonationTwoGroupClient(t *testing.T) {
args.GetHolderBuilder().SetEnableSkipLogin(true)
cases := []struct {
request *restful.Request
expected string
expectedImpersonationUser string
expectedImpersonationGroups []string
}{
{
&restful.Request{
Request: &http.Request{
Header: http.Header(map[string][]string{
"Authorization": {"Bearer test-token"},
"Impersonate-User": {"impersonatedUser"},
"Impersonate-Group": {"group1", "groups2"},
}),
TLS: &tls.ConnectionState{},
},
},
"test-token",
"impersonatedUser",
[]string{"group1", "groups2"},
},
}
for _, c := range cases {
manager := NewClientManager("", "https://localhost:8080")
cfg, err := manager.Config(c.request)
//authInfo := manager.extractAuthInfo(c.request)
if err != nil {
t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+
" %s",
c.request, err.Error())
}
if cfg.BearerToken != c.expected {
t.Fatalf("Config(%v): Expected token to be %s but got %s",
c.request, c.expected, cfg.BearerToken)
}
if cfg.Impersonate.UserName != c.expectedImpersonationUser {
t.Fatalf("Config(%v): Expected impersonated user to be %s but got %s",
c.request, c.expectedImpersonationUser, cfg.Impersonate.UserName)
}
if len(cfg.Impersonate.Groups) != 2 {
t.Fatalf("Config(%v): Expected two impersonated group but got %d",
c.request, len(cfg.Impersonate.Groups))
}
if cfg.Impersonate.Groups[0] != c.expectedImpersonationGroups[0] {
t.Fatalf("Config(%v): Expected impersonated group to be %s but got %s",
c.request, cfg.Impersonate.Groups[0], c.expectedImpersonationGroups[0])
}
if cfg.Impersonate.Groups[1] != c.expectedImpersonationGroups[1] {
t.Fatalf("Config(%v): Expected impersonated group to be %s but got %s",
c.request, cfg.Impersonate.Groups[1], c.expectedImpersonationGroups[1])
}
}
}
func TestImpersonationExtrasClient(t *testing.T) {
args.GetHolderBuilder().SetEnableSkipLogin(true)
cases := []struct {
request *restful.Request
expected string
expectedImpersonationUser string
expectedImpersonationExtra map[string][]string
}{
{
&restful.Request{
Request: &http.Request{
Header: http.Header(map[string][]string{
"Authorization": {"Bearer test-token"},
"Impersonate-User": {"impersonatedUser"},
"Impersonate-Extra-scope": {"views", "writes"},
"Impersonate-Extra-service": {"iguess"},
}),
TLS: &tls.ConnectionState{},
},
},
"test-token",
"impersonatedUser",
map[string][]string{"scope": {"views", "writes"},
"service": {"iguess"}},
},
}
for _, c := range cases {
manager := NewClientManager("", "https://localhost:8080")
cfg, err := manager.Config(c.request)
//authInfo := manager.extractAuthInfo(c.request)
if err != nil {
t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+
" %s",
c.request, err.Error())
}
if cfg.BearerToken != c.expected {
t.Fatalf("Config(%v): Expected token to be %s but got %s",
c.request, c.expected, cfg.BearerToken)
}
if cfg.Impersonate.UserName != c.expectedImpersonationUser {
t.Fatalf("Config(%v): Expected impersonated user to be %s but got %s",
c.request, c.expectedImpersonationUser, cfg.Impersonate.UserName)
}
if len(cfg.Impersonate.Extra) != 2 {
t.Fatalf("Config(%v): Expected two impersonated extra but got %d",
c.request, len(cfg.Impersonate.Extra))
}
if cfg.Impersonate.Extra["service"][0] != c.expectedImpersonationExtra["service"][0] {
t.Fatalf("Config(%v): Expected service extra to be %s but got %s",
c.request, cfg.Impersonate.Extra["service"][0], c.expectedImpersonationExtra["service"][0])
}
//check multi value scope
if len(cfg.Impersonate.Extra["scope"]) != 2 {
t.Fatalf("Config(%v): Expected two scope impersonated extra but got %d",
c.request, len(cfg.Impersonate.Extra["scope"]))
}
if cfg.Impersonate.Extra["scope"][0] != c.expectedImpersonationExtra["scope"][0] {
t.Fatalf("Config(%v): Expected scope extra to be %s but got %s",
c.request, c.expectedImpersonationExtra["scope"][0], cfg.Impersonate.Extra["scope"][0])
}
if cfg.Impersonate.Extra["scope"][1] != c.expectedImpersonationExtra["scope"][1] {
t.Fatalf("Config(%v): Expected scope extra to be %s but got %s",
c.request, c.expectedImpersonationExtra["scope"][1], cfg.Impersonate.Extra["scope"][1])
}
if len(cfg.Impersonate.Extra["scope"]) != 2 {
t.Fatalf("Config(%v): Expected two scope impersonated extra but got %d",
c.request, len(cfg.Impersonate.Extra["scope"]))
}
}
}
......@@ -30,21 +30,34 @@ type LoginStatus struct {
// True if dashboard is configured to use HTTPS connection. It is required for secure
// data exchange during login operation.
HTTPSMode bool `json:"httpsMode"`
// True if impersonation is enabled
ImpersonationPresent bool `json:"impersonationPresent"`
// The impersonated user
ImpersonatedUser string `json:"impersonatedUser"`
}
// ValidateLoginStatus returns information about user login status and if request was made over HTTPS.
func ValidateLoginStatus(request *restful.Request) *LoginStatus {
authHeader := request.HeaderParameter("Authorization")
tokenHeader := request.HeaderParameter(client.JWETokenHeader)
impersonationHeader := request.HeaderParameter("Impersonate-User")
httpsMode := request.Request.TLS != nil
if args.Holder.GetEnableInsecureLogin() {
httpsMode = true
}
return &LoginStatus{
TokenPresent: len(tokenHeader) > 0,
HeaderPresent: len(authHeader) > 0,
HTTPSMode: httpsMode,
loginStatus := &LoginStatus{
TokenPresent: len(tokenHeader) > 0,
HeaderPresent: len(authHeader) > 0,
ImpersonationPresent: len(impersonationHeader) > 0,
HTTPSMode: httpsMode,
}
if loginStatus.ImpersonationPresent {
loginStatus.ImpersonatedUser = impersonationHeader
}
return loginStatus
}
......@@ -18,13 +18,15 @@ limitations under the License.
<div class="kd-auth-status kd-muted-light">
<ng-container *ngIf="isLoginStatusInitialized">
<ng-container [ngSwitch]="true">
<ng-container *ngSwitchCase="loginStatus.headerPresent"
<ng-container *ngSwitchCase="loginStatus.headerPresent && !loginStatus.impersonationPresent"
i18n>Logged in with auth header</ng-container>
<ng-container *ngSwitchCase="loginStatus.tokenPresent"
i18n>Logged in with token</ng-container>
<ng-container *ngSwitchCase="loginStatus.headerPresent && loginStatus.impersonationPresent">{{loginStatus.impersonatedUser}}</ng-container>
<ng-container *ngSwitchDefault
i18n>Default service account</ng-container>
</ng-container>
</ng-container>
</div>
<mat-divider></mat-divider>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册