From 45df4d0d9b577fecee798d672695fe24ff57fb1b Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 15 Jul 2025 16:37:08 -0600 Subject: feat: migrate from Cedar to SpiceDB authorization system This is a major architectural change that replaces the Cedar policy-based authorization system with SpiceDB's relation-based authorization. Key changes: - Migrate from Rust to Go implementation - Replace Cedar policies with SpiceDB schema and relationships - Switch from envoy `ext_authz` with Cedar to SpiceDB permission checks - Update build system and dependencies for Go ecosystem - Maintain Envoy integration for external authorization This change enables more flexible permission modeling through SpiceDB's Google Zanzibar inspired relation-based system, supporting complex hierarchical permissions that were difficult to express in Cedar. Breaking change: Existing Cedar policies and Rust-based configuration will no longer work and need to be migrated to SpiceDB schema. --- pkg/authz/check_service.go | 158 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 pkg/authz/check_service.go (limited to 'pkg/authz/check_service.go') diff --git a/pkg/authz/check_service.go b/pkg/authz/check_service.go new file mode 100644 index 00000000..4df0ebe7 --- /dev/null +++ b/pkg/authz/check_service.go @@ -0,0 +1,158 @@ +package authz + +import ( + "context" + "net/http" + "path/filepath" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + authzed "github.com/authzed/authzed-go/v1" + core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + types "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/xlgmokha/x/pkg/log" + "github.com/xlgmokha/x/pkg/x" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/pls" + status "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc/codes" +) + +var StaticAssets []string = []string{".png", ".css", ".html", ".js", ".ico"} + +type CheckService struct { + client *authzed.Client + auth.UnimplementedAuthorizationServer +} + +func NewCheckService(client *authzed.Client) auth.AuthorizationServer { + return &CheckService{ + client: client, + } +} + +func (svc *CheckService) Check(ctx context.Context, request *auth.CheckRequest) (*auth.CheckResponse, error) { + if svc.isAuthorized(ctx, request) { + return svc.OK(ctx), nil + } + return svc.Denied(ctx), nil +} + +func (svc *CheckService) isAuthorized(ctx context.Context, r *auth.CheckRequest) bool { + if !svc.validRequest(ctx, r) { + return false + } + log.WithFields(ctx, svc.fieldsFor(r)) + + if svc.isStaticAsset(ctx, r) { + return true + } + + if x.IsZero(svc.client) { + return false + } + + response, err := svc.client.CheckPermission(ctx, svc.mapFrom(ctx, r)) + if err != nil { + pls.LogError(ctx, err) + return false + } + return response.Permissionship == v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION +} + +func (svc *CheckService) isStaticAsset(ctx context.Context, r *auth.CheckRequest) bool { + if r.Attributes.Request.Http.Method != http.MethodGet { + return false + } + + extension := filepath.Ext(r.Attributes.Request.Http.Path) + return x.Contains(StaticAssets, func(item string) bool { + return item == extension + }) +} + +func (svc *CheckService) validRequest(ctx context.Context, r *auth.CheckRequest) bool { + return x.IsPresent(r) && + x.IsPresent(r.Attributes) && + x.IsPresent(r.Attributes.Request) && + x.IsPresent(r.Attributes.Request.Http) +} + +func (svc *CheckService) OK(ctx context.Context) *auth.CheckResponse { + log.WithFields(ctx, log.Fields{"authorized": true}) + return &auth.CheckResponse{ + Status: &status.Status{ + Code: int32(codes.OK), + }, + HttpResponse: &auth.CheckResponse_OkResponse{ + OkResponse: &auth.OkHttpResponse{ + Headers: []*core.HeaderValueOption{}, + HeadersToRemove: []string{}, + ResponseHeadersToAdd: []*core.HeaderValueOption{}, + }, + }, + } +} + +func (svc *CheckService) Denied(ctx context.Context) *auth.CheckResponse { + log.WithFields(ctx, log.Fields{"authorized": false}) + return &auth.CheckResponse{ + Status: &status.Status{ + Code: int32(codes.PermissionDenied), + }, + HttpResponse: &auth.CheckResponse_DeniedResponse{ + DeniedResponse: &auth.DeniedHttpResponse{ + Status: &types.HttpStatus{ + Code: types.StatusCode_Unauthorized, + }, + Headers: []*core.HeaderValueOption{}, + }, + }, + } +} + +func (svc *CheckService) fieldsFor(r *auth.CheckRequest) log.Fields { + return log.Fields{ + "host": r.Attributes.Request.Http.Host, + "id": r.Attributes.Request.Http.Id, + "method": r.Attributes.Request.Http.Method, + "path": r.Attributes.Request.Http.Path, + "protocol": r.Attributes.Request.Http.Protocol, + "request_id": r.Attributes.Request.Http.Headers["x-request-id"], + "scheme": r.Attributes.Request.Http.Scheme, + "subject": r.Attributes.Request.Http.Headers["x-jwt-claim-username"], + } +} + +func (svc *CheckService) mapFrom(ctx context.Context, r *auth.CheckRequest) *v1.CheckPermissionRequest { + return &v1.CheckPermissionRequest{ + Resource: svc.resourceFrom(ctx, r), + Permission: svc.permissionFrom(ctx, r), + Subject: svc.subjectFrom(ctx, r), + } +} + +func (svc *CheckService) resourceFrom(ctx context.Context, r *auth.CheckRequest) *v1.ObjectReference { + return &v1.ObjectReference{ + ObjectType: "project", + ObjectId: "1", + } +} + +func (svc *CheckService) subjectFrom(ctx context.Context, r *auth.CheckRequest) *v1.SubjectReference { + //TODO:: username is not ideal but it works for demo purposes + username := r.Attributes.Request.Http.Headers["x-jwt-claim-username"] + if x.IsZero(username) { + username = "public" + } + + return &v1.SubjectReference{ + Object: &v1.ObjectReference{ + ObjectType: "user", + ObjectId: username, + }, + } +} + +func (svc *CheckService) permissionFrom(ctx context.Context, r *auth.CheckRequest) string { + return "read" +} -- cgit v1.2.3