Skip to content

Commit 951c67b

Browse files
committed
add acting-as request handling
1 parent e1643c0 commit 951c67b

File tree

10 files changed

+867
-0
lines changed

10 files changed

+867
-0
lines changed

contrib/completions/bash/oadm

Lines changed: 67 additions & 0 deletions
Large diffs are not rendered by default.

contrib/completions/bash/oc

Lines changed: 154 additions & 0 deletions
Large diffs are not rendered by default.

contrib/completions/bash/openshift

Lines changed: 280 additions & 0 deletions
Large diffs are not rendered by default.

pkg/authorization/api/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ const (
2727
ServiceAccountKind = "ServiceAccount"
2828
SystemUserKind = "SystemUser"
2929
SystemGroupKind = "SystemGroup"
30+
31+
UserResource = "users"
32+
GroupResource = "groups"
33+
ServiceAccountResource = "serviceaccounts"
34+
SystemUserResource = "systemusers"
35+
SystemGroupResource = "systemgroups"
3036
)
3137

3238
const (

pkg/cmd/server/origin/handlers.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@ import (
1616
kapierrors "k8s.io/kubernetes/pkg/api/errors"
1717
"k8s.io/kubernetes/pkg/api/unversioned"
1818
"k8s.io/kubernetes/pkg/apiserver"
19+
"k8s.io/kubernetes/pkg/auth/user"
20+
"k8s.io/kubernetes/pkg/httplog"
1921
"k8s.io/kubernetes/pkg/runtime"
22+
"k8s.io/kubernetes/pkg/serviceaccount"
2023
"k8s.io/kubernetes/pkg/util/sets"
2124

25+
authorizationapi "github.com/openshift/origin/pkg/authorization/api"
2226
"github.com/openshift/origin/pkg/authorization/authorizer"
2327
configapi "github.com/openshift/origin/pkg/cmd/server/api"
28+
"github.com/openshift/origin/pkg/cmd/server/bootstrappolicy"
29+
userapi "github.com/openshift/origin/pkg/user/api"
30+
uservalidation "github.com/openshift/origin/pkg/user/api/validation"
2431
"github.com/openshift/origin/pkg/util/httprequest"
2532
)
2633

@@ -291,3 +298,120 @@ func assetServerRedirect(handler http.Handler, assetPublicURL string) http.Handl
291298
handler.ServeHTTP(w, req)
292299
})
293300
}
301+
302+
func (c *MasterConfig) impersonationFilter(handler http.Handler) http.Handler {
303+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
304+
requestedSubject := req.Header.Get("Impersonate-User")
305+
if len(requestedSubject) == 0 {
306+
handler.ServeHTTP(w, req)
307+
return
308+
}
309+
310+
resource, namespace, name, err := parseRequestedSubject(requestedSubject)
311+
if err != nil {
312+
forbidden(err.Error(), nil, w, req)
313+
return
314+
}
315+
316+
ctx, exists := c.RequestContextMapper.Get(req)
317+
if !exists {
318+
forbidden("context not found", nil, w, req)
319+
return
320+
}
321+
322+
actingAsAttributes := &authorizer.DefaultAuthorizationAttributes{
323+
Verb: "impersonate",
324+
APIGroup: resource.Group,
325+
Resource: resource.Resource,
326+
ResourceName: name,
327+
}
328+
authCheckCtx := kapi.WithNamespace(ctx, namespace)
329+
330+
allowed, reason, err := c.Authorizer.Authorize(authCheckCtx, actingAsAttributes)
331+
if err != nil {
332+
forbidden(err.Error(), actingAsAttributes, w, req)
333+
return
334+
}
335+
if !allowed {
336+
forbidden(reason, actingAsAttributes, w, req)
337+
return
338+
}
339+
340+
switch resource {
341+
case kapi.Resource(authorizationapi.ServiceAccountResource):
342+
newUser := &user.DefaultInfo{
343+
Name: serviceaccount.MakeUsername(namespace, name),
344+
Groups: serviceaccount.MakeGroupNames(namespace, name),
345+
}
346+
newUser.Groups = append(newUser.Groups, bootstrappolicy.AuthenticatedGroup)
347+
c.RequestContextMapper.Update(req, kapi.WithUser(ctx, newUser))
348+
349+
case userapi.Resource(authorizationapi.UserResource):
350+
newUser := &user.DefaultInfo{
351+
Name: name,
352+
}
353+
groups, err := c.GroupCache.GroupsFor(name)
354+
if err == nil {
355+
for _, group := range groups {
356+
newUser.Groups = append(newUser.Groups, group.Name)
357+
}
358+
}
359+
360+
newUser.Groups = append(newUser.Groups, bootstrappolicy.AuthenticatedGroup, bootstrappolicy.AuthenticatedOAuthGroup)
361+
c.RequestContextMapper.Update(req, kapi.WithUser(ctx, newUser))
362+
363+
case userapi.Resource(authorizationapi.SystemUserResource):
364+
newUser := &user.DefaultInfo{
365+
Name: name,
366+
}
367+
368+
if name == bootstrappolicy.UnauthenticatedUsername {
369+
newUser.Groups = append(newUser.Groups, bootstrappolicy.UnauthenticatedGroup)
370+
} else {
371+
newUser.Groups = append(newUser.Groups, bootstrappolicy.AuthenticatedGroup)
372+
}
373+
c.RequestContextMapper.Update(req, kapi.WithUser(ctx, newUser))
374+
375+
default:
376+
forbidden(fmt.Sprintf("%v is an unhandled resource for acting-as", resource), nil, w, req)
377+
return
378+
}
379+
380+
newCtx, _ := c.RequestContextMapper.Get(req)
381+
oldUser, _ := kapi.UserFrom(ctx)
382+
newUser, _ := kapi.UserFrom(newCtx)
383+
httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser)
384+
385+
handler.ServeHTTP(w, req)
386+
})
387+
}
388+
389+
func parseRequestedSubject(requestedSubject string) (unversioned.GroupResource, string, string, error) {
390+
subjects := authorizationapi.BuildSubjects([]string{requestedSubject}, nil,
391+
// validates whether the usernames are regular users or system users
392+
uservalidation.ValidateUserName,
393+
// validates group names, but we never pass any groups
394+
func(s string, b bool) (bool, string) { return true, "" })
395+
396+
if len(subjects) == 0 {
397+
return unversioned.GroupResource{}, "", "", fmt.Errorf("subject must be in the form of a username, not %v", requestedSubject)
398+
399+
}
400+
401+
resource := unversioned.GroupResource{}
402+
switch subjects[0].GetObjectKind().GroupVersionKind().GroupKind() {
403+
case userapi.Kind(authorizationapi.UserKind):
404+
resource = userapi.Resource(authorizationapi.UserResource)
405+
406+
case userapi.Kind(authorizationapi.SystemUserKind):
407+
resource = userapi.Resource(authorizationapi.SystemUserResource)
408+
409+
case kapi.Kind(authorizationapi.ServiceAccountKind):
410+
resource = kapi.Resource(authorizationapi.ServiceAccountResource)
411+
412+
default:
413+
return unversioned.GroupResource{}, "", "", fmt.Errorf("unknown subject type: %v", subjects[0])
414+
}
415+
416+
return resource, subjects[0].Namespace, subjects[0].Name, nil
417+
}

