Skip to content

Commit

Permalink
Merge pull request #58 from authzed/fixes-for-integration-with-cloud
Browse files Browse the repository at this point in the history
fixes for integration with cloud
  • Loading branch information
vroldanbet authored Oct 11, 2023
2 parents 1a27743 + 78f7d7b commit 566b8f8
Show file tree
Hide file tree
Showing 14 changed files with 142 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/testbin/
/**/apiserver.local.config/
client-ca.crt
client-cert.crt
*__failpoint_*.go
*.go__failpoint*
*.sqlite
Expand Down
9 changes: 9 additions & 0 deletions e2e/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@ var _ = Describe("Proxy", func() {
failpoints.EnableFailPoint("panicKubeReadResp", 1)
Expect(DeletePod(ctx, paulClient, paulNamespace, paulPod)).ToNot(BeNil())

// Make sure tuples are gone
owners := GetAllTuples(ctx, &v1.RelationshipFilter{
ResourceType: "pod",
OptionalResourceId: paulNamespace + "/" + paulPod,
OptionalRelation: "creator",
OptionalSubjectFilter: &v1.SubjectFilter{SubjectType: "user"},
})
Expect(len(owners)).To(BeZero())

// the pod is gone on subsequent calls
Expect(k8serrors.IsUnauthorized(GetPod(ctx, paulClient, paulNamespace, paulPod))).To(BeTrue())
Expect(k8serrors.IsNotFound(GetPod(ctx, adminClient, paulNamespace, paulPod))).To(BeTrue())
Expand Down
8 changes: 8 additions & 0 deletions magefiles/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build mage

package main

var Aliases = map[string]interface{}{
"test": Test.Unit,
"up": Dev.Up,
}
6 changes: 6 additions & 0 deletions magefiles/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ func generateDevKubeconfig(ctx context.Context, proxyHost string) error {
return err
}

certPath := path.Join(wd, "client-cert.crt")
if err := os.WriteFile(certPath, clientCertBytes, 0o600); err != nil {
fmt.Printf("unable to cache proxy client certificate in host machine: %s\n", err.Error())
}

clientKeyBytes, err := getBytesFromSecretField(ctx, clientset, "spicedb-kubeapi-proxy", "rakis-client-cert", "tls.key")
if err != nil {
return err
Expand Down Expand Up @@ -219,6 +224,7 @@ func (d Dev) Clean() error {
_ = os.Remove(kubeconfigPath)
_ = os.Remove(developmentKubeConfigFileName)
_ = os.Remove("client-ca.crt")
_ = os.Remove("client-cert.crt")
return nil
}

Expand Down
27 changes: 23 additions & 4 deletions pkg/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package authz

import (
"context"
"k8s.io/klog/v2"
"net/http"

v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
Expand All @@ -19,7 +20,7 @@ func WithAuthorization(handler, failed http.Handler, permissionsClient v1.Permis

input, err := rules.NewResolveInputFromHttp(req)
if err != nil {
failed.ServeHTTP(w, req)
handleError(w, failed, req, err)
return
}

Expand All @@ -31,17 +32,31 @@ func WithAuthorization(handler, failed http.Handler, permissionsClient v1.Permis
}

matchingRules := (*matcher).Match(input.Request)

if len(matchingRules) == 0 {
klog.V(3).InfoSDepth(1,
"request did not match any authorization rule",
"verb", input.Request.Verb,
"APIGroup", input.Request.APIGroup,
"APIVersion", input.Request.APIVersion,
"Resource", input.Request.Resource)
} else {
klog.V(3).InfoSDepth(1,
"request matched authorization rule/s",
"verb", input.Request.Verb,
"APIGroup", input.Request.APIGroup,
"APIVersion", input.Request.APIVersion,
"Resource", input.Request.Resource)
}
// run all checks for this request
if err := runAllMatchingChecks(ctx, matchingRules, input, permissionsClient); err != nil {
failed.ServeHTTP(w, req)
handleError(w, failed, req, err)
return
}

// if this request is a write, perform the dual write and return
if rule := getWriteRule(matchingRules); rule != nil {
if err := write(ctx, w, rule, input, workflowClient); err != nil {
failed.ServeHTTP(w, req)
handleError(w, failed, req, err)
return
}
return
Expand All @@ -68,6 +83,10 @@ func WithAuthorization(handler, failed http.Handler, permissionsClient v1.Permis
}), nil
}

func handleError(w http.ResponseWriter, failHandler http.Handler, req *http.Request, err error) {
failHandler.ServeHTTP(w, req)
}

// alwaysAllow allows unfiltered access to api metadata
func alwaysAllow(info *request.RequestInfo) bool {
return (info.Path == "/api" || info.Path == "/apis" || info.Path == "/openapi/v2") && info.Verb == "get"
Expand Down
14 changes: 10 additions & 4 deletions pkg/authz/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package authz

import (
"context"
"errors"
"fmt"
"k8s.io/klog/v2"

v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
"golang.org/x/sync/errgroup"

"github.com/authzed/spicedb-kubeapi-proxy/pkg/rules"
)

var ErrUnauthorized = errors.New("unauthorized operation")

func runAllMatchingChecks(ctx context.Context, matchingRules []*rules.RunnableRule, input *rules.ResolveInput, client v1.PermissionsServiceClient) error {
var checkGroup errgroup.Group

Expand All @@ -21,7 +25,7 @@ func runAllMatchingChecks(ctx context.Context, matchingRules []*rules.RunnableRu
if err != nil {
return err
}
resp, err := client.CheckPermission(ctx, &v1.CheckPermissionRequest{
req := &v1.CheckPermissionRequest{
Consistency: &v1.Consistency{
Requirement: &v1.Consistency_MinimizeLatency{MinimizeLatency: true},
},
Expand All @@ -37,12 +41,14 @@ func runAllMatchingChecks(ctx context.Context, matchingRules []*rules.RunnableRu
},
OptionalRelation: rel.SubjectRelation,
},
})
}
resp, err := client.CheckPermission(ctx, req)
klog.V(3).InfoSDepth(1, "CheckPermission", "request", req, "response", resp, "error", err)
if err != nil {
return err
return fmt.Errorf("failed runAllMatchingChecks: %w", err)
}
if resp.Permissionship != v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION {
return fmt.Errorf("failed runAllMatchingChecks for %v", rel)
return ErrUnauthorized
}
return nil
})
Expand Down
12 changes: 11 additions & 1 deletion pkg/authz/distributedtx/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package distributedtx

import (
"fmt"
"k8s.io/klog/v2"
"net/http"
"time"

Expand Down Expand Up @@ -205,13 +206,14 @@ func PessimisticWriteToSpiceDBAndKube(ctx workflow.Context, input *WriteObjInput
continue
}

if out.StatusCode == http.StatusConflict || out.StatusCode == http.StatusCreated || out.StatusCode == http.StatusOK {
if isSuccessfulCreate(input, out) || isSuccessfulDelete(input, out) {
rollback.Cleanup(ctx)
return out, nil
}

// some other status code is some other type of error, remove
// the original tuple and the lock tuple
klog.V(3).ErrorS(err, "unsuccessful Kube API operation on PessimisticWriteToSpiceDBAndKube", "response", out)
rollback.WithRels(updates...).Cleanup(ctx)
return out, nil
}
Expand All @@ -220,6 +222,14 @@ func PessimisticWriteToSpiceDBAndKube(ctx workflow.Context, input *WriteObjInput
return nil, fmt.Errorf("failed to communicate with kubernetes after %d attempts", MaxKubeAttempts)
}

func isSuccessfulDelete(input *WriteObjInput, out *KubeResp) bool {
return input.RequestInfo.Verb == "delete" && (out.StatusCode == http.StatusNotFound || out.StatusCode == http.StatusOK)
}

func isSuccessfulCreate(input *WriteObjInput, out *KubeResp) bool {
return input.RequestInfo.Verb == "create" && (out.StatusCode == http.StatusConflict || out.StatusCode == http.StatusCreated || out.StatusCode == http.StatusOK)
}

// OptimisticWriteToSpiceDBAndKube ensures that a write exists in both SpiceDB and kube,
// or neither. It attempts to perform the writes and rolls back if errors are
// encountered, leaving the user to retry on write conflicts.
Expand Down
4 changes: 2 additions & 2 deletions pkg/authz/distributedtx/workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestWorkflow(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

srv, err := spicedb.NewServer(ctx)
srv, err := spicedb.NewServer(ctx, "")
require.NoError(t, err)
go func() {
require.NoError(t, srv.Run(ctx))
Expand Down Expand Up @@ -67,7 +67,7 @@ func TestWorkflow(t *testing.T) {
id, err := workflowClient.CreateWorkflowInstance(ctx, client.WorkflowInstanceOptions{
InstanceID: uuid.NewString(),
}, workflowFunc, &WriteObjInput{
RequestInfo: &request.RequestInfo{},
RequestInfo: &request.RequestInfo{Verb: "create"},
UserInfo: &user.DefaultInfo{Name: "janedoe"},
ObjectMeta: &metav1.ObjectMeta{Name: "my_object_meta"},
Rels: []*v1.Relationship{{
Expand Down
53 changes: 38 additions & 15 deletions pkg/authz/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/kyverno/go-jmespath"
"io"
"k8s.io/klog/v2"

v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -32,7 +34,7 @@ func filterResponse(ctx context.Context, matchingRules []*rules.RunnableRule, in

switch input.Request.Verb {
case "list":
filterList(ctx, client, filter, authzData)
filterList(ctx, client, filter, input, authzData)
// only one filter allowed per request
return nil
case "watch":
Expand Down Expand Up @@ -63,14 +65,14 @@ type wrapper struct {
SubjectID string `json:"subjectId"`
}

func filterList(ctx context.Context, client v1.PermissionsServiceClient, filter *rules.ResolvedPreFilter, authzData *AuthzData) {
func filterList(ctx context.Context, client v1.PermissionsServiceClient, filter *rules.ResolvedPreFilter, input *rules.ResolveInput, authzData *AuthzData) {
go func() {
authzData.Lock()
defer authzData.Unlock()
defer close(authzData.allowedNNC)
defer close(authzData.removedNNC)

lr, err := client.LookupResources(ctx, &v1.LookupResourcesRequest{
req := &v1.LookupResourcesRequest{
Consistency: &v1.Consistency{
Requirement: &v1.Consistency_MinimizeLatency{MinimizeLatency: true},
},
Expand All @@ -83,9 +85,11 @@ func filterList(ctx context.Context, client v1.PermissionsServiceClient, filter
},
OptionalRelation: filter.Rel.SubjectRelation,
},
})
}
klog.V(3).InfoSDepth(1, "LookupResources", "request", req)
lr, err := client.LookupResources(ctx, req)
if err != nil {
fmt.Println(err)
handleFilterListError(err)
return
}
for {
Expand All @@ -95,50 +99,69 @@ func filterList(ctx context.Context, client v1.PermissionsServiceClient, filter
}

if err != nil {
fmt.Println(err)
handleFilterListError(err)
return
}

if resp.Permissionship != v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION {
klog.V(3).InfoS("denying conditional resource in list", "resource_type", filter.Rel.ResourceType, "resource_id", resp.ResourceObjectId, "condition", resp.PartialCaveatInfo.String())
continue
}

byteIn, err := json.Marshal(wrapper{ResourceID: resp.ResourceObjectId})
if err != nil {
fmt.Println(err)
handleFilterListError(err)
return
}
var data any
if err := json.Unmarshal(byteIn, &data); err != nil {
fmt.Println(err)
handleFilterListError(err)
return
}

fmt.Println("GOT WATCH FILTER EVENT", string(byteIn))
klog.V(4).InfoS("received list filter event", "event", string(byteIn))
name, err := filter.Name.Search(data)
if err != nil {
fmt.Println(err)
handleFilterListError(err)
return
}
if name == nil || len(name.(string)) == 0 {
klog.V(3).InfoS("unable to determine name for resource", "event", string(byteIn))
return
}

namespace, err := filter.Namespace.Search(data)
if err != nil {
fmt.Println(err)
return
if _, ok := err.(jmespath.NotFoundError); !ok {
handleFilterListError(err)
return
}
}
if namespace == nil {
namespace, err = filter.Namespace.Search(input)
if err != nil {
handleFilterListError(err)
return
}
}
if namespace == nil {
namespace = ""
}
fmt.Println("NAMENS", name, namespace)

// TODO: check permissionship?
authzData.allowedNN[types.NamespacedName{
Name: name.(string),
Namespace: namespace.(string),
}] = struct{}{}
fmt.Println("allowed", resp.ResourceObjectId)

klog.V(3).InfoS("allowed resource in list/LR response", "resource_type", filter.Rel.ResourceType, "resource_id", resp.ResourceObjectId)
}
}()
}

func handleFilterListError(err error) {
klog.V(3).ErrorS(err, "error on filterList")
}

func filterWatch(ctx context.Context, client v1.PermissionsServiceClient, watchClient v1.WatchServiceClient, filter *rules.ResolvedPreFilter, input *rules.ResolveInput, authzData *AuthzData) {
go func() {
defer close(authzData.allowedNNC)
Expand Down
Loading

0 comments on commit 566b8f8

Please sign in to comment.