Skip to content

Commit

Permalink
added rbac to allow force-run
Browse files Browse the repository at this point in the history
  • Loading branch information
asiyani committed Sep 21, 2023
1 parent 4971237 commit d3b55b1
Show file tree
Hide file tree
Showing 9 changed files with 447 additions and 91 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ spec:
pollInterval: 60
runTimeout: 900
delegateServiceAccountSecretRef: terraform-applier-delegate-token
rbac:
- role: Admin
subjects:
- name: user@email.com
kind: User
- name: some_group_name
kind: Group
backend:
- name: bucket
value: dev-terraform-state
Expand Down Expand Up @@ -105,6 +112,25 @@ Terraform applier supports strongbox decryption, its triggered if `TF_APPLIER_ST
content of this ENV should be valid strongbox keyring file data which should include strongbox key used to encrypt secrets in the module.
TF Applier will also configure Git and Strongbox Home before running `init` to decrypt any encrypted file from remote base as well.
### RBAC
Terraform applier does user authentication using OIDC flow (see Controller config).
during oidc flow it requests `openid, email, groups` scopes to get user's email and groups info as part of `id_token`.
`rbac` section of module crd can be use to set list of Admins who's allowed to do `force run`.
```
rbac:
- role: Admin
subjects:
- name: user@email.com
kind: User
- name: some_group_name
kind: Group
```
At the moment only "Admin" role is supported, value of subjects can be either `email address` of users as kind `User` or the group name as kind `Group`.
**If `OIDC Issuer` is not set then web server will skip authentication and all `force run` requests will be allowed.**
### Graceful shutdown
To make sure all terraform module run does complete in finite time `runTimeout` is added to the module spec.
Expand Down
109 changes: 66 additions & 43 deletions api/v1beta1/module_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,50 @@ import (
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// The potential reasons for events and current state
const (
ReasonRunTriggered = "RunTriggered"
ReasonForcedRunTriggered = "ForcedRunTriggered"
ReasonPollingRunTriggered = "PollingRunTriggered"
ReasonScheduledRunTriggered = "ScheduledRunTriggered"

ReasonRunPreparationFailed = "RunPreparationFailed"
ReasonDelegationFailed = "DelegationFailed"
ReasonControllerShutdown = "ControllerShutdown"
ReasonSpecsParsingFailure = "SpecsParsingFailure"
ReasonGitFailure = "GitFailure"

ReasonInitialiseFailed = "InitialiseFailed"
ReasonPlanFailed = "PlanFailed"
ReasonApplyFailed = "ApplyFailed"

ReasonInitialised = "Initialised"
ReasonPlanedDriftDetected = "PlanedDriftDetected"
ReasonPlanedNoDriftDetected = "PlanedNoDriftDetected"
ReasonApplied = "Applied"
)

const (
// ScheduledRun indicates a scheduled, regular terraform run.
ScheduledRun = "ScheduledRun"
// PollingRun indicated a run triggered by changes in the git repository.
PollingRun = "PollingRun"
// ForcedRun indicates a forced (triggered on the UI) terraform run.
ForcedRun = "ForcedRun"
)

// Overall state of Module run
type state string

const (
// 'Running' -> module is in running state
StatusRunning state = "Running"
// 'Ready' -> last run finished successfully and its waiting on next run/event
StatusReady state = "Ready"
// 'Errored' -> last run finished with Error and its waiting on next run/event
StatusErrored state = "Errored"
)

// ModuleSpec defines the desired state of Module
type ModuleSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Expand Down Expand Up @@ -86,6 +130,10 @@ type ModuleSpec struct {
// +kubebuilder:default=900
// +kubebuilder:validation:Maximum=1800
RunTimeout int `json:"runTimeout,omitempty"`

// List of roles and subjects assigned to that role for the module.
// +optional
RBAC []RBAC `json:"rbac,omitempty"`
}

// ModuleStatus defines the observed state of Module
Expand Down Expand Up @@ -222,49 +270,24 @@ type VaultAWSRequest struct {
RoleARN string `json:"roleARN,omitempty"`
}

// The potential reasons for events and current state
const (
ReasonRunTriggered = "RunTriggered"
ReasonForcedRunTriggered = "ForcedRunTriggered"
ReasonPollingRunTriggered = "PollingRunTriggered"
ReasonScheduledRunTriggered = "ScheduledRunTriggered"

ReasonRunPreparationFailed = "RunPreparationFailed"
ReasonDelegationFailed = "DelegationFailed"
ReasonControllerShutdown = "ControllerShutdown"
ReasonSpecsParsingFailure = "SpecsParsingFailure"
ReasonGitFailure = "GitFailure"

ReasonInitialiseFailed = "InitialiseFailed"
ReasonPlanFailed = "PlanFailed"
ReasonApplyFailed = "ApplyFailed"

ReasonInitialised = "Initialised"
ReasonPlanedDriftDetected = "PlanedDriftDetected"
ReasonPlanedNoDriftDetected = "PlanedNoDriftDetected"
ReasonApplied = "Applied"
)

const (
// ScheduledRun indicates a scheduled, regular terraform run.
ScheduledRun = "ScheduledRun"
// PollingRun indicated a run triggered by changes in the git repository.
PollingRun = "PollingRun"
// ForcedRun indicates a forced (triggered on the UI) terraform run.
ForcedRun = "ForcedRun"
)

// Overall state of Module run
type state string

const (
// 'Running' -> module is in running state
StatusRunning state = "Running"
// 'Ready' -> last run finished successfully and its waiting on next run/event
StatusReady state = "Ready"
// 'Errored' -> last run finished with Error and its waiting on next run/event
StatusErrored state = "Errored"
)
type RBAC struct {
// Name of the role. Allowed value at the moment is just "Admin"
// +required
// +kubebuilder:validation:Enum=Admin
Role string `json:"role,omitempty"`
// Subjects holds references to the objects the role applies to.
// +required
Subjects []Subject `json:"subjects,omitempty"`
}
type Subject struct {
// Kind of object being referenced. Allowed values are "User" & "Group"
// +required
// +kubebuilder:validation:Enum=User;Group
Kind string `json:"kind,omitempty"`
// Name of the object being referenced. For "User" kind value should be email
// +required
Name string `json:"name,omitempty"`
}

func (m *Module) IsSuspended() bool {
return m.Spec.Suspend != nil && *m.Spec.Suspend
Expand Down
35 changes: 35 additions & 0 deletions api/v1beta1/rbac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package v1beta1

import (
"slices"
"strings"
)

const RoleAdmin = "Admin"

func CanForceRun(email string, groups []string, module *Module) bool {
// remove empty values
groups = slices.DeleteFunc(groups, func(g string) bool {
return strings.TrimSpace(g) == ""
})

for _, rbac := range module.Spec.RBAC {
// only module "Admins" are allowed to do force run
if rbac.Role == RoleAdmin {
for _, subject := range rbac.Subjects {
// check if email matching and its not a empty value
if subject.Kind == "User" &&
subject.Name == email &&
strings.TrimSpace(email) != "" {
return true
}
if subject.Kind == "Group" &&
slices.Contains(groups, subject.Name) {
return true
}
}
}
}

return false
}
168 changes: 168 additions & 0 deletions api/v1beta1/rbac_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package v1beta1

import "testing"

func TestCanForceRun(t *testing.T) {
type args struct {
email string
groups []string
module *Module
}
tests := []struct {
name string
args args
want bool
}{
{
"no rbac",
args{
"u1", []string{"group1", "group2"},
&Module{Spec: ModuleSpec{RepoName: "test"}},
},
false,
}, {
"no Admin role",
args{
"u1", []string{"group1", "group2"},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{Role: "View", Subjects: []Subject{{Kind: "User", Name: "u1"}}}},
}},
},
false,
}, {
"email or group doesn't match",
args{
"u1", []string{"group1", "group2"},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{
Role: "Admin",
Subjects: []Subject{
{Kind: "User", Name: "u3"},
{Kind: "Group", Name: "group3"},
}}}}},
},
false,
}, {
"email match",
args{
"u1", []string{"group1", "group2"},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{
Role: "Admin",
Subjects: []Subject{
{Kind: "User", Name: "u1"},
{Kind: "Group", Name: "group3"},
}}}}},
},
true,
}, {
"email match",
args{
"u1", []string{"group1", "group2"},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{
Role: "Admin",
Subjects: []Subject{
{Kind: "User", Name: "u3"},
{Kind: "Group", Name: "group3"},
{Kind: "User", Name: "u1"},
}}}}},
},
true,
}, {
"empty email match",
args{
" ", []string{"group1", "group2"},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{
Role: "Admin",
Subjects: []Subject{
{Kind: "User", Name: " "},
{Kind: "Group", Name: "group3"},
{Kind: "User", Name: "u1"},
}}}}},
},
false,
}, {
"group match",
args{
"u1", []string{"group1", "group2"},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{
Role: "Admin",
Subjects: []Subject{
{Kind: "User", Name: "u3"},
{Kind: "Group", Name: "group2"},
}}}}},
},
true,
}, {
"empty group match",
args{
"u1", []string{" ", "group2"},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{
Role: "Admin",
Subjects: []Subject{
{Kind: "User", Name: "u3"},
{Kind: "Group", Name: " "},
}}}}},
},
false,
}, {
"with multiple roles",
args{
"u1", []string{"group1", "group2"},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{
Role: "View",
Subjects: []Subject{
{Kind: "User", Name: "u3"},
{Kind: "Group", Name: "group2"},
}}, {
Role: "Admin",
Subjects: []Subject{
{Kind: "User", Name: "u3"},
{Kind: "Group", Name: "group2"},
}},
}}},
},
true,
}, {
"no_user_groups",
args{
"u1", []string{},
&Module{
Spec: ModuleSpec{
RBAC: []RBAC{{
Role: "View",
Subjects: []Subject{
{Kind: "User", Name: "u3"},
{Kind: "Group", Name: "group2"},
}}, {
Role: "Admin",
Subjects: []Subject{
{Kind: "User", Name: "u3"},
{Kind: "Group", Name: "group2"},
}},
}}},
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CanForceRun(tt.args.email, tt.args.groups, tt.args.module); got != tt.want {
t.Errorf("CanForceRun() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit d3b55b1

Please sign in to comment.