Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ authz: add warrants to default rule resolver #155

Merged
merged 3 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 177 additions & 4 deletions pkg/registry/rbac/validation/kcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ package validation

import (
"context"
"fmt"
"strings"

"github.com/kcp-dev/logicalcluster/v3"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/sets"
authserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
)

const (
// WarrantExtraKey is the key used in a user's "extra" to specify
// JSON-encoded user infos for attached extra permissions for that user
// evaluated by the authorizer.
WarrantExtraKey = "authorization.kcp.io/warrant"
mjudeikis marked this conversation as resolved.
Show resolved Hide resolved

// ScopeExtraKey is the key used in a user's "extra" to specify
// that the user is restricted to a given scope. Valid values for
// one extra value are:
Expand All @@ -27,15 +34,85 @@ const (
clusterPrefix = "cluster:"
)

// Warrant is serialized into the user's "extra" field authorization.kcp.io/warrant
// to hold user information for extra permissions.
type Warrant struct {
// User is the user you're testing for.
// If you specify "User" but not "Groups", then is it interpreted as "What if User were not a member of any groups
// +optional
User string `json:"user,omitempty"`
// Groups is the groups you're testing for.
// +optional
// +listType=atomic
Groups []string `json:"groups,omitempty"`
// Extra corresponds to the user.Info.GetExtra() method from the authenticator. Since that is input to the authorizer
// it needs a reflection here.
// +optional
Extra map[string][]string `json:"extra,omitempty"`
// UID information about the requesting user.
// +optional
UID string `json:"uid,omitempty"`
}

type appliesToUserFunc func(user user.Info, subject rbacv1.Subject, namespace string) bool
type appliesToUserFuncCtx func(ctx context.Context, user user.Info, subject rbacv1.Subject, namespace string) bool

var appliesToUserWithScopes = withScopes(appliesToUser)
var (
appliesToUserWithScopes = withScopes(appliesToUser)
appliesToUserWithScopedAndWarrants = withWarrants(appliesToUserWithScopes)
)

// withScopes wraps the appliesToUser predicate to check for the base user and any warrants.
func withScopes(appliesToUser appliesToUserFunc) appliesToUserFuncCtx {
// withWarrants wraps the appliesToUser predicate to check for the base user and any warrants.
func withWarrants(appliesToUser appliesToUserFuncCtx) appliesToUserFuncCtx {
var recursive appliesToUserFuncCtx
recursive = func(ctx context.Context, u user.Info, bindingSubject rbacv1.Subject, namespace string) bool {
if appliesToUser(ctx, u, bindingSubject, namespace) {
return true
}

if IsServiceAccount(u) {
if cluster := genericapirequest.ClusterFrom(ctx); cluster != nil && cluster.Name != "" {
nsNameSuffix := strings.TrimPrefix(u.GetName(), "system:serviceaccount:")
rewritten := &user.DefaultInfo{
Name: fmt.Sprintf("system:kcp:serviceaccount:%s:%s", cluster.Name, nsNameSuffix),
Groups: []string{user.AllAuthenticated},
Extra: u.GetExtra(),
}
if appliesToUser(ctx, rewritten, bindingSubject, namespace) {
return true
}
}
}

for _, v := range u.GetExtra()[WarrantExtraKey] {
var w Warrant
if err := json.Unmarshal([]byte(v), &w); err != nil {
continue
}

wu := &user.DefaultInfo{
Name: w.User,
UID: w.UID,
Groups: w.Groups,
Extra: w.Extra,
}
if IsServiceAccount(wu) && len(w.Extra[authserviceaccount.ClusterNameKey]) == 0 {
// warrants must be scoped to a cluster
continue
}
if recursive(ctx, wu, bindingSubject, namespace) {
return true
}
}

return false
}
return recursive
}

// withScopes wraps the appliesToUser predicate to check the scopes.
func withScopes(appliesToUser appliesToUserFunc) appliesToUserFuncCtx {
return func(ctx context.Context, u user.Info, bindingSubject rbacv1.Subject, namespace string) bool {
var clusterName logicalcluster.Name
if cluster := genericapirequest.ClusterFrom(ctx); cluster != nil {
clusterName = cluster.Name
Expand All @@ -49,7 +126,6 @@ func withScopes(appliesToUser appliesToUserFunc) appliesToUserFuncCtx {

return false
}
return recursive
}

var (
Expand Down Expand Up @@ -106,3 +182,100 @@ func IsInScope(attr user.Info, cluster logicalcluster.Name) bool {

return true
}

// EffectiveGroups returns the effective groups of the user in the given context
// taking scopes and warrants into account.
func EffectiveGroups(ctx context.Context, u user.Info) sets.Set[string] {
var clusterName logicalcluster.Name
if cluster := genericapirequest.ClusterFrom(ctx); cluster != nil {
clusterName = cluster.Name
}

groups := sets.New[string]()
var recursive func(u user.Info)
recursive = func(u user.Info) {
if IsInScope(u, clusterName) {
groups.Insert(u.GetGroups()...)
} else {
for _, g := range u.GetGroups() {
if g == user.AllAuthenticated {
groups.Insert(g)
}
}
}

for _, v := range u.GetExtra()[WarrantExtraKey] {
var w Warrant
if err := json.Unmarshal([]byte(v), &w); err != nil {
continue
}
recursive(&user.DefaultInfo{Name: w.User, UID: w.UID, Groups: w.Groups, Extra: w.Extra})
}
}
recursive(u)

return groups
}

// PrefixUser returns a new user with the name and groups prefixed with the
// given prefix, and all warrants recursively prefixed.
//
// If the user is a service account, the prefix is added to the global service
// account name.
//
// Invalid warrants are skipped.
func PrefixUser(u user.Info, prefix string) user.Info {
pu := &user.DefaultInfo{
Name: prefix + u.GetName(),
UID: u.GetUID(),
}
if IsServiceAccount(u) {
if clusters := u.GetExtra()[authserviceaccount.ClusterNameKey]; len(clusters) != 1 {
// this should not happen. But if it does, we are defensive.
for _, g := range u.GetGroups() {
if g == user.AllAuthenticated {
return &user.DefaultInfo{Name: prefix + user.Anonymous, Groups: []string{prefix + user.AllAuthenticated}}
}
}
return &user.DefaultInfo{Name: prefix + user.Anonymous, Groups: []string{prefix + user.AllUnauthenticated}}
} else {
pu.Name = fmt.Sprintf("%ssystem:kcp:serviceaccount:%s:%s", prefix, clusters[0], strings.TrimPrefix(u.GetName(), "system:serviceaccount:"))
}
}

for _, g := range u.GetGroups() {
pu.Groups = append(pu.Groups, prefix+g)
}

for k, v := range u.GetExtra() {
if k == WarrantExtraKey {
continue
}
if pu.Extra == nil {
pu.Extra = map[string][]string{}
}
pu.Extra[k] = v
}

for _, w := range u.GetExtra()[WarrantExtraKey] {
var warrant Warrant
if err := json.Unmarshal([]byte(w), &warrant); err != nil {
continue // skip invalid warrant
}

wpu := PrefixUser(&user.DefaultInfo{Name: warrant.User, UID: warrant.UID, Groups: warrant.Groups, Extra: warrant.Extra}, prefix)
warrant = Warrant{User: wpu.GetName(), UID: wpu.GetUID(), Groups: wpu.GetGroups(), Extra: wpu.GetExtra()}

bs, err := json.Marshal(warrant)
if err != nil {
continue // skip invalid warrant
}

if pu.Extra == nil {
pu.Extra = map[string][]string{}
}
pu.Extra[WarrantExtraKey] = append(pu.Extra[WarrantExtraKey], string(bs))
}

return pu
}
Loading