Skip to content

Commit

Permalink
Fleet RBAC - InheritedFleetWorkspacePermissions validation (#348)
Browse files Browse the repository at this point in the history
- Validate the user have enough permission to create/update the rules defined in InheritedFleetWorkspacePermissions.ResourceRules
- Validate the user have enough permission to create/update the rules that are generated based on the InheritedFleetWorkspacePermissions.WorkspaceVerbs

---------

Co-authored-by: Michael Bolot <michael.bolot@suse.com>
  • Loading branch information
raulcabello and MbolotSuse authored Jun 24, 2024
1 parent 236a12f commit af2d8bd
Show file tree
Hide file tree
Showing 21 changed files with 1,543 additions and 159 deletions.
18 changes: 17 additions & 1 deletion docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ Rules without verbs, resources, or apigroups are not permitted. The `rules` incl

Escalation checks are bypassed if a user has the `escalate` verb on the GlobalRole that they are attempting to update or create. This can also be given through a wildcard permission (i.e. the `*` verb also gives `escalate`).

Users can only change GlobalRoles with rights less than or equal to those they currently possess. This is to prevent privilege escalation. This includes the rules in the RoleTemplates referred to in `inheritedClusterRoles`.
Users can only change GlobalRoles with rights less than or equal to those they currently possess. This is to prevent privilege escalation. This includes the rules in the RoleTemplates referred to in `inheritedClusterRoles` and the rules in `inheritedFleetWorkspacePermissions`.

Users can only grant rules in the `NamespacedRules` field with rights less than or equal to those they currently possess. This works on a per namespace basis, meaning that the user must have the permission
in the namespace specified. The `Rules` field apply to every namespace, which means a user can create `NamespacedRules` in any namespace that are equal to or less than the `Rules` they currently possess.
Expand Down Expand Up @@ -345,6 +345,22 @@ When a UserAttribute is updated, the following checks take place:

# rbac.authorization.k8s.io/v1

## ClusterRole

### Validation Checks

#### Invalid Fields - Update
Users cannot update or remove the following label after it has been added:
- authz.management.cattle.io/gr-owner

## ClusterRoleBinding

### Validation Checks

#### Invalid Fields - Update
Users cannot update or remove the following label after it has been added:
- authz.management.cattle.io/grb-owner

## Role

### Validation Checks
Expand Down
55 changes: 55 additions & 0 deletions pkg/auth/globalrole.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,64 @@ func (g *GlobalRoleResolver) ClusterRulesFromRole(gr *v3.GlobalRole) ([]rbacv1.P
}
rules = append(rules, templateRules...)
}

return rules, nil
}

// FleetWorkspacePermissionsResourceRulesFromRole finds rules which this GlobalRole gives on fleet resources in the workspace backing namespace.
// This is assuming a user has permissions in all workspaces (including fleet-local), which is not true. That's fine if we
// use it to evaluate InheritedFleetWorkspacePermissions.ResourceRules. However, it shouldn't be used in a more generic evaluation
// of permissions on the workspace backing namespace.
func (g *GlobalRoleResolver) FleetWorkspacePermissionsResourceRulesFromRole(gr *v3.GlobalRole) []rbacv1.PolicyRule {
for _, name := range adminRoles {
if gr.Name == name {
return []rbacv1.PolicyRule{
{
Verbs: []string{"*"},
APIGroups: []string{"fleet.cattle.io"},
Resources: []string{"clusterregistrationtokens", "gitreporestrictions", "clusterregistrations", "clusters", "gitrepos", "bundles", "clustergroups"},
},
}
}
}

if gr == nil || gr.InheritedFleetWorkspacePermissions == nil {
return nil
}

return gr.InheritedFleetWorkspacePermissions.ResourceRules
}

// FleetWorkspacePermissionsWorkspaceVerbsFromRole finds rules which this GlobalRole gives on the fleetworkspace cluster-wide resources.
// This is assuming a user has permissions in all workspaces (including fleet-local), which is not true. That's fine if we
// use it to evaluate InheritedFleetWorkspacePermissions.WorkspaceVerbs. However, it shouldn't be used in a more generic evaluation
// of permissions on the workspace object.
func (g *GlobalRoleResolver) FleetWorkspacePermissionsWorkspaceVerbsFromRole(gr *v3.GlobalRole) []rbacv1.PolicyRule {
for _, name := range adminRoles {
if gr.Name == name {
return []rbacv1.PolicyRule{{
Verbs: []string{"*"},
APIGroups: []string{"management.cattle.io"},
Resources: []string{"fleetworkspaces"},
}}
}
}

if gr == nil || gr.InheritedFleetWorkspacePermissions == nil {
return nil
}

if gr.InheritedFleetWorkspacePermissions.WorkspaceVerbs != nil {
return []rbacv1.PolicyRule{{
Verbs: gr.InheritedFleetWorkspacePermissions.WorkspaceVerbs,
APIGroups: []string{"management.cattle.io"},
Resources: []string{"fleetworkspaces"},
}}
}

return nil
}

// GetRoleTemplate allows the caller to retrieve the roleTemplates in use by a given global role. Does not
// recursively evaluate roleTemplates - only returns the top-level resources.
func (g *GlobalRoleResolver) GetRoleTemplatesForGlobalRole(gr *v3.GlobalRole) ([]*v3.RoleTemplate, error) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ func main() {
Types: []interface{}{
&rbacv1.Role{},
&rbacv1.RoleBinding{},
&rbacv1.ClusterRole{},
&rbacv1.ClusterRoleBinding{},
},
}}); err != nil {
fmt.Printf("ERROR: %v\n", err)
Expand Down
106 changes: 106 additions & 0 deletions pkg/generated/objects/rbac.authorization.k8s.io/v1/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,109 @@ func RoleBindingFromRequest(request *admissionv1.AdmissionRequest) (*v1.RoleBind

return object, nil
}

// ClusterRoleOldAndNewFromRequest gets the old and new ClusterRole objects, respectively, from the webhook request.
// If the request is a Delete operation, then the new object is the zero value for ClusterRole.
// Similarly, if the request is a Create operation, then the old object is the zero value for ClusterRole.
func ClusterRoleOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v1.ClusterRole, *v1.ClusterRole, error) {
if request == nil {
return nil, nil, fmt.Errorf("nil request")
}

object := &v1.ClusterRole{}
oldObject := &v1.ClusterRole{}

if request.Operation != admissionv1.Delete {
err := json.Unmarshal(request.Object.Raw, object)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal request object: %w", err)
}
}

if request.Operation == admissionv1.Create {
return oldObject, object, nil
}

err := json.Unmarshal(request.OldObject.Raw, oldObject)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal request oldObject: %w", err)
}

return oldObject, object, nil
}

// ClusterRoleFromRequest returns a ClusterRole object from the webhook request.
// If the operation is a Delete operation, then the old object is returned.
// Otherwise, the new object is returned.
func ClusterRoleFromRequest(request *admissionv1.AdmissionRequest) (*v1.ClusterRole, error) {
if request == nil {
return nil, fmt.Errorf("nil request")
}

object := &v1.ClusterRole{}
raw := request.Object.Raw

if request.Operation == admissionv1.Delete {
raw = request.OldObject.Raw
}

err := json.Unmarshal(raw, object)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal request object: %w", err)
}

return object, nil
}

// ClusterRoleBindingOldAndNewFromRequest gets the old and new ClusterRoleBinding objects, respectively, from the webhook request.
// If the request is a Delete operation, then the new object is the zero value for ClusterRoleBinding.
// Similarly, if the request is a Create operation, then the old object is the zero value for ClusterRoleBinding.
func ClusterRoleBindingOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v1.ClusterRoleBinding, *v1.ClusterRoleBinding, error) {
if request == nil {
return nil, nil, fmt.Errorf("nil request")
}

object := &v1.ClusterRoleBinding{}
oldObject := &v1.ClusterRoleBinding{}

if request.Operation != admissionv1.Delete {
err := json.Unmarshal(request.Object.Raw, object)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal request object: %w", err)
}
}

if request.Operation == admissionv1.Create {
return oldObject, object, nil
}

err := json.Unmarshal(request.OldObject.Raw, oldObject)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal request oldObject: %w", err)
}

return oldObject, object, nil
}

// ClusterRoleBindingFromRequest returns a ClusterRoleBinding object from the webhook request.
// If the operation is a Delete operation, then the old object is returned.
// Otherwise, the new object is returned.
func ClusterRoleBindingFromRequest(request *admissionv1.AdmissionRequest) (*v1.ClusterRoleBinding, error) {
if request == nil {
return nil, fmt.Errorf("nil request")
}

object := &v1.ClusterRoleBinding{}
raw := request.Object.Raw

if request.Operation == admissionv1.Delete {
raw = request.OldObject.Raw
}

err := json.Unmarshal(raw, object)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal request object: %w", err)
}

return object, nil
}
100 changes: 0 additions & 100 deletions pkg/resolvers/grbClusterResolver.go

This file was deleted.

Loading

0 comments on commit af2d8bd

Please sign in to comment.