Skip to content

Commit b6c1036

Browse files
committed
EVEREST-1799 get groups claim and validate permissions
1 parent 4b1722e commit b6c1036

File tree

6 files changed

+243
-48
lines changed

6 files changed

+243
-48
lines changed

internal/server/handlers/rbac/backup_storage_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func TestRBAC_BackupStorage(t *testing.T) {
9696
},
9797
}
9898

99-
ctx := context.WithValue(context.Background(), common.UserCtxKey, "test-user")
99+
ctx := context.WithValue(context.Background(), common.UserCtxKey, rbac.User{Subject: "test-user"})
100100
for _, tc := range testCases {
101101
t.Run(tc.desc, func(t *testing.T) {
102102
t.Parallel()
@@ -174,7 +174,7 @@ func TestRBAC_BackupStorage(t *testing.T) {
174174
},
175175
}
176176

177-
ctx := context.WithValue(context.Background(), common.UserCtxKey, "test-user")
177+
ctx := context.WithValue(context.Background(), common.UserCtxKey, rbac.User{Subject: "test-user"})
178178
for _, tc := range testCases {
179179
t.Run(tc.desc, func(t *testing.T) {
180180
t.Parallel()
@@ -216,10 +216,10 @@ func newConfigMapPolicy(policy string) *corev1.ConfigMap {
216216
}
217217
}
218218

219-
func testUserGetter(ctx context.Context) (string, error) {
220-
user, ok := ctx.Value(common.UserCtxKey).(string)
219+
func testUserGetter(ctx context.Context) (rbac.User, error) {
220+
user, ok := ctx.Value(common.UserCtxKey).(rbac.User)
221221
if !ok {
222-
return "", errors.New("user not found in context")
222+
return rbac.User{}, errors.New("user not found in context")
223223
}
224224
return user, nil
225225
}

internal/server/handlers/rbac/handler.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type rbacHandler struct {
2121
enforcer casbin.IEnforcer
2222
log *zap.SugaredLogger
2323
next handlers.Handler
24-
userGetter func(ctx context.Context) (string, error)
24+
userGetter func(ctx context.Context) (rbac.User, error)
2525
}
2626

2727
// New returns a new RBAC handler.
@@ -55,17 +55,23 @@ func (h *rbacHandler) enforce(
5555
action,
5656
object string,
5757
) error {
58-
subject, err := h.userGetter(ctx)
58+
user, err := h.userGetter(ctx)
5959
if err != nil {
6060
return err
6161
}
62-
ok, err := h.enforcer.Enforce(subject, resource, action, object)
63-
if err != nil {
64-
return fmt.Errorf("enforce error: %w", err)
65-
}
66-
if !ok {
67-
h.log.Warnf("Permission denied: [%s %s %s %s]", subject, resource, action, object)
68-
return ErrInsufficientPermissions
62+
63+
// User is allowed to perform the operation if the user's subjects or any
64+
// of its groups have the required permission.
65+
for _, sub := range append([]string{user.Subject}, user.Groups...) {
66+
ok, err := h.enforcer.Enforce(sub, resource, action, object)
67+
if err != nil {
68+
return fmt.Errorf("enforce error: %w", err)
69+
}
70+
if ok {
71+
return nil
72+
}
6973
}
70-
return nil
74+
75+
h.log.Warnf("Permission denied: [%s %s %s %s]", user.Subject, resource, action, object)
76+
return ErrInsufficientPermissions
7177
}

internal/server/handlers/rbac/kubernetes.go

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package rbac
33

44
import (
55
"context"
6-
"errors"
76
"fmt"
87

98
"github.com/AlekSi/pointer"
@@ -24,15 +23,39 @@ func (h *rbacHandler) GetUserPermissions(ctx context.Context) (*api.UserPermissi
2423
if err != nil {
2524
return nil, err
2625
}
27-
perms, err := h.enforcer.GetImplicitPermissionsForUser(user)
28-
if err != nil {
29-
return nil, fmt.Errorf("failed to GetImplicitPermissionsForUser: %w", err)
26+
27+
// Let's use a map to deduplicate the permissions after resolving all roles
28+
permsMap := make(map[[4]string]struct{})
29+
30+
// Get permissions for the user and the groups it belongs to
31+
for _, sub := range append([]string{user.Subject}, user.Groups...) {
32+
perms, err := h.enforcer.GetImplicitPermissionsForUser(sub)
33+
if err != nil {
34+
return nil, fmt.Errorf("failed to GetImplicitPermissionsForUser: %w", err)
35+
}
36+
37+
// GetImplicitPermissionsForUser returns all policies assigned to the
38+
// user/group directly as well as the policies assigned to the roles
39+
// the user/group has. We need to resolve all roles for the user.
40+
for _, perm := range perms {
41+
if len(perm) != 4 {
42+
// This should never happen, but let's be safe
43+
return nil, fmt.Errorf("invalid permission")
44+
}
45+
46+
// We don't want to expose the groups or roles in the permissions
47+
// so we replace them with the user
48+
permsMap[[4]string{user.Subject, perm[1], perm[2], perm[3]}] = struct{}{}
49+
}
3050
}
3151

32-
if err := h.resolveRoles(user, perms); err != nil {
33-
return nil, err
52+
// Convert the map back to a slice
53+
result := make([][]string, len(permsMap))
54+
i := 0
55+
for k := range permsMap {
56+
result[i] = []string(k[:])
57+
i++
3458
}
35-
result := pointer.To(perms)
3659

3760
nextRes, err := h.next.GetUserPermissions(ctx)
3861
if err != nil {
@@ -45,25 +68,8 @@ func (h *rbacHandler) GetUserPermissions(ctx context.Context) (*api.UserPermissi
4568
}
4669

4770
res := &api.UserPermissions{
48-
Permissions: result,
71+
Permissions: pointer.To(result),
4972
Enabled: enabled,
5073
}
5174
return res, nil
5275
}
53-
54-
// For a given set of `permissions` for a `user`, this function
55-
// will resolve all roles for the user.
56-
func (h *rbacHandler) resolveRoles(user string, permissions [][]string) error {
57-
userRoles, err := h.enforcer.GetRolesForUser(user)
58-
if err != nil {
59-
return errors.Join(err, errors.New("cannot get user roles"))
60-
}
61-
for _, role := range userRoles {
62-
for i, perm := range permissions {
63-
if perm[0] == role {
64-
permissions[i][0] = user
65-
}
66-
}
67-
}
68-
return nil
69-
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package rbac
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/mock"
9+
"github.com/stretchr/testify/require"
10+
"go.uber.org/zap"
11+
12+
"github.com/percona/everest/api"
13+
"github.com/percona/everest/internal/server/handlers"
14+
"github.com/percona/everest/pkg/common"
15+
"github.com/percona/everest/pkg/rbac"
16+
)
17+
18+
func TestRBAC_Kubernetes(t *testing.T) {
19+
t.Parallel()
20+
21+
data := func() *handlers.MockHandler {
22+
next := handlers.MockHandler{}
23+
next.On("GetUserPermissions",
24+
mock.Anything,
25+
).Return(
26+
&api.UserPermissions{
27+
Enabled: true,
28+
},
29+
nil,
30+
)
31+
return &next
32+
}
33+
34+
t.Run("GetUserPermissions", func(t *testing.T) {
35+
t.Parallel()
36+
37+
testCases := []struct {
38+
desc string
39+
user rbac.User
40+
policy string
41+
outPerms [][]string
42+
}{
43+
{
44+
desc: "default admin permissions",
45+
user: rbac.User{
46+
Subject: "test-user",
47+
},
48+
policy: newPolicy(
49+
"g, test-user, role:admin",
50+
),
51+
outPerms: [][]string{
52+
{"test-user", "monitoring-instances", "*", "*/*"},
53+
{"test-user", "database-cluster-backups", "*", "*/*"},
54+
{"test-user", "database-cluster-restores", "*", "*/*"},
55+
{"test-user", "database-clusters", "*", "*/*"},
56+
{"test-user", "database-cluster-credentials", "*", "*/*"},
57+
{"test-user", "database-engines", "*", "*/*"},
58+
{"test-user", "namespaces", "*", "*"},
59+
{"test-user", "backup-storages", "*", "*/*"},
60+
},
61+
},
62+
{
63+
desc: "permissions from different roles are merged",
64+
user: rbac.User{
65+
Subject: "test-user",
66+
},
67+
policy: newPolicy(
68+
"p, test-user, database-clusters, *, */*",
69+
"p, role:creater, database-clusters, create, */*",
70+
"p, role:reader, database-clusters, read, */*",
71+
"p, role:updater, database-clusters, update, */*",
72+
"p, role:deleter, database-clusters, delete, */*",
73+
"g, test-user, role:creater",
74+
"g, test-user, role:reader",
75+
"g, test-user, role:updater",
76+
"g, another-user, role:deleter",
77+
),
78+
outPerms: [][]string{
79+
{"test-user", "database-clusters", "*", "*/*"},
80+
{"test-user", "database-clusters", "create", "*/*"},
81+
{"test-user", "database-clusters", "read", "*/*"},
82+
{"test-user", "database-clusters", "update", "*/*"},
83+
},
84+
},
85+
{
86+
desc: "permissions from different groups are merged",
87+
user: rbac.User{
88+
Subject: "test-user",
89+
Groups: []string{"test-group-1", "test-group-2"},
90+
},
91+
policy: newPolicy(
92+
"p, test-user, database-clusters, read, */*",
93+
"p, test-group-1, database-clusters, create, */*",
94+
"p, test-group-2, database-clusters, update, */*",
95+
"p, test-group-3, database-clusters, delete, */*",
96+
),
97+
outPerms: [][]string{
98+
{"test-user", "database-clusters", "read", "*/*"},
99+
{"test-user", "database-clusters", "create", "*/*"},
100+
{"test-user", "database-clusters", "update", "*/*"},
101+
},
102+
},
103+
{
104+
desc: "duplicate permissions are removed",
105+
user: rbac.User{
106+
Subject: "test-user",
107+
},
108+
policy: newPolicy(
109+
"p, test-user, database-clusters, *, */*",
110+
"p, role:test, database-clusters, *, */*",
111+
"g, test-user, role:test",
112+
),
113+
outPerms: [][]string{
114+
{"test-user", "database-clusters", "*", "*/*"},
115+
},
116+
},
117+
{
118+
desc: "no policy",
119+
user: rbac.User{
120+
Subject: "test-user",
121+
},
122+
policy: newPolicy(),
123+
outPerms: [][]string{},
124+
},
125+
}
126+
127+
for _, tc := range testCases {
128+
ctx := context.WithValue(context.Background(), common.UserCtxKey, tc.user)
129+
t.Run(tc.desc, func(t *testing.T) {
130+
t.Parallel()
131+
k8sMock := newConfigMapMock(tc.policy)
132+
enf, err := rbac.NewEnforcer(ctx, k8sMock, zap.NewNop().Sugar())
133+
require.NoError(t, err)
134+
next := data()
135+
136+
h := &rbacHandler{
137+
next: next,
138+
log: zap.NewNop().Sugar(),
139+
enforcer: enf,
140+
userGetter: testUserGetter,
141+
}
142+
143+
perms, err := h.GetUserPermissions(ctx)
144+
require.NoError(t, err)
145+
assert.True(t, perms.Enabled)
146+
assert.ElementsMatch(t, tc.outPerms, *perms.Permissions)
147+
})
148+
}
149+
})
150+
}

pkg/rbac/rbac.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ const (
6868
rbacEnabledValueTrue = "true"
6969
)
7070

71+
type User struct {
72+
Subject string
73+
Groups []string
74+
}
75+
7176
// Setup a new informer that watches our RBAC ConfigMap.
7277
// This informer reloads the policy whenever the ConfigMap is updated.
7378
func refreshEnforcerInBackground(
@@ -178,31 +183,59 @@ func NewEnforcer(ctx context.Context, kubeClient kubernetes.KubernetesConnector,
178183
}
179184

180185
// GetUser extracts the user from the JWT token in the context.
181-
func GetUser(ctx context.Context) (string, error) {
186+
func GetUser(ctx context.Context) (User, error) {
182187
token, ok := ctx.Value(common.UserCtxKey).(*jwt.Token)
183188
if !ok {
184-
return "", errors.New("failed to get token from context")
189+
return User{}, errors.New("failed to get token from context")
185190
}
186191

187192
claims, ok := token.Claims.(jwt.MapClaims) // by default claims is of type `jwt.MapClaims`
188193
if !ok {
189-
return "", errors.New("failed to get claims from token")
194+
return User{}, errors.New("failed to get claims from token")
190195
}
191196

192197
subject, err := claims.GetSubject()
193198
if err != nil {
194-
return "", errors.Join(err, errors.New("failed to get subject from claims"))
199+
return User{}, errors.Join(err, errors.New("failed to get subject from claims"))
195200
}
196201

197202
issuer, err := claims.GetIssuer()
198203
if err != nil {
199-
return "", errors.Join(err, errors.New("failed to get issuer from claims"))
204+
return User{}, errors.Join(err, errors.New("failed to get issuer from claims"))
200205
}
201206

202207
if issuer == session.SessionManagerClaimsIssuer {
203-
return strings.Split(subject, ":")[0], nil
208+
subject = strings.Split(subject, ":")[0]
209+
}
210+
211+
groups := getScopeValues(claims, []string{"groups"})
212+
return User{Subject: subject, Groups: groups}, nil
213+
}
214+
215+
func getScopeValues(claims jwt.MapClaims, scopes []string) []string {
216+
groups := []string{}
217+
for i := range scopes {
218+
scopeIf, ok := claims[scopes[i]]
219+
if !ok {
220+
continue
221+
}
222+
223+
switch val := scopeIf.(type) {
224+
case []interface{}:
225+
for _, groupIf := range val {
226+
group, ok := groupIf.(string)
227+
if ok {
228+
groups = append(groups, group)
229+
}
230+
}
231+
case []string:
232+
groups = append(groups, val...)
233+
case string:
234+
groups = append(groups, val)
235+
}
204236
}
205-
return subject, nil
237+
238+
return groups
206239
}
207240

208241
func loadAdminPolicy(enf casbin.IEnforcer) error {

ui/apps/everest/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const App = () => {
7272
oidcConfig={{
7373
...configs?.oidc,
7474
redirectUri: `${window.location.protocol}//${window.location.host}/login-callback`,
75-
scope: 'openid profile email',
75+
scope: 'openid profile email groups',
7676
responseType: 'code',
7777
autoSignIn: false,
7878
automaticSilentRenew: false,

0 commit comments

Comments
 (0)