This repository was archived by the owner on Nov 14, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
feat: kubernetes: check RBAC #6
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
1615083
chore: goimports
johnstcn 4d9da29
feat: log current kubernetes context
johnstcn 1d276e3
feat: kubernetes: check local helm version
johnstcn cbe2bbb
chore: add api.PassResult convenience function
johnstcn e0a7efd
add namespace to KubernetesChecker
johnstcn 70498f9
chore: golangci.yml: fix importas settings for authorizationv1 and au…
johnstcn 9083c18
feat: kubernetes: check RBAC requirements
johnstcn b0d6593
fixup! feat: kubernetes: check RBAC requirements
johnstcn 6275eea
internal/checks/kube/rbac_test.go: fix test
johnstcn 1235ba1
chore: importas: work around aliasing issue
johnstcn c74ab29
internal/checks/kube/rbac.go: add APIGroup to requirements
johnstcn ed0f873
internal/cmd/check/kubernetes/kubernetes.go: noop: rename variable
johnstcn 6592f55
internal/checks/kube/rbac.go: rename variable
johnstcn de04989
internal/checks/kube/rbac.go: support different RBAC requirements per…
johnstcn e57e0cc
internal/checks/kube/rbac_test.go: add test for unhandled version
johnstcn c8d5795
Merge remote-tracking branch 'origin/main' into cianjohnston/ch15968/…
johnstcn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package api | ||
|
||
// PassResults returns a CheckResult when everything is OK. | ||
func PassResult(name string, summary string) *CheckResult { | ||
return &CheckResult{ | ||
Name: name, | ||
State: StatePassed, | ||
Summary: summary, | ||
Details: map[string]interface{}{}, | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package api_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"cdr.dev/slog/sloggers/slogtest/assert" | ||
"github.com/cdr/coder-doctor/internal/api" | ||
) | ||
|
||
func TestPassResult(t *testing.T) { | ||
t.Parallel() | ||
|
||
res := api.PassResult("check-name", "check succeeded") | ||
|
||
assert.Equal(t, "name matches", "check-name", res.Name) | ||
assert.Equal(t, "state matches", api.StatePassed, res.State) | ||
assert.Equal(t, "summary matches", "check succeeded", res.Summary) | ||
assert.Equal(t, "error matches", nil, res.Details["error"]) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,130 @@ | ||
package kube | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"golang.org/x/xerrors" | ||
|
||
"github.com/Masterminds/semver/v3" | ||
|
||
"github.com/cdr/coder-doctor/internal/api" | ||
|
||
authorizationv1 "k8s.io/api/authorization/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
authorizationclientv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" | ||
) | ||
|
||
type RBACRequirement struct { | ||
APIGroup string | ||
Resource string | ||
Verbs []string | ||
} | ||
|
||
type VersionedRBACRequirements struct { | ||
VersionConstraints *semver.Constraints | ||
RBACRequirements []*RBACRequirement | ||
} | ||
|
||
var verbsCreateDeleteList = []string{"create", "delete", "list"} | ||
|
||
func NewRBACRequirement(apiGroup, resource string, verbs ...string) *RBACRequirement { | ||
return &RBACRequirement{ | ||
APIGroup: apiGroup, | ||
Resource: resource, | ||
Verbs: verbs, | ||
} | ||
} | ||
|
||
var allVersionedRBACRequirements = []VersionedRBACRequirements{ | ||
{ | ||
VersionConstraints: api.MustConstraint(">= 1.20"), | ||
RBACRequirements: []*RBACRequirement{ | ||
NewRBACRequirement("", "pods", verbsCreateDeleteList...), | ||
NewRBACRequirement("", "roles", verbsCreateDeleteList...), | ||
NewRBACRequirement("", "rolebindings", verbsCreateDeleteList...), | ||
NewRBACRequirement("", "secrets", verbsCreateDeleteList...), | ||
NewRBACRequirement("", "serviceaccounts", verbsCreateDeleteList...), | ||
NewRBACRequirement("", "services", verbsCreateDeleteList...), | ||
NewRBACRequirement("apps", "deployments", verbsCreateDeleteList...), | ||
NewRBACRequirement("apps", "replicasets", verbsCreateDeleteList...), | ||
NewRBACRequirement("apps", "statefulsets", verbsCreateDeleteList...), | ||
NewRBACRequirement("extensions", "ingresses", verbsCreateDeleteList...), | ||
}, | ||
}, | ||
} | ||
|
||
func (k *KubernetesChecker) CheckRBAC(ctx context.Context) []*api.CheckResult { | ||
const checkName = "kubernetes-rbac" | ||
authClient := k.client.AuthorizationV1() | ||
rbacReqs := findClosestVersionRequirements(k.coderVersion) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not important to do now, can do this as another PR -- can we check this in the Validate step, so that these errors are impossible? The idea is that only server errors or genuine failures should cause FAIL results, bad input should be caught before anything runs (calling code should call Validate, check for errors, and then Run) |
||
results := make([]*api.CheckResult, 0) | ||
if rbacReqs == nil { | ||
results = append(results, | ||
api.ErrorResult( | ||
checkName, | ||
"unable to check RBAC requirements", | ||
xerrors.Errorf("unhandled coder version: %s", k.coderVersion.String()), | ||
), | ||
) | ||
return results | ||
} | ||
|
||
for _, req := range rbacReqs.RBACRequirements { | ||
resName := fmt.Sprintf("%s-%s", checkName, req.Resource) | ||
if err := k.checkOneRBAC(ctx, authClient, req); err != nil { | ||
summary := fmt.Sprintf("missing permissions on resource %s: %s", req.Resource, err) | ||
results = append(results, api.ErrorResult(resName, summary, err)) | ||
continue | ||
} | ||
|
||
summary := fmt.Sprintf("%s: can %s", req.Resource, strings.Join(req.Verbs, ", ")) | ||
results = append(results, api.PassResult(resName, summary)) | ||
} | ||
|
||
return results | ||
} | ||
|
||
func (k *KubernetesChecker) checkOneRBAC(ctx context.Context, authClient authorizationclientv1.AuthorizationV1Interface, req *RBACRequirement) error { | ||
have := make([]string, 0, len(req.Verbs)) | ||
for _, verb := range req.Verbs { | ||
sar := &authorizationv1.SelfSubjectAccessReview{ | ||
Spec: authorizationv1.SelfSubjectAccessReviewSpec{ | ||
ResourceAttributes: &authorizationv1.ResourceAttributes{ | ||
Namespace: k.namespace, | ||
Group: req.APIGroup, | ||
Resource: req.Resource, | ||
Verb: verb, | ||
}, | ||
}, | ||
} | ||
|
||
response, err := authClient.SelfSubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{}) | ||
|
||
if err != nil { | ||
// should not fail - short-circuit | ||
return xerrors.Errorf("failed to create SelfSubjectAccessReview request: %w", err) | ||
} | ||
|
||
if response.Status.Allowed { | ||
have = append(have, verb) | ||
continue | ||
} | ||
} | ||
|
||
if len(have) != len(req.Verbs) { | ||
return xerrors.Errorf(fmt.Sprintf("need: %+v have: %+v", req.Verbs, have)) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func findClosestVersionRequirements(v *semver.Version) *VersionedRBACRequirements { | ||
for _, vreqs := range allVersionedRBACRequirements { | ||
if vreqs.VersionConstraints.Check(v) { | ||
return &vreqs | ||
} | ||
} | ||
return nil | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package kube | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
authorizationv1 "k8s.io/api/authorization/v1" | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/rest" | ||
|
||
"cdr.dev/slog/sloggers/slogtest/assert" | ||
|
||
"github.com/Masterminds/semver/v3" | ||
|
||
"github.com/cdr/coder-doctor/internal/api" | ||
) | ||
|
||
func Test_CheckRBAC(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := []struct { | ||
Name string | ||
Response *authorizationv1.SelfSubjectAccessReview | ||
F func(*testing.T, []*api.CheckResult) | ||
}{ | ||
{ | ||
Name: "all allowed", | ||
Response: &selfSubjectAccessReviewAllowed, | ||
F: func(t *testing.T, results []*api.CheckResult) { | ||
for _, result := range results { | ||
assert.True(t, result.Name+" should not error", result.Details["error"] == nil) | ||
assert.True(t, result.Name+" should pass", result.State == api.StatePassed) | ||
} | ||
}, | ||
}, | ||
{ | ||
Name: "all denied", | ||
Response: &selfSubjectAccessReviewDenied, | ||
F: func(t *testing.T, results []*api.CheckResult) { | ||
for _, result := range results { | ||
assert.True(t, result.Name+" should have an error", result.Details["error"] != nil) | ||
assert.True(t, result.Name+" should fail", result.State == api.StateFailed) | ||
} | ||
}, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
test := test | ||
t.Run(test.Name, func(t *testing.T) { | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
t.Parallel() | ||
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusOK) | ||
err := json.NewEncoder(w).Encode(test.Response) | ||
assert.Success(t, "failed to encode response", err) | ||
})) | ||
defer server.Close() | ||
|
||
client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL}) | ||
assert.Success(t, "failed to create client", err) | ||
|
||
checker := NewKubernetesChecker(client) | ||
results := checker.CheckRBAC(context.Background()) | ||
test.F(t, results) | ||
}) | ||
} | ||
} | ||
|
||
func Test_CheckRBAC_ClientError(t *testing.T) { | ||
t.Parallel() | ||
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusInternalServerError) | ||
})) | ||
defer server.Close() | ||
|
||
client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL}) | ||
assert.Success(t, "failed to create client", err) | ||
|
||
checker := NewKubernetesChecker(client) | ||
results := checker.CheckRBAC(context.Background()) | ||
for _, result := range results { | ||
assert.ErrorContains(t, result.Name+" should show correct error", result.Details["error"].(error), "failed to create SelfSubjectAccessReview request") | ||
assert.True(t, result.Name+" should fail", result.State == api.StateFailed) | ||
} | ||
} | ||
|
||
func Test_CheckRBAC_UnknownCoderVerseion(t *testing.T) { | ||
t.Parallel() | ||
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusInternalServerError) | ||
})) | ||
defer server.Close() | ||
|
||
client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL}) | ||
assert.Success(t, "failed to create client", err) | ||
|
||
checker := NewKubernetesChecker(client, WithCoderVersion(semver.MustParse("0.0.1"))) | ||
|
||
results := checker.CheckRBAC(context.Background()) | ||
for _, result := range results { | ||
assert.ErrorContains(t, result.Name+" should show correct error", result.Details["error"].(error), "unhandled coder version") | ||
assert.True(t, result.Name+" should fail", result.State == api.StateFailed) | ||
} | ||
} | ||
|
||
var selfSubjectAccessReviewAllowed authorizationv1.SelfSubjectAccessReview = authorizationv1.SelfSubjectAccessReview{ | ||
Status: authorizationv1.SubjectAccessReviewStatus{ | ||
Allowed: true, | ||
}, | ||
} | ||
|
||
var selfSubjectAccessReviewDenied authorizationv1.SelfSubjectAccessReview = authorizationv1.SelfSubjectAccessReview{ | ||
Status: authorizationv1.SubjectAccessReviewStatus{ | ||
Allowed: false, | ||
}, | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, it's kinda unfortunate that we have to import a bunch of these this way, but makes sense to me!