diff --git a/pkg/auth/oauth/handlers/grant.go b/pkg/auth/oauth/handlers/grant.go index 20f41f057c57..4e42b41a3b0e 100644 --- a/pkg/auth/oauth/handlers/grant.go +++ b/pkg/auth/oauth/handlers/grant.go @@ -162,7 +162,7 @@ func (g *redirectGrant) GrantNeeded(user user.Info, grant *api.Grant, w http.Res redirectURL.RawQuery = url.Values{ "then": {req.URL.String()}, "client_id": {grant.Client.GetId()}, - "scopes": {grant.Scope}, + "scope": {grant.Scope}, "redirect_uri": {grant.RedirectURI}, }.Encode() http.Redirect(w, req, redirectURL.String(), http.StatusFound) diff --git a/pkg/auth/oauth/registry/registry_test.go b/pkg/auth/oauth/registry/registry_test.go index c5b8c5f3b9fd..32b2d68df281 100644 --- a/pkg/auth/oauth/registry/registry_test.go +++ b/pkg/auth/oauth/registry/registry_test.go @@ -239,7 +239,7 @@ func TestRegistryAndServer(t *testing.T) { ClientAuthorization: testCase.ClientAuth, } if testCase.ClientAuth == nil { - grant.Err = apierrs.NewNotFound(oapi.Resource("OAuthClientAuthorization"), "test:test") + grant.GetErr = apierrs.NewNotFound(oapi.Resource("OAuthClientAuthorization"), "test:test") } storage := registrystorage.New(access, authorize, client, NewUserConversion()) config := osinserver.NewDefaultServerConfig() diff --git a/pkg/auth/server/grant/grant.go b/pkg/auth/server/grant/grant.go index 21a6de765fa6..f0a2a9b15cc3 100644 --- a/pkg/auth/server/grant/grant.go +++ b/pkg/auth/server/grant/grant.go @@ -2,13 +2,13 @@ package grant import ( "fmt" - "html/template" "net/http" "net/url" "strings" kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/auth/user" + "k8s.io/kubernetes/pkg/serviceaccount" utilruntime "k8s.io/kubernetes/pkg/util/runtime" "github.com/golang/glog" @@ -26,7 +26,7 @@ const ( csrfParam = "csrf" clientIDParam = "client_id" userNameParam = "user_name" - scopesParam = "scopes" + scopeParam = "scope" redirectURIParam = "redirect_uri" approveParam = "approve" @@ -42,24 +42,38 @@ type FormRenderer interface { type Form struct { Action string Error string - Values FormValues + + ServiceAccountName string + ServiceAccountNamespace string + + GrantedScopes interface{} + + Names GrantFormFields + Values GrantFormFields +} + +type GrantFormFields struct { + Then string + CSRF string + ClientID string + UserName string + Scopes interface{} + RedirectURI string + Approve string + Deny string } -type FormValues struct { - Then string - ThenParam string - CSRF string - CSRFParam string - ClientID string - ClientIDParam string - UserName string - UserNameParam string - Scopes string - ScopesParam string - RedirectURI string - RedirectURIParam string - ApproveParam string - DenyParam string +type Scope struct { + // Name is the string included in the OAuth scope parameter + Name string + // Description is a human-readable description of the scope. May be empty. + Description string + // Warning is a human-readable warning about the scope. Typically used to scare the user about escalating permissions. May be empty. + Warning string + // Error is a human-readable error, typically around the validity of the scope. May be empty. + Error string + // Granted indicates whether the user has already granted this scope. + Granted bool } type Grant struct { @@ -108,10 +122,10 @@ func (l *Grant) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (l *Grant) handleForm(user user.Info, w http.ResponseWriter, req *http.Request) { q := req.URL.Query() - then := q.Get("then") - clientID := q.Get("client_id") - scopes := q.Get("scopes") - redirectURI := q.Get("redirect_uri") + then := q.Get(thenParam) + clientID := q.Get(clientIDParam) + scopes := scope.Split(q.Get(scopeParam)) + redirectURI := q.Get(redirectURIParam) client, err := l.clientregistry.GetClient(kapi.NewContext(), clientID) if err != nil || client == nil { @@ -119,7 +133,7 @@ func (l *Grant) handleForm(user user.Info, w http.ResponseWriter, req *http.Requ return } - if err := scopeauthorizer.ValidateScopeRestrictions(client, scope.Split(scopes)...); err != nil { + if err := scopeauthorizer.ValidateScopeRestrictions(client, scopes...); err != nil { failure := fmt.Sprintf("%v requested illegal scopes (%v): %v", client.Name, scopes, err) l.failed(failure, w, req) return @@ -139,41 +153,73 @@ func (l *Grant) handleForm(user user.Info, w http.ResponseWriter, req *http.Requ return } + grantedScopeNames := []string{} + grantedScopes := []Scope{} + requestedScopes := []Scope{} + + clientAuthID := l.authregistry.ClientAuthorizationName(user.GetName(), client.Name) + if clientAuth, err := l.authregistry.GetClientAuthorization(kapi.NewContext(), clientAuthID); err == nil { + grantedScopeNames = clientAuth.Scopes + } + + for _, s := range scopes { + requestedScopes = append(requestedScopes, getScopeData(s, grantedScopeNames)) + } + for _, s := range grantedScopeNames { + grantedScopes = append(grantedScopes, getScopeData(s, grantedScopeNames)) + } + form := Form{ - Action: uri.String(), - Values: FormValues{ - Then: then, - ThenParam: thenParam, - CSRF: csrf, - CSRFParam: csrfParam, - ClientID: client.Name, - ClientIDParam: clientIDParam, - UserName: user.GetName(), - UserNameParam: userNameParam, - Scopes: scopes, - ScopesParam: scopesParam, - RedirectURI: redirectURI, - RedirectURIParam: redirectURIParam, - ApproveParam: approveParam, - DenyParam: denyParam, + Action: uri.String(), + GrantedScopes: grantedScopes, + Names: GrantFormFields{ + Then: thenParam, + CSRF: csrfParam, + ClientID: clientIDParam, + UserName: userNameParam, + Scopes: scopeParam, + RedirectURI: redirectURIParam, + Approve: approveParam, + Deny: denyParam, + }, + Values: GrantFormFields{ + Then: then, + CSRF: csrf, + ClientID: client.Name, + UserName: user.GetName(), + Scopes: requestedScopes, + RedirectURI: redirectURI, }, } + if saNamespace, saName, err := serviceaccount.SplitUsername(client.Name); err == nil { + form.ServiceAccountName = saName + form.ServiceAccountNamespace = saNamespace + } + l.render.Render(form, w, req) } func (l *Grant) handleGrant(user user.Info, w http.ResponseWriter, req *http.Request) { - if ok, err := l.csrf.Check(req, req.FormValue("csrf")); !ok || err != nil { + if ok, err := l.csrf.Check(req, req.FormValue(csrfParam)); !ok || err != nil { glog.Errorf("Unable to check CSRF token: %v", err) l.failed("Invalid CSRF token", w, req) return } - then := req.FormValue("then") - scopes := req.FormValue("scopes") + req.ParseForm() + then := req.FormValue(thenParam) + scopes := scope.Join(req.Form[scopeParam]) + username := req.FormValue(userNameParam) + + if username != user.GetName() { + glog.Errorf("User (%v) did not match authenticated user (%v)", username, user.GetName()) + l.failed("User did not match", w, req) + return + } - if len(req.FormValue(approveParam)) == 0 { - // Redirect with rejection param + if len(req.FormValue(approveParam)) == 0 || len(scopes) == 0 { + // Redirect with an error param url, err := url.Parse(then) if len(then) == 0 || err != nil { l.failed("Access denied, but no redirect URL was specified", w, req) @@ -186,7 +232,7 @@ func (l *Grant) handleGrant(user user.Info, w http.ResponseWriter, req *http.Req return } - clientID := req.FormValue("client_id") + clientID := req.FormValue(clientIDParam) client, err := l.clientregistry.GetClient(kapi.NewContext(), clientID) if err != nil || client == nil { l.failed("Could not find client for client_id", w, req) @@ -227,12 +273,16 @@ func (l *Grant) handleGrant(user user.Info, w http.ResponseWriter, req *http.Req } } - if len(then) == 0 { - l.failed("Approval granted, but no redirect URL was specified", w, req) + // Redirect, overriding the scope param on the redirect with the scopes that were actually granted + url, err := url.Parse(then) + if len(then) == 0 || err != nil { + l.failed("Access granted, but no redirect URL was specified", w, req) return } - - http.Redirect(w, req, then, http.StatusFound) + q := url.Query() + q.Set("scope", scopes) + url.RawQuery = q.Encode() + http.Redirect(w, req, url.String(), http.StatusFound) } func (l *Grant) failed(reason string, w http.ResponseWriter, req *http.Request) { @@ -261,6 +311,29 @@ func getBaseURL(req *http.Request) (*url.URL, error) { return uri, nil } +func getScopeData(scopeName string, grantedScopeNames []string) Scope { + scopeData := Scope{ + Name: scopeName, + Error: fmt.Sprintf("Unknown scope"), + Granted: scope.Covers(grantedScopeNames, []string{scopeName}), + } + for _, evaluator := range scopeauthorizer.ScopeEvaluators { + if !evaluator.Handles(scopeName) { + continue + } + description, warning, err := evaluator.Describe(scopeName) + scopeData.Description = description + scopeData.Warning = warning + if err == nil { + scopeData.Error = "" + } else { + scopeData.Error = err.Error() + } + break + } + return scopeData +} + // DefaultFormRenderer displays a page prompting the user to approve an OAuth grant. // The requesting client id, requested scopes, and redirect URI are displayed to the user. var DefaultFormRenderer = grantTemplateRenderer{} @@ -270,40 +343,8 @@ type grantTemplateRenderer struct{} func (r grantTemplateRenderer) Render(form Form, w http.ResponseWriter, req *http.Request) { w.Header().Add("Content-Type", "text/html; charset=UTF-8") w.WriteHeader(http.StatusOK) - if err := grantTemplate.Execute(w, form); err != nil { + + if err := defaultGrantTemplate.Execute(w, form); err != nil { utilruntime.HandleError(fmt.Errorf("unable to render grant template: %v", err)) } } - -// TODO: allow template to be read from an external file -var grantTemplate = template.Must(template.New("grantForm").Parse(` - -{{ if .Error }} -
{{ .Error }}
-{{ else }} -
- - - - - - - -

Approve Client?

-

Do you approve granting an access token to the following OAuth client?

-
-Client: {{ .Values.ClientID }}
-Scope:  {{ .Values.Scopes }}
-URI:    {{ .Values.RedirectURI }}
-
- - - -
-{{ end }} -`)) diff --git a/pkg/auth/server/grant/grant_test.go b/pkg/auth/server/grant/grant_test.go index 9055ec1fa991..391cab99f20e 100644 --- a/pkg/auth/server/grant/grant_test.go +++ b/pkg/auth/server/grant/grant_test.go @@ -11,6 +11,7 @@ import ( "testing" kapi "k8s.io/kubernetes/pkg/api" + kapierrors "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/auth/user" knet "k8s.io/kubernetes/pkg/util/net" @@ -50,7 +51,9 @@ func badClientRegistry(err error) *test.ClientRegistry { } func emptyAuthRegistry() *test.ClientAuthorizationRegistry { - return &test.ClientAuthorizationRegistry{} + return &test.ClientAuthorizationRegistry{ + GetErr: kapierrors.NewNotFound(oapi.Resource("oauthclientauthorizations"), "foo"), + } } func existingAuthRegistry(scopes []string) *test.ClientAuthorizationRegistry { auth := oapi.OAuthClientAuthorization{ @@ -85,14 +88,36 @@ func TestGrant(t *testing.T) { Auth: goodAuth("username"), ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}), AuthRegistry: emptyAuthRegistry(), - Path: "/grant?client_id=myclient&scopes=myscope1%20myscope2&redirect_uri=/myredirect&then=/authorize", + Path: "/grant?client_id=myclient&scope=myscope1%20myscope2&redirect_uri=/myredirect&then=/authorize", ExpectStatusCode: 200, ExpectContains: []string{ `action="/grant"`, `name="csrf" value="test"`, `name="client_id" value="myclient"`, - `name="scopes" value="myscope1 myscope2"`, + `checked name="scope" value="myscope1"`, + `checked name="scope" value="myscope2"`, + `name="redirect_uri" value="/myredirect"`, + `name="then" value="/authorize"`, + }, + }, + + "display form with existing scopes": { + CSRF: &csrf.FakeCSRF{Token: "test"}, + Auth: goodAuth("username"), + ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"newscope1", "newscope2", "existingscope1", "existingscope2"}), + AuthRegistry: existingAuthRegistry([]string{"existingscope1", "existingscope2"}), + Path: "/grant?client_id=myclient&scope=newscope1%20newscope2%20existingscope1%20existingscope2&redirect_uri=/myredirect&then=/authorize", + + ExpectStatusCode: 200, + ExpectContains: []string{ + `action="/grant"`, + `name="csrf" value="test"`, + `name="client_id" value="myclient"`, + `checked name="scope" value="newscope1"`, + `checked name="scope" value="newscope1"`, + `type="hidden" name="scope" value="existingscope1"`, + `type="hidden" name="scope" value="existingscope2"`, `name="redirect_uri" value="/myredirect"`, `name="then" value="/authorize"`, }, @@ -142,7 +167,7 @@ func TestGrant(t *testing.T) { Path: "/grant", PostValues: url.Values{ "client_id": {"myclient"}, - "scopes": {"myscope1 myscope2"}, + "scope": {"myscope1", "myscope2"}, "redirect_uri": {"/myredirect"}, "then": {"/authorize"}, "csrf": {"wrong"}, @@ -152,6 +177,25 @@ func TestGrant(t *testing.T) { ExpectContains: []string{"CSRF"}, }, + "error when POST fails user check": { + CSRF: &csrf.FakeCSRF{Token: "test"}, + Auth: goodAuth("username"), + ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}), + AuthRegistry: emptyAuthRegistry(), + Path: "/grant", + PostValues: url.Values{ + "client_id": {"myclient"}, + "scope": {"myscope1", "myscope2"}, + "redirect_uri": {"/myredirect"}, + "then": {"/authorize"}, + "csrf": {"test"}, + "user_name": {"wrong"}, + }, + + ExpectStatusCode: 200, + ExpectContains: []string{"User did not match"}, + }, + "error displaying form with invalid client": { CSRF: &csrf.FakeCSRF{Token: "test"}, Auth: goodAuth("username"), @@ -172,10 +216,11 @@ func TestGrant(t *testing.T) { PostValues: url.Values{ "approve": {"true"}, "client_id": {"myclient"}, - "scopes": {"myscope1 myscope2"}, + "scope": {"myscope1", "myscope2"}, "redirect_uri": {"/myredirect"}, "then": {"/authorize"}, "csrf": {"test"}, + "user_name": {"username"}, }, ExpectStatusCode: 200, @@ -191,15 +236,16 @@ func TestGrant(t *testing.T) { PostValues: url.Values{ "approve": {"true"}, "client_id": {"myclient"}, - "scopes": {"myscope1 myscope2"}, + "scope": {"myscope1", "myscope2"}, "redirect_uri": {"/myredirect"}, "then": {"/authorize"}, "csrf": {"test"}, + "user_name": {"username"}, }, ExpectStatusCode: 302, ExpectCreatedAuthScopes: []string{"myscope1", "myscope2"}, - ExpectRedirect: "/authorize", + ExpectRedirect: "/authorize?scope=myscope1+myscope2", }, "successful create grant without redirect": { @@ -211,9 +257,10 @@ func TestGrant(t *testing.T) { PostValues: url.Values{ "approve": {"true"}, "client_id": {"myclient"}, - "scopes": {"myscope1 myscope2"}, + "scope": {"myscope1", "myscope2"}, "redirect_uri": {"/myredirect"}, "csrf": {"test"}, + "user_name": {"username"}, }, ExpectStatusCode: 200, @@ -233,15 +280,37 @@ func TestGrant(t *testing.T) { PostValues: url.Values{ "approve": {"true"}, "client_id": {"myclient"}, - "scopes": {"myscope1 myscope2"}, + "scope": {"myscope1", "myscope2"}, "redirect_uri": {"/myredirect"}, "then": {"/authorize"}, "csrf": {"test"}, + "user_name": {"username"}, }, ExpectStatusCode: 302, ExpectUpdatedAuthScopes: []string{"myscope1", "myscope2"}, - ExpectRedirect: "/authorize", + ExpectRedirect: "/authorize?scope=myscope1+myscope2", + }, + + "successful update grant with partial additional scopes": { + CSRF: &csrf.FakeCSRF{Token: "test"}, + Auth: goodAuth("username"), + ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"newscope1", "newscope2", "existingscope1", "existingscope2"}), + AuthRegistry: existingAuthRegistry([]string{"existingscope2", "existingscope1"}), + Path: "/grant", + PostValues: url.Values{ + "approve": {"true"}, + "client_id": {"myclient"}, + "scope": {"newscope1", "existingscope1"}, + "redirect_uri": {"/myredirect"}, + "then": {"/authorize?scope=newscope1+newscope2+existingscope1"}, + "csrf": {"test"}, + "user_name": {"username"}, + }, + + ExpectStatusCode: 302, + ExpectUpdatedAuthScopes: []string{"existingscope1", "existingscope2", "newscope1"}, + ExpectRedirect: "/authorize?scope=newscope1+existingscope1", }, "successful update grant with additional scopes": { @@ -253,18 +322,19 @@ func TestGrant(t *testing.T) { PostValues: url.Values{ "approve": {"true"}, "client_id": {"myclient"}, - "scopes": {"newscope1 existingscope1"}, + "scope": {"newscope1", "existingscope1"}, "redirect_uri": {"/myredirect"}, "then": {"/authorize"}, "csrf": {"test"}, + "user_name": {"username"}, }, ExpectStatusCode: 302, ExpectUpdatedAuthScopes: []string{"existingscope1", "existingscope2", "newscope1"}, - ExpectRedirect: "/authorize", + ExpectRedirect: "/authorize?scope=newscope1+existingscope1", }, - "successful reject grant": { + "successful reject grant via deny button": { CSRF: &csrf.FakeCSRF{Token: "test"}, Auth: goodAuth("username"), ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}), @@ -273,10 +343,31 @@ func TestGrant(t *testing.T) { PostValues: url.Values{ "deny": {"true"}, "client_id": {"myclient"}, - "scopes": {"newscope1 existingscope1"}, + "scope": {"newscope1", "existingscope1"}, + "redirect_uri": {"/myredirect"}, + "then": {"/authorize"}, + "csrf": {"test"}, + "user_name": {"username"}, + }, + + ExpectStatusCode: 302, + ExpectRedirect: "/authorize?error=access_denied", + }, + + "successful reject grant via unchecking all requested scopes and approving": { + CSRF: &csrf.FakeCSRF{Token: "test"}, + Auth: goodAuth("username"), + ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}), + AuthRegistry: existingAuthRegistry([]string{"existingscope2", "existingscope1"}), + Path: "/grant", + PostValues: url.Values{ + "approve": {"true"}, + "client_id": {"myclient"}, + // "scope": {"newscope1", "existingscope1"}, "redirect_uri": {"/myredirect"}, "then": {"/authorize"}, "csrf": {"test"}, + "user_name": {"username"}, }, ExpectStatusCode: 302, diff --git a/pkg/auth/server/grant/templates.go b/pkg/auth/server/grant/templates.go new file mode 100644 index 000000000000..72d5a16be0ef --- /dev/null +++ b/pkg/auth/server/grant/templates.go @@ -0,0 +1,168 @@ +package grant + +import "html/template" + +var defaultGrantTemplate = template.Must(template.New("defaultGrantForm").Parse(defaultGrantTemplateString)) + +const defaultGrantTemplateString = ` + + + + + + Authorize + {{ if and .ServiceAccountName .ServiceAccountNamespace }} + service account {{ .ServiceAccountName }} in project {{ .ServiceAccountNamespace }} + {{ else }} + {{ .Values.ClientID }} + {{ end }} + + + + + +{{ define "scope" }} +
{{ .Name }}
+ {{ if .Description }}
{{ .Description }}
+{{ end -}} + {{ if .Warning }}
{{ .Warning }}
+{{ end -}} + {{ if .Error }}
{{ .Error }}
+{{ end -}} +{{ end }} + + +{{ if .Error }} +
{{ .Error }}
+{{ else }} +
+ + + + + + +

Authorize Access

+ +

+ {{ if and .ServiceAccountName .ServiceAccountNamespace }} + Service account {{ .ServiceAccountName }} in project {{ .ServiceAccountNamespace }} + {{ else }} + {{ .Values.ClientID }} + {{ end }} + + is requesting + + {{ if .GrantedScopes }} + additional permissions + {{ else }} + permission + {{ end }} + + to access your account ({{ .Values.UserName }}) +

+ + + {{ if .GrantedScopes -}} + + + + {{ range $i,$scope := .GrantedScopes -}} + + + + + {{ end }} +
Existing access
+
+ + +
+
+{{ template "scope" . }} +
+
+ {{ end }} + + + {{ range $i,$scope := .Values.Scopes -}} + {{ if .Granted -}} + + {{- end }} + {{ end }} + + + + + + {{ range $i,$scope := .Values.Scopes }} + {{ if not .Granted }} + + + + + {{ end }} + {{ end }} +
+ {{- if .GrantedScopes -}} + Additional requested permissions + {{- else -}} + Requested permissions + {{- end -}} +
+ + + +
+ + + {{ if .Values.RedirectURI -}} +
+
You will be redirected to {{ .Values.RedirectURI }}
+
+ {{- end }} + +
+ + +
+
+{{ end }} + + +` diff --git a/pkg/authorization/authorizer/scope/converter.go b/pkg/authorization/authorizer/scope/converter.go index 5dbbbbb9059c..07ad842d59d0 100644 --- a/pkg/authorization/authorizer/scope/converter.go +++ b/pkg/authorization/authorizer/scope/converter.go @@ -85,17 +85,19 @@ func ScopesToVisibleNamespaces(scopes []string, clusterPolicyGetter client.Clust } const ( - UserIndicator = "user:" - ClusterRoleIndicator = "role:" - ClusterWideIndicator = "clusterwide:" - NamespaceWideIndicator = "namespace:" + UserIndicator = "user:" + ClusterRoleIndicator = "role:" ) // ScopeEvaluator takes a scope and returns the rules that express it type ScopeEvaluator interface { + // Handles returns true if this evaluator can evaluate this scope Handles(scope string) bool - Describe(scope string) string + // Validate returns an error if the scope is malformed Validate(scope string) error + // Describe returns a description, warning (typically used to warn about escalation dangers), or an error if the scope is malformed + Describe(scope string) (description string, warning string, err error) + // ResolveRules returns the policy rules that this scope allows ResolveRules(scope, namespace string, clusterPolicyGetter client.ClusterPolicyLister) ([]authorizationapi.PolicyRule, error) ResolveGettableNamespaces(scope string, clusterPolicyGetter client.ClusterPolicyLister) ([]string, error) } @@ -143,18 +145,18 @@ func (userEvaluator) Validate(scope string) error { return fmt.Errorf("unrecognized scope: %v", scope) } -func (userEvaluator) Describe(scope string) string { +func (userEvaluator) Describe(scope string) (string, string, error) { switch scope { case UserInfo: - return "Information about you, including username, identity names, and group membership." + return "Read-only access to your user information (including username, identities, and group membership)", "", nil case UserAccessCheck: - return `Information about user privileges, e.g. "Can I create builds?"` + return `Read-only access to view your privileges (for example, "can I create builds?")`, "", nil case UserListProject: - return `See projects you're aware of and the metadata (display name, description, etc) about those projects.` + return `Read-only access to list your projects and view their metadata (display name, description, etc.)`, "", nil case UserFull: - return `Full access to the server with all of your permissions.` + return `Full read/write access with all of your permissions`, `Includes any access you have to escalating resources like secrets`, nil default: - return fmt.Sprintf("unrecognized scope: %v", scope) + return "", "", fmt.Errorf("unrecognized scope: %v", scope) } } @@ -248,21 +250,32 @@ func ParseClusterRoleScope(scope string) (string /*role name*/, string /*namespa return tokens[1][0:lastColonIndex], tokens[1][lastColonIndex+1:], escalating, nil } -func (e clusterRoleEvaluator) Describe(scope string) string { +func (e clusterRoleEvaluator) Describe(scope string) (string, string, error) { roleName, scopeNamespace, escalating, err := e.parseScope(scope) if err != nil { - return err.Error() - } - escalatingPhrase := "including any escalating resources like secrets" - if !escalating { - escalatingPhrase = "excluding any escalating resources like secrets" + return "", "", err } + // Anything you can do [in project "foo" | server-wide] that is also allowed by the "admin" role[, except access escalating resources like secrets] + + scopePhrase := "" if scopeNamespace == authorizationapi.ScopesAllNamespaces { - return roleName + " access in all projects, " + escalatingPhrase + scopePhrase = "server-wide" + } else { + scopePhrase = fmt.Sprintf("in project %q", scopeNamespace) } - return roleName + " access in the " + scopeNamespace + " project, " + escalatingPhrase + warning := "" + escalatingPhrase := "" + if escalating { + warning = fmt.Sprintf("Includes access to escalating resources like secrets") + } else { + escalatingPhrase = ", except access escalating resources like secrets" + } + + description := fmt.Sprintf("Anything you can do %s that is also allowed by the %q role%s", scopePhrase, roleName, escalatingPhrase) + + return description, warning, nil } func (e clusterRoleEvaluator) ResolveRules(scope, namespace string, clusterPolicyGetter client.ClusterPolicyLister) ([]authorizationapi.PolicyRule, error) { diff --git a/pkg/oauth/registry/test/clientauthorization.go b/pkg/oauth/registry/test/clientauthorization.go index e968f4b8da6e..64280be99184 100644 --- a/pkg/oauth/registry/test/clientauthorization.go +++ b/pkg/oauth/registry/test/clientauthorization.go @@ -9,11 +9,17 @@ import ( ) type ClientAuthorizationRegistry struct { - Err error - ClientAuthorizations *api.OAuthClientAuthorizationList - ClientAuthorization *api.OAuthClientAuthorization - CreatedAuthorization *api.OAuthClientAuthorization - UpdatedAuthorization *api.OAuthClientAuthorization + GetErr error + ClientAuthorizations *api.OAuthClientAuthorizationList + ClientAuthorization *api.OAuthClientAuthorization + + CreateErr error + CreatedAuthorization *api.OAuthClientAuthorization + + UpdateErr error + UpdatedAuthorization *api.OAuthClientAuthorization + + DeleteErr error DeletedClientAuthorizationName string } @@ -22,24 +28,24 @@ func (r *ClientAuthorizationRegistry) ClientAuthorizationName(userName, clientNa } func (r *ClientAuthorizationRegistry) ListClientAuthorizations(ctx kapi.Context, options *kapi.ListOptions) (*api.OAuthClientAuthorizationList, error) { - return r.ClientAuthorizations, r.Err + return r.ClientAuthorizations, r.GetErr } func (r *ClientAuthorizationRegistry) GetClientAuthorization(ctx kapi.Context, name string) (*api.OAuthClientAuthorization, error) { - return r.ClientAuthorization, r.Err + return r.ClientAuthorization, r.GetErr } func (r *ClientAuthorizationRegistry) CreateClientAuthorization(ctx kapi.Context, grant *api.OAuthClientAuthorization) (*api.OAuthClientAuthorization, error) { r.CreatedAuthorization = grant - return r.ClientAuthorization, r.Err + return r.ClientAuthorization, r.CreateErr } func (r *ClientAuthorizationRegistry) UpdateClientAuthorization(ctx kapi.Context, grant *api.OAuthClientAuthorization) (*api.OAuthClientAuthorization, error) { r.UpdatedAuthorization = grant - return r.ClientAuthorization, r.Err + return r.ClientAuthorization, r.UpdateErr } func (r *ClientAuthorizationRegistry) DeleteClientAuthorization(ctx kapi.Context, name string) error { r.DeletedClientAuthorizationName = name - return r.Err + return r.DeleteErr } diff --git a/test/integration/sa_oauthclient_test.go b/test/integration/sa_oauthclient_test.go index b0765b7fba95..da636f48423c 100644 --- a/test/integration/sa_oauthclient_test.go +++ b/test/integration/sa_oauthclient_test.go @@ -2,6 +2,7 @@ package integration import ( "crypto/tls" + "fmt" "net/http" "net/http/cookiejar" "net/http/httptest" @@ -129,25 +130,6 @@ func TestSAAsOAuthClient(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - promptApprovalSuccess := []string{ - "GET /oauth/authorize", - "received challenge", - "GET /oauth/authorize", - "redirect to /oauth/approve", - "form", - "POST /oauth/approve", - "redirect to /oauth/authorize", - "redirect to /oauthcallback", - "code", - } - noPromptSuccess := []string{ - "GET /oauth/authorize", - "received challenge", - "GET /oauth/authorize", - "redirect to /oauthcallback", - "code", - } - // Test with a normal OAuth client { oauthClientConfig := &osincli.ClientConfig{ @@ -161,7 +143,18 @@ func TestSAAsOAuthClient(t *testing.T) { t.Log("Testing unrestricted scope") oauthClientConfig.Scope = "" // approval steps are needed for unscoped access - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, true, promptApprovalSuccess) + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauth/approve", + "form", + "POST /oauth/approve", + "redirect to /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:user:full", + }) // verify the persisted client authorization looks like we expect if clientAuth, err := clusterAdminClient.OAuthClientAuthorizations().Get("harold:" + oauthClientConfig.ClientId); err != nil { t.Fatalf("Unexpected error: %v", err) @@ -175,20 +168,90 @@ func TestSAAsOAuthClient(t *testing.T) { } } // approval steps are needed again for unscoped access - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, true, promptApprovalSuccess) + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauth/approve", + "form", + "POST /oauth/approve", + "redirect to /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:user:full", + }) // with the authorization stored, approval steps are skipped - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, true, noPromptSuccess) + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:user:full", + }) - // Approval step is needed again. Second time, the approval steps is not needed + // Approval step is needed again t.Log("Testing restricted scope") - oauthClientConfig.Scope = "user:info" - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, false, promptApprovalSuccess) - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, false, noPromptSuccess) + oauthClientConfig.Scope = "user:info user:check-access" + // filter to disapprove of granting the user:check-access scope + deniedScope := false + inputFilter := func(inputType, name, value string) bool { + if inputType == "checkbox" && name == "scope" && value == "user:check-access" { + deniedScope = true + return false + } + return true + } + // our token only gets the approved one + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, inputFilter, authorizationCodes, authorizationErrors, true, false, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauth/approve", + "form", + "POST /oauth/approve", + "redirect to /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:user:info", + }) + if !deniedScope { + t.Errorf("Expected form filter to deny user:info scope") + } + // second time, we approve all, and our token gets all requested scopes + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauth/approve", + "form", + "POST /oauth/approve", + "redirect to /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:" + oauthClientConfig.Scope, + }) + // third time, the approval steps is not needed, and the token gets all requested scopes + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:" + oauthClientConfig.Scope, + }) // Now request an unscoped token again, and no approval should be needed t.Log("Testing unrestricted scope") oauthClientConfig.Scope = "" - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, true, noPromptSuccess) + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:user:full", + }) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) } @@ -206,8 +269,26 @@ func TestSAAsOAuthClient(t *testing.T) { t.Log("Testing allowed scopes") // First time, the approval steps are needed // Second time, the approval steps are skipped - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, true, promptApprovalSuccess) - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, true, noPromptSuccess) + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauth/approve", + "form", + "POST /oauth/approve", + "redirect to /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:" + oauthClientConfig.Scope, + }) + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:" + oauthClientConfig.Scope, + }) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) } @@ -222,7 +303,7 @@ func TestSAAsOAuthClient(t *testing.T) { SendClientSecretInParams: true, } t.Log("Testing disallowed scopes") - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, false, false, []string{ + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, false, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", @@ -243,7 +324,7 @@ func TestSAAsOAuthClient(t *testing.T) { Scope: scope.Join([]string{"unknown-scope"}), SendClientSecretInParams: true, } - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, false, false, []string{ + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, false, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", @@ -266,8 +347,26 @@ func TestSAAsOAuthClient(t *testing.T) { } // First time, the approval is needed // Second time, the approval is skipped - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, false, promptApprovalSuccess) - runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, false, noPromptSuccess) + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauth/approve", + "form", + "POST /oauth/approve", + "redirect to /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:" + oauthClientConfig.Scope, + }) + runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{ + "GET /oauth/authorize", + "received challenge", + "GET /oauth/authorize", + "redirect to /oauthcallback", + "code", + "scope:" + oauthClientConfig.Scope, + }) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) } } @@ -300,6 +399,7 @@ func runOAuthFlow( clusterAdminClientConfig *restclient.Config, projectName string, oauthClientConfig *osincli.ClientConfig, + inputFilter htmlutil.InputFilterFunc, authorizationCodes chan string, authorizationErrors chan string, expectGrantSuccess bool, @@ -382,7 +482,7 @@ func runOAuthFlow( if len(forms) == 0 { break } - req, err = htmlutil.NewRequestFromForm(forms[0], currentURL) + req, err = htmlutil.NewRequestFromForm(forms[0], currentURL, inputFilter) if err != nil { t.Error(err) return @@ -400,10 +500,6 @@ func runOAuthFlow( t.Error("didn't get a code or an error") } - if !reflect.DeepEqual(operations, expectOperations) { - t.Errorf("Expected:\n%v\nGot\n%v", expectOperations, operations) - } - if len(authorizationCode) > 0 { accessRequest := oauthRuntimeClient.NewAccessRequest(osincli.AUTHORIZATION_CODE, &osincli.AuthorizeData{Code: authorizationCode}) accessData, err := accessRequest.GetToken() @@ -411,6 +507,7 @@ func runOAuthFlow( t.Errorf("unexpected error: %v", err) return } + operations = append(operations, fmt.Sprintf("scope:%v", accessData.ResponseData["scope"])) whoamiConfig := clientcmd.AnonymousClientConfig(clusterAdminClientConfig) whoamiConfig.BearerToken = accessData.AccessToken @@ -440,4 +537,9 @@ func runOAuthFlow( return } } + + if !reflect.DeepEqual(operations, expectOperations) { + t.Errorf("Expected:\n%#v\nGot\n%#v", expectOperations, operations) + } + } diff --git a/test/util/html/form.go b/test/util/html/form.go index a58b47410cc4..8592dcf771f0 100644 --- a/test/util/html/form.go +++ b/test/util/html/form.go @@ -36,11 +36,15 @@ func GetAttr(element *html.Node, attrName string) (string, bool) { return "", false } +type InputFilterFunc func(inputType, inputName, inputValue string) bool + // NewRequestFromForm builds a request that simulates submitting the given form. It honors: // Form method (defaults to GET) // Form action (defaults to currentURL if missing, or currentURL's scheme/host if server-relative) -// values (only the first type="submit" input's value is included) -func NewRequestFromForm(form *html.Node, currentURL *url.URL) (*http.Request, error) { +// Input values from elements +// * only the first type="submit" input's value is included +// * only radio and checkbox inputs with the "checked" attribute are included +func NewRequestFromForm(form *html.Node, currentURL *url.URL, filterFunc InputFilterFunc) (*http.Request, error) { var ( reqMethod string reqURL *url.URL @@ -84,17 +88,28 @@ func NewRequestFromForm(form *html.Node, currentURL *url.URL) (*http.Request, er for _, input := range GetElementsByTagName(form, "input") { if name, ok := GetAttr(input, "name"); ok { if value, ok := GetAttr(input, "value"); ok { - // Check if this is a submit input - if inputType, _ := GetAttr(input, "type"); inputType == "submit" { - // Only add the value of the first one. + inputType, _ := GetAttr(input, "type") + + // Allow skipping inputs + if filterFunc != nil && !filterFunc(inputType, name, value) { + continue + } + + switch inputType { + case "submit": + // If this is a submit input, only add the value of the first one. // We're simulating submitting the form. - if addedSubmit { - continue + if !addedSubmit { + formData.Add(name, value) + addedSubmit = true + } + case "radio", "checkbox": + if _, checked := GetAttr(input, "checked"); checked { + formData.Add(name, value) } - // Remember we've added a submit input - addedSubmit = true + default: + formData.Add(name, value) } - formData.Add(name, value) } } }