diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-15 16:37:08 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-17 16:30:22 -0600 |
| commit | 45df4d0d9b577fecee798d672695fe24ff57fb1b (patch) | |
| tree | 1b99bf645035b58e0d6db08c7a83521f41f7a75b /pkg/authz/check_service.go | |
| parent | f94f79608393d4ab127db63cc41668445ef6b243 (diff) | |
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.
Diffstat (limited to 'pkg/authz/check_service.go')
| -rw-r--r-- | pkg/authz/check_service.go | 158 |
1 files changed, 158 insertions, 0 deletions
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" +} |