pkg/cmd/server/origin/handlers_test.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,245 @@
11
package origin
22

33
import (
4+
"fmt"
45
"io/ioutil"
56
"net/http"
67
"net/http/httptest"
8+
"reflect"
79
"strings"
10+
"sync"
811
"testing"
912

13+
kapi "k8s.io/kubernetes/pkg/api"
14+
"k8s.io/kubernetes/pkg/auth/user"
15+
"k8s.io/kubernetes/pkg/util/sets"
16+
"k8s.io/kubernetes/pkg/watch"
17+
18+
"github.com/openshift/origin/pkg/authorization/authorizer"
1019
configapi "github.com/openshift/origin/pkg/cmd/server/api"
20+
userapi "github.com/openshift/origin/pkg/user/api"
21+
usercache "github.com/openshift/origin/pkg/user/cache"
1122
)
1223

24+
type impersonateAuthorizer struct{}
25+
26+
func (impersonateAuthorizer) Authorize(ctx kapi.Context, a authorizer.AuthorizationAttributes) (allowed bool, reason string, err error) {
27+
user, exists := kapi.UserFrom(ctx)
28+
if !exists {
29+
return false, "missing user", nil
30+
}
31+
32+
switch {
33+
case user.GetName() == "system:admin":
34+
return true, "", nil
35+
36+
case user.GetName() == "tester":
37+
return false, "", fmt.Errorf("works on my machine")
38+
39+
case user.GetName() == "deny-me":
40+
return false, "denied", nil
41+
}
42+
43+
if len(user.GetGroups()) == 1 && user.GetGroups()[0] == "wheel" && a.GetVerb() == "impersonate" && a.GetResource() == "systemusers" {
44+
return true, "", nil
45+
}
46+
47+
if len(user.GetGroups()) == 1 && user.GetGroups()[0] == "sa-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "serviceaccounts" {
48+
return true, "", nil
49+
}
50+
51+
if len(user.GetGroups()) == 1 && user.GetGroups()[0] == "regular-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "users" {
52+
return true, "", nil
53+
}
54+
55+
return false, "deny by default", nil
56+
}
57+
58+
func (impersonateAuthorizer) GetAllowedSubjects(ctx kapi.Context, attributes authorizer.AuthorizationAttributes) (sets.String, sets.String, error) {
59+
return nil, nil, nil
60+
}
61+
62+
type groupCache struct {
63+
}
64+
65+
func (*groupCache) ListGroups(ctx kapi.Context, options *kapi.ListOptions) (*userapi.GroupList, error) {
66+
return &userapi.GroupList{}, nil
67+
}
68+
func (*groupCache) GetGroup(ctx kapi.Context, name string) (*userapi.Group, error) {
69+
return nil, nil
70+
}
71+
func (*groupCache) CreateGroup(ctx kapi.Context, group *userapi.Group) (*userapi.Group, error) {
72+
return nil, nil
73+
}
74+
func (*groupCache) UpdateGroup(ctx kapi.Context, group *userapi.Group) (*userapi.Group, error) {
75+
return nil, nil
76+
}
77+
func (*groupCache) DeleteGroup(ctx kapi.Context, name string) error {
78+
return nil
79+
}
80+
func (*groupCache) WatchGroups(ctx kapi.Context, options *kapi.ListOptions) (watch.Interface, error) {
81+
return watch.NewFake(), nil
82+
}
83+
84+
func TestImpersonationFilter(t *testing.T) {
85+
testCases := []struct {
86+
name string
87+
user user.Info
88+
impersonationString string
89+
expectedUser user.Info
90+
expectedCode int
91+
}{
92+
{
93+
name: "not-impersonating",
94+
user: &user.DefaultInfo{
95+
Name: "tester",
96+
},
97+
expectedUser: &user.DefaultInfo{
98+
Name: "tester",
99+
},
100+
expectedCode: http.StatusOK,
101+
},
102+
{
103+
name: "impersonating-error",
104+
user: &user.DefaultInfo{
105+
Name: "tester",
106+
},
107+
impersonationString: "anyone",
108+
expectedUser: &user.DefaultInfo{
109+
Name: "tester",
110+
},
111+
expectedCode: http.StatusForbidden,
112+
},
113+
{
114+
name: "allowed-systemusers-impersonation",
115+
user: &user.DefaultInfo{
116+
Name: "dev",
117+
Groups: []string{"wheel"},
118+
},
119+
impersonationString: "system:admin",
120+
expectedUser: &user.DefaultInfo{
121+
Name: "system:admin",
122+
Groups: []string{"system:authenticated"},
123+
},
124+
expectedCode: http.StatusOK,
125+
},
126+
{
127+
name: "allowed-users-impersonation",
128+
user: &user.DefaultInfo{
129+
Name: "dev",
130+
Groups: []string{"regular-impersonater"},
131+
},
132+
impersonationString: "tester",
133+
expectedUser: &user.DefaultInfo{
134+
Name: "tester",
135+
Groups: []string{"system:authenticated", "system:authenticated:oauth"},
136+
},
137+
expectedCode: http.StatusOK,
138+
},
139+
{
140+
name: "disallowed-impersonating",
141+
user: &user.DefaultInfo{
142+
Name: "dev",
143+
Groups: []string{"sa-impersonater"},
144+
},
145+
impersonationString: "tester",
146+
expectedUser: &user.DefaultInfo{
147+
Name: "dev",
148+
Groups: []string{"sa-impersonater"},
149+
},
150+
expectedCode: http.StatusForbidden,
151+
},
152+
{
153+
name: "allowed-sa-impersonating",
154+
user: &user.DefaultInfo{
155+
Name: "dev",
156+
Groups: []string{"sa-impersonater"},
157+
},
158+
impersonationString: "system:serviceaccount:foo:default",
159+
expectedUser: &user.DefaultInfo{
160+
Name: "system:serviceaccount:foo:default",
161+
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:foo", "system:authenticated"},
162+
},
163+
expectedCode: http.StatusOK,
164+
},
165+
}
166+
167+
config := MasterConfig{}
168+
config.RequestContextMapper = kapi.NewRequestContextMapper()
169+
config.Authorizer = impersonateAuthorizer{}
170+
config.GroupCache = usercache.NewGroupCache(&groupCache{})
171+
var ctx kapi.Context
172+
var actualUser user.Info
173+
var lock sync.Mutex
174+
175+
doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
176+
currentCtx, _ := config.RequestContextMapper.Get(req)
177+
user, exists := kapi.UserFrom(currentCtx)
178+
if !exists {
179+
actualUser = nil
180+
return
181+
}
182+
183+
actualUser = user
184+
})
185+
handler := func(delegate http.Handler) http.Handler {
186+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
187+
defer func() {
188+
if r := recover(); r != nil {
189+
t.Errorf("Recovered %v", r)
190+
}
191+
}()
192+
lock.Lock()
193+
defer lock.Unlock()
194+
config.RequestContextMapper.Update(req, ctx)
195+
currentCtx, _ := config.RequestContextMapper.Get(req)
196+
197+
user, exists := kapi.UserFrom(currentCtx)
198+
if !exists {
199+
actualUser = nil
200+
return
201+
} else {
202+
actualUser = user
203+
}
204+
205+
delegate.ServeHTTP(w, req)
206+
})
207+
}(config.impersonationFilter(doNothingHandler))
208+
handler, _ = kapi.NewRequestContextFilter(config.RequestContextMapper, handler)
209+
210+
server := httptest.NewServer(handler)
211+
defer server.Close()
212+
213+
for _, tc := range testCases {
214+
func() {
215+
lock.Lock()
216+
defer lock.Unlock()
217+
ctx = kapi.WithUser(kapi.NewContext(), tc.user)
218+
}()
219+
220+
req, err := http.NewRequest("GET", server.URL, nil)
221+
if err != nil {
222+
t.Errorf("%s: unexpected error: %v", tc.name, err)
223+
continue
224+
}
225+
req.Header.Add("Impersonate-User", tc.impersonationString)
226+
resp, err := http.DefaultClient.Do(req)
227+
if err != nil {
228+
t.Errorf("%s: unexpected error: %v", tc.name, err)
229+
continue
230+
}
231+
if resp.StatusCode != tc.expectedCode {
232+
t.Errorf("%s: expected %v, actual %v", tc.name, tc.expectedCode, resp.StatusCode)
233+
continue
234+
}
235+
236+
if !reflect.DeepEqual(actualUser, tc.expectedUser) {
237+
t.Errorf("%s: expected %#v, actual %#v", tc.name, tc.expectedUser, actualUser)
238+
continue
239+
}
240+
}
241+
}
242+
13243
var (
14244
currentOCKubeResources = "oc/v1.2.0 (linux/amd64) kubernetes/bc4550d"
15245
currentOCOriginResources = "oc/v1.1.3 (linux/amd64) openshift/b348c2f"

0 commit comments

Comments
 (0)