Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
feat: filter project with authn user
Browse files Browse the repository at this point in the history
Signed-off-by: thxCode <[email protected]>
  • Loading branch information
thxCode committed Apr 19, 2024
1 parent 8625a7e commit 49d3176
Show file tree
Hide file tree
Showing 20 changed files with 415 additions and 672 deletions.
2 changes: 1 addition & 1 deletion pkg/extensionapis/walrus/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (h *EnvironmentHandler) SetupHandler(
return []string{obj.GetName()}
})
if err != nil {
return
return gvr, srs, fmt.Errorf("index namespace 'metadata.name': %w", err)
}

// Declare GVR.
Expand Down
178 changes: 168 additions & 10 deletions pkg/extensionapis/walrus/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@ package walrus

import (
"context"
"errors"
"fmt"
"slices"
"sort"

"github.com/seal-io/utils/pools/gopool"
"github.com/seal-io/utils/stringx"
core "k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/watch"
authnuser "k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
ctrlcli "sigs.k8s.io/controller-runtime/pkg/client"

walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1"
"github.com/seal-io/walrus/pkg/extensionapi"
"github.com/seal-io/walrus/pkg/kubemeta"
"github.com/seal-io/walrus/pkg/kubereviewsubject"
"github.com/seal-io/walrus/pkg/systemauthz"
"github.com/seal-io/walrus/pkg/systemkuberes"
"github.com/seal-io/walrus/pkg/systemmeta"
Expand Down Expand Up @@ -54,7 +60,24 @@ func (h *ProjectHandler) SetupHandler(
return []string{obj.GetName()}
})
if err != nil {
return
return gvr, srs, fmt.Errorf("index namespace 'metadata.name': %w", err)
}
err = fi.IndexField(ctx, &rbac.RoleBinding{}, "rolebindings[scope=project].subject",
func(obj ctrlcli.Object) []string {
if obj == nil {
return nil
}
resType, notes := systemmeta.DescribeResource(obj)
if resType != "rolebindings" {
return nil
}
if notes["scope"] != "project" {
return nil
}
return []string{notes["subject"]}
})
if err != nil {
return gvr, srs, fmt.Errorf("index role binding 'rolebindings[scope=project].subject': %w", err)
}

// Declare GVR.
Expand All @@ -72,7 +95,7 @@ func (h *ProjectHandler) SetupHandler(
JSONPath: ".status.phase",
})
if err != nil {
return
return gvr, srs, err
}
}

Expand All @@ -89,7 +112,7 @@ func (h *ProjectHandler) SetupHandler(
"subjects": newProjectSubjectsHandler(opts),
}

return
return gvr, srs, err
}

var (
Expand Down Expand Up @@ -191,6 +214,11 @@ func (h *ProjectHandler) NewList() runtime.Object {
}

func (h *ProjectHandler) OnList(ctx context.Context, opts ctrlcli.ListOptions) (runtime.Object, error) {
ui, ok := genericapirequest.UserFrom(ctx)
if !ok {
return nil, kerrors.NewForbidden(walrus.SchemeResource("projects"), "", errors.New("request user not found"))
}

// List.
nsList := new(core.NamespaceList)
err := h.APIReader.List(ctx, nsList,
Expand All @@ -199,14 +227,82 @@ func (h *ProjectHandler) OnList(ctx context.Context, opts ctrlcli.ListOptions) (
return nil, err
}

// TODO Validate RBAC

// Convert.
pList := convertProjectListFromNamespaceList(nsList, opts)
return pList, nil
return h.filterProjectList(ctx, ui, pList), nil
}

func (h *ProjectHandler) filterProjectList(ctx context.Context, ui authnuser.Info, pList *walrus.ProjectList) *walrus.ProjectList {
// Fast-path: check with well-known admin user.
if systemauthz.IsWellKnownAdminUser(ui) {
return pList
}

// Slow-path: check with a subject access review.
revs := kubereviewsubject.Reviews{
{
ResourceAttributes: &kubereviewsubject.ResourceAttributes{
Group: walrus.GroupName,
Resource: "projects",
Verb: "list",
},
},
}
err := kubereviewsubject.CanSpecificUserDoWithCtrlClient(ctx, h.Client, revs, ui)
if err == nil {
return pList
}

// Slower-path: check with informer cache.
{
subjNamespace, subjName, ok := systemauthz.ConvertSubjectNamesFromAuthnUser(ui)
if ok {
rbList := new(rbac.RoleBindingList)
err = h.Client.List(ctx, rbList, ctrlcli.MatchingFields{
"rolebindings[scope=project].subject": subjNamespace + "/" + subjName,
})
if err == nil {
allowProjects := sets.New[string]()
for _, rb := range rbList.Items {
allowProjects.Insert(rb.Namespace)
}
pList.Items = slices.DeleteFunc(pList.Items, func(proj walrus.Project) bool {
return !allowProjects.Has(proj.Name)
})
return pList
}
}
}

// Slowest-path: check with multiple subject access reviews.
items := pList.Items
pList.Items = pList.Items[:0]
for i := range items {
revs = kubereviewsubject.Reviews{
{
ResourceAttributes: &kubereviewsubject.ResourceAttributes{
Group: walrus.GroupName,
Resource: "projects",
Namespace: items[i].Name,
Verb: "list",
},
},
}
err := kubereviewsubject.CanSpecificUserDoWithCtrlClient(ctx, h.Client, revs, ui)
if err != nil {
continue
}
pList.Items = append(pList.Items, items[i])
}
return pList
}

func (h *ProjectHandler) OnWatch(ctx context.Context, opts ctrlcli.ListOptions) (watch.Interface, error) {
ui, ok := genericapirequest.UserFrom(ctx)
if !ok {
return nil, kerrors.NewForbidden(walrus.SchemeResource("projects"), "", errors.New("request user not found"))
}

// Watch.
uw, err := h.Client.(ctrlcli.WithWatch).Watch(ctx, new(core.NamespaceList),
convertNamespaceListOptsFromProjectListOpts(opts))
Expand Down Expand Up @@ -240,8 +336,6 @@ func (h *ProjectHandler) OnWatch(ctx context.Context, opts ctrlcli.ListOptions)
continue
}

// TODO RBAC

// Type assert.
ns, ok := e.Object.(*core.Namespace)
if !ok {
Expand All @@ -262,6 +356,12 @@ func (h *ProjectHandler) OnWatch(ctx context.Context, opts ctrlcli.ListOptions)
continue
}

// Filter.
proj = h.filterProjectWatch(ctx, ui, proj)
if proj == nil {
continue
}

// Ignore if not be selected by `kubectl get --field-selector=metadata.namespace=...`.
if fs := opts.FieldSelector; fs != nil &&
!fs.Matches(fields.Set{"metadata.namespace": proj.Namespace, "metadata.name": proj.Name}) {
Expand All @@ -278,7 +378,36 @@ func (h *ProjectHandler) OnWatch(ctx context.Context, opts ctrlcli.ListOptions)
return dw, nil
}

func (h *ProjectHandler) filterProjectWatch(ctx context.Context, ui authnuser.Info, proj *walrus.Project) *walrus.Project {
// Fast-path: check with well-known admin user.
if systemauthz.IsWellKnownAdminUser(ui) {
return proj
}

// Slow-path: check with a subject access review.
revs := kubereviewsubject.Reviews{
{
ResourceAttributes: &kubereviewsubject.ResourceAttributes{
Group: walrus.GroupName,
Resource: "projects",
Namespace: proj.Name,
Verb: "watch",
},
},
}
err := kubereviewsubject.CanSpecificUserDoWithCtrlClient(ctx, h.Client, revs, ui)
if err == nil {
return proj
}
return nil
}

func (h *ProjectHandler) OnGet(ctx context.Context, key types.NamespacedName, opts ctrlcli.GetOptions) (runtime.Object, error) {
ui, ok := genericapirequest.UserFrom(ctx)
if !ok {
return nil, kerrors.NewForbidden(walrus.SchemeResource("projects"), "", errors.New("request user not found"))
}

// Validate.
if key.Namespace != systemkuberes.SystemNamespaceName {
return nil, kerrors.NewNotFound(walrus.SchemeResource("projects"), key.Name)
Expand All @@ -295,16 +424,45 @@ func (h *ProjectHandler) OnGet(ctx context.Context, key types.NamespacedName, op
return nil, err
}

// TODO Validate RBAC

// Convert.
proj := convertProjectFromNamespace(ns)
if proj == nil {
return nil, kerrors.NewNotFound(walrus.SchemeResource("projects"), key.Name)
}

// Filter.
proj = h.filterProjectGet(ctx, ui, proj)
if proj == nil {
return nil, kerrors.NewNotFound(walrus.SchemeResource("projects"), key.Name)
}

return proj, nil
}

func (h *ProjectHandler) filterProjectGet(ctx context.Context, ui authnuser.Info, proj *walrus.Project) *walrus.Project {
// Fast-path: check with well-known admin user.
if systemauthz.IsWellKnownAdminUser(ui) {
return proj
}

// Slow-path: check with a subject access review.
revs := kubereviewsubject.Reviews{
{
ResourceAttributes: &kubereviewsubject.ResourceAttributes{
Group: walrus.GroupName,
Resource: "projects",
Namespace: proj.Name,
Verb: "get",
},
},
}
err := kubereviewsubject.CanSpecificUserDoWithCtrlClient(ctx, h.Client, revs, ui)
if err == nil {
return proj
}
return nil
}

func (h *ProjectHandler) OnUpdate(ctx context.Context, obj, _ runtime.Object, opts ctrlcli.UpdateOptions) (runtime.Object, error) {
// Validate.
proj := obj.(*walrus.Project)
Expand Down
23 changes: 16 additions & 7 deletions pkg/extensionapis/walrus/project.subjects.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package walrus
import (
"context"
"fmt"
"slices"

"golang.org/x/exp/maps"
rbac "k8s.io/api/rbac/v1"
Expand Down Expand Up @@ -100,21 +101,26 @@ func (h *ProjectSubjectsHandler) OnGet(ctx context.Context, key types.Namespaced

func (h *ProjectSubjectsHandler) OnUpdate(ctx context.Context, obj, objOld runtime.Object, _ ctrlcli.UpdateOptions) (runtime.Object, error) {
psbjs, psbjsOld := obj.(*walrus.ProjectSubjects), objOld.(*walrus.ProjectSubjects)
subjRoleMap := make(map[walrus.SubjectReference]walrus.SubjectRole)

// Validate.
// Validate and map.
{
var errs field.ErrorList
for i, psbj := range psbjs.Items {
err := h.Client.Get(ctx, psbj.ToNamespacedName(), new(walrus.Subject))
err := psbj.Role.Validate()
if err != nil {
errs = append(errs, field.Invalid(
field.NewPath(fmt.Sprintf("items[%d]", i)), psbj.SubjectReference, err.Error()),
)
field.NewPath(fmt.Sprintf("items[%d].role", i)), psbj.Role, err.Error()))
}
if err := psbj.Role.Validate(); err != nil {
subj := new(walrus.Subject)
err = h.Client.Get(ctx, psbj.ToNamespacedName(), subj)
if err != nil {
errs = append(errs, field.Invalid(
field.NewPath(fmt.Sprintf("items[%d].role", i)), psbj.Role, err.Error()))
field.NewPath(fmt.Sprintf("items[%d]", i)), psbj.SubjectReference, err.Error()),
)
continue
}
subjRoleMap[psbj.SubjectReference] = subj.Spec.Role
}
if len(errs) > 0 {
return nil, kerrors.NewInvalid(walrus.SchemeKind("projectsubjects"), psbjs.Name, errs)
Expand Down Expand Up @@ -150,7 +156,10 @@ func (h *ProjectSubjectsHandler) OnUpdate(ctx context.Context, obj, objOld runti

// NB(thxCode): we grant the new permission to the Project namespace first,
// then ProjectSubjectAuthzReconciler will take care of granting the new permission to the Environment namespace.
psbjs.Items = maps.Keys(psbjsReverseIndex)
psbjs.Items = slices.DeleteFunc(maps.Keys(psbjsReverseIndex), func(psbj walrus.ProjectSubject) bool {
// Only grant to subject who is not an admin.
return subjRoleMap[psbj.SubjectReference] == walrus.SubjectRoleAdmin
})
err = systemauthz.GrantProjectSubjects(ctx, h.Client, psbjs)
if err != nil {
return nil, kerrors.NewInternalError(fmt.Errorf("grant project subject: %w", err))
Expand Down
3 changes: 2 additions & 1 deletion pkg/extensionapis/walrus/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package walrus
import (
"context"
"errors"
"fmt"

"github.com/seal-io/utils/pools/gopool"
core "k8s.io/api/core/v1"
Expand Down Expand Up @@ -55,7 +56,7 @@ func (h *SettingHandler) SetupHandler(
return []string{obj.GetName()}
})
if err != nil {
return
return gvr, srs, fmt.Errorf("index secret 'metadata.name': %w", err)
}

// Declare GVR.
Expand Down
5 changes: 4 additions & 1 deletion pkg/extensionapis/walrus/subject.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (h *SubjectHandler) SetupHandler(
return []string{obj.GetName()}
})
if err != nil {
return gvr, srs, err
return gvr, srs, fmt.Errorf("index service account 'metadata.name': %w", err)
}

// Declare GVR.
Expand Down Expand Up @@ -368,6 +368,9 @@ func (h *SubjectHandler) OnUpdate(ctx context.Context, obj, oldObj runtime.Objec
if err := subj.Spec.Role.Validate(); err != nil {
errs = append(errs, field.Invalid(
field.NewPath("spec.role"), subj.Spec.Role, err.Error()))
} else if subj.Name == systemkuberes.AdminSubjectName && subj.Spec.Role != walrus.SubjectRoleAdmin {
errs = append(errs, field.Invalid(
field.NewPath("spec.role"), subj.Spec.Role, "admin role is immutable"))
}
if stringx.StringWidth(subj.Spec.DisplayName) > 30 {
errs = append(errs, field.TooLongMaxLength(
Expand Down
3 changes: 2 additions & 1 deletion pkg/extensionapis/walrus/subject_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package walrus

import (
"context"
"fmt"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -54,7 +55,7 @@ func (h *SubjectProviderHandler) SetupHandler(
return []string{obj.GetName()}
})
if err != nil {
return
return gvr, srs, fmt.Errorf("index secret 'metadata.name': %w", err)
}

// Declare GVR.
Expand Down
Loading

0 comments on commit 49d3176

Please sign in to comment.