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

Fleet RBAC - InheritedFleetWorkspacePermissions validation #348

Merged
merged 16 commits into from
Jun 24, 2024
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
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 {
MbolotSuse marked this conversation as resolved.
Show resolved Hide resolved
JonCrowther marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cluster rules contains a "Stint" that treats these users as having full permissions on these areas. I think you need something similar for this function as well - a "*" for the verbs should work ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you mean here. Could you elaborate on this, please?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - Here is the relevant logic. Basically, even though RestrictedAdmin has no permissions in these fields, we need to give the appropriate permissions so that RA can give this role to others. But the RA doesn't have/doesn't use this field, so you need special logic here to handle that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. see #348 (comment)

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{{
MbolotSuse marked this conversation as resolved.
Show resolved Hide resolved
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