Skip to content

feat: Replace RBACRule with RBACRoleRule #219

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

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 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
146 changes: 87 additions & 59 deletions api/v1alpha1/azurevalidator_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,40 @@ limitations under the License.
package v1alpha1

import (
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// AzureValidatorSpec defines the desired state of AzureValidator
type AzureValidatorSpec struct {
// Rules for validating that the correct role assignments have been created in Azure RBAC to
// provide needed permissions.
// +kubebuilder:validation:MaxItems=5
// +kubebuilder:validation:XValidation:message="RBACRules must have unique names",rule="self.all(e, size(self.filter(x, x.name == e.name)) == 1)"
RBACRules []RBACRule `json:"rbacRules,omitempty" yaml:"rbacRules,omitempty"`
// Rules for validating that images exist in an Azure Compute Gallery published as a community
// gallery.
// +kubebuilder:validation:MaxItems=5
// +kubebuilder:validation:XValidation:message="CommunityGalleryImageRules must have unique names",rule="self.all(e, size(self.filter(x, x.name == e.name)) == 1)"
CommunityGalleryImageRules []CommunityGalleryImageRule `json:"communityGalleryImageRules,omitempty" yaml:"communityGalleryImageRules,omitempty"`
Auth AzureAuth `json:"auth" yaml:"auth"`
// RBACRoleRules validate that a security principal has permissions at a specified scope via
// role assignments and role definitions.
// +kubebuilder:validation:MaxItems=5
// +kubebuilder:validation:XValidation:message="RBACRoleRules must have unique names",rule="self.all(e, size(self.filter(x, x.name == e.name)) == 1)"
RBACRoleRules []RBACRoleRule `json:"rbacRoleRules,omitempty" yaml:"rbacRoleRules,omitempty"`
Auth AzureAuth `json:"auth" yaml:"auth"`
}

// ResultCount returns the number of validation results expected for an AzureValidatorSpec.
func (s AzureValidatorSpec) ResultCount() int {
return len(s.RBACRules) + len(s.CommunityGalleryImageRules)
return len(s.CommunityGalleryImageRules) + len(s.RBACRoleRules)
}

// RBACRule verifies that a security principal has permissions via role assignments and that no deny
// assignments deny the permissions.
type RBACRule struct {
// Unique identifier for the rule in the validator. Used to ensure conditions do not overwrite
// each other.
Name string `json:"name" yaml:"name"`
// The permissions that the principal must have. If the principal has permissions less than
// this, validation will fail. If the principal has permissions equal to or more than this
// (e.g., inherited permissions from higher level scope, more roles than needed) validation
// will pass.
//+kubebuilder:validation:MinItems=1
//+kubebuilder:validation:MaxItems=20
//+kubebuilder:validation:XValidation:message="Each permission set must have Actions, DataActions, or both defined",rule="self.all(item, size(item.actions) > 0 || size(item.dataActions) > 0)"
Permissions []PermissionSet `json:"permissionSets" yaml:"permissionSets"`
// The principal being validated. This can be any type of principal - Device, ForeignGroup,
// Group, ServicePrincipal, or User.
PrincipalID string `json:"principalId" yaml:"principalId"`
// AzureAuth defines authentication configuration for an AzureValidator.
type AzureAuth struct {
// If true, the AzureValidator will use the Azure SDK's default credential chain to authenticate.
// Set to true if using WorkloadIdentityCredentials.
Implicit bool `json:"implicit" yaml:"implicit"`
// Name of a Secret in the same namespace as the AzureValidator that contains Azure credentials.
// The secret data's keys and values are expected to align with valid Azure environment variable credentials,
// per the options defined in https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#readme-environment-variables.
SecretName string `json:"secretName,omitempty" yaml:"secretName,omitempty"`
}

// CommunityGalleryImageRule verifies that one or more images in a community gallery exist and are
Expand All @@ -77,49 +73,81 @@ type CommunityGalleryImageRule struct {
// CommunityGallery is a community gallery in a particular location.
type CommunityGallery struct {
// Location is the location of the community gallery (e.g. "westus").
// +kubebuilder:validation:MaxLength=50
Location string `json:"location" yaml:"location"`
// Name is the name of the community gallery.
// +kubebuilder:validation:MaxLength=200
Name string `json:"name" yaml:"name"`
}

// AzureAuth defines authentication configuration for an AzureValidator.
type AzureAuth struct {
// If true, the AzureValidator will use the Azure SDK's default credential chain to authenticate.
// Set to true if using WorkloadIdentityCredentials.
Implicit bool `json:"implicit" yaml:"implicit"`
// Name of a Secret in the same namespace as the AzureValidator that contains Azure credentials.
// The secret data's keys and values are expected to align with valid Azure environment variable credentials,
// per the options defined in https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#readme-environment-variables.
SecretName string `json:"secretName,omitempty" yaml:"secretName,omitempty"`
// RBACRoleRule verifies that a role definition with a role type, role name, and set of permissions
// exists, and that it is assigned at a scope to a security principal.
type RBACRoleRule struct {
// Name is a unique identifier for the rule in the validator. Used to ensure conditions do not
// overwrite each other.
// +kubebuilder:validation:MaxLength=200
Name string `json:"name" yaml:"name"`
// PrincipalID is the security principal being validated. This can be any type of principal -
// Device, ForeignGroup, Group, ServicePrincipal, or User.
PrincipalID string `json:"principalId" yaml:"principalId"`
// RoleAssignments are combinations of scope and role data.
// +kubebuilder:validation:MinItems=1
RoleAssignments []RoleAssignment `json:"roleAssignments" yaml:"roleAssignments"`
}

// ActionStr is a type used for Action strings and DataAction strings. Alias exists to enable
// kubebuilder max string length validation for arrays of these.
// +kubebuilder:validation:MaxLength=200
type ActionStr string

// PermissionSet is part of an RBAC rule and verifies that a security principal has the specified
// permissions (via role assignments) at the specified scope. Scope can be either subscription,
// resource group, or resource.
type PermissionSet struct {
// Actions is a list of actions that the role must be able to perform. Must not contain any
// wildcards. If not specified, the role is assumed to already be able to perform all required
// actions.
//+kubebuilder:validation:MaxItems=1000
//+kubebuilder:validation:XValidation:message="Actions cannot have wildcards.",rule="self.all(item, !item.contains('*'))"
Actions []ActionStr `json:"actions,omitempty" yaml:"actions,omitempty"`
// DataActions is a list of data actions that the role must be able to perform. Must not
// contain any wildcards. If not provided, the role is assumed to already be able to perform
// all required data actions.
//+kubebuilder:validation:MaxItems=1000
//+kubebuilder:validation:XValidation:message="DataActions cannot have wildcards.",rule="self.all(item, !item.contains('*'))"
DataActions []ActionStr `json:"dataActions,omitempty" yaml:"dataActions,omitempty"`
// Scope is the minimum scope of the role. Role assignments found at higher level scopes will
// satisfy this. For example, a role assignment found with subscription scope will satisfy a
// permission set where the role scope specified is a resource group within that subscription.
// RoleAssignment is a combination of scope and role data.
type RoleAssignment struct {
// Scope is the exact scope the role is assigned to the security principal at.
Scope string `json:"scope" yaml:"scope"`
// Role is the role data.
Role Role `json:"role" yaml:"role"`
}

// Role is role data in a role assignment. Is it a subset of a role definition.
type Role struct {
// Name is the role name property of the role definition.
Name string `json:"name" yaml:"name"`
// Type is the role type property of the role definition. Must be "BuiltInRole" or "Custom".
// Required to disambiguate built in roles and custom roles with the same name.
// +kubebuilder:validation:Enum=BuiltInRole;CustomRole
Type string `json:"type" yaml:"type"`
// Permission is the permissions data of the role definition.
Permission Permission `json:"permissions" yaml:"permissions"`
}

// Permission is the permission data in a role definition.
type Permission struct {
// Actions is the "actions" of the role definition.
Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"`
// DataActions is the "dataActions" of the role definition.
DataActions []string `json:"dataActions,omitempty" yaml:"dataActions,omitempty"`
// NotActions is the "notActions" of the role definition.
NotActions []string `json:"notActions,omitempty" yaml:"notActions,omitempty"`
// NotDataActions is the "notDataActions" of the role definition.
NotDataActions []string `json:"notDataActions,omitempty" yaml:"notDataActions,omitempty"`
}

// Equal compares a Permission (from the spec) to an armauthorization.Permission (from the Azure API
// response).
func (p Permission) Equal(other armauthorization.Permission) bool {
compareSlices := func(a []string, b []*string) bool {
if len(a) != len(b) {
return false
}
for i, val := range a {
if b[i] == nil {
return false
}
bVal := *b[i]
if val != bVal {
return false
}
}
return true
}

return compareSlices(p.Actions, other.Actions) &&
compareSlices(p.DataActions, other.DataActions) &&
compareSlices(p.NotActions, other.NotActions) &&
compareSlices(p.NotDataActions, other.NotDataActions)
}

// AzureValidatorStatus defines the observed state of AzureValidator
Expand Down
140 changes: 140 additions & 0 deletions api/v1alpha1/azurevalidator_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
Copyright 2024.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
"k8s.io/utils/ptr"
)

func TestPermission_Equal(t *testing.T) {
type fields struct {
Actions []string
DataActions []string
NotActions []string
NotDataActions []string
}
type args struct {
other armauthorization.Permission
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "Returns true when both empty.",
fields: fields{},
args: args{
other: armauthorization.Permission{},
},
want: true,
},
{
name: "Returns true when equal.",
fields: fields{
Actions: []string{"a", "b"},
DataActions: []string{"c", "d"},
NotActions: []string{"d", "e"},
NotDataActions: []string{"f", "g"},
},
args: args{
other: armauthorization.Permission{
Actions: []*string{ptr.To("a"), ptr.To("b")},
DataActions: []*string{ptr.To("c"), ptr.To("d")},
NotActions: []*string{ptr.To("d"), ptr.To("e")},
NotDataActions: []*string{ptr.To("f"), ptr.To("g")},
},
},
want: true,
},
{
name: "Returns true when equal (some slices omitted).",
fields: fields{
Actions: []string{"a", "b"},
},
args: args{
other: armauthorization.Permission{
Actions: []*string{ptr.To("a"), ptr.To("b")},
},
},
want: true,
},
{
name: "Returns false when inequal (1).",
fields: fields{
Actions: []string{"a", "b"},
},
args: args{
other: armauthorization.Permission{
Actions: []*string{ptr.To("c"), ptr.To("d")},
},
},
want: false,
},
{
name: "Returns false when inequal (2).",
fields: fields{
Actions: []string{"a", "b"},
},
args: args{
other: armauthorization.Permission{
Actions: []*string{ptr.To("c")},
},
},
want: false,
},
{
name: "Returns false when inequal (3).",
fields: fields{
Actions: []string{"a", "b"},
},
args: args{
other: armauthorization.Permission{},
},
want: false,
},
{
name: "Returns false when inequal (4).",
fields: fields{
Actions: []string{"a", "b"},
},
args: args{
other: armauthorization.Permission{
Actions: []*string{nil, nil},
},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := Permission{
Actions: tt.fields.Actions,
DataActions: tt.fields.DataActions,
NotActions: tt.fields.NotActions,
NotDataActions: tt.fields.NotDataActions,
}
if got := p.Equal(tt.args.other); got != tt.want {
t.Errorf("Permission.Equal() = %v, want %v", got, tt.want)
}
})
}
}
Loading
Loading