diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-17 16:38:49 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-17 16:38:49 -0600 |
| commit | 21e16bf3dcd0231f2f2e1ac246eed211aaa9e756 (patch) | |
| tree | 1b99bf645035b58e0d6db08c7a83521f41f7a75b /pkg/authz | |
| parent | f94f79608393d4ab127db63cc41668445ef6b243 (diff) | |
| parent | 45df4d0d9b577fecee798d672695fe24ff57fb1b (diff) | |
Merge branch 'golang' into 'main'
Rewrite authzd from Rust to Go
See merge request gitlab-org/software-supply-chain-security/authorization/authzd!11
Diffstat (limited to 'pkg/authz')
| -rw-r--r-- | pkg/authz/check_service.go | 158 | ||||
| -rw-r--r-- | pkg/authz/client.go | 51 | ||||
| -rw-r--r-- | pkg/authz/server.go | 37 | ||||
| -rw-r--r-- | pkg/authz/server_test.go | 67 |
4 files changed, 313 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" +} diff --git a/pkg/authz/client.go b/pkg/authz/client.go new file mode 100644 index 00000000..eab1fe99 --- /dev/null +++ b/pkg/authz/client.go @@ -0,0 +1,51 @@ +package authz + +import ( + "context" + "crypto/x509" + "net" + + authzed "github.com/authzed/authzed-go/v1" + "github.com/authzed/grpcutil" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/pls" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +func NewClient(ctx context.Context, host string, token string) (*authzed.Client, error) { + tokenOption := grpcutil.WithInsecureBearerToken(token) + if isTLS(ctx, host) { + tokenOption = grpcutil.WithBearerToken(token) + } + return authzed.NewClient( + host, + grpc.WithTransportCredentials(credentialsFor(ctx, host)), + tokenOption, + ) +} +func credentialsFor(ctx context.Context, host string) credentials.TransportCredentials { + if isTLS(ctx, host) { + pool, err := x509.SystemCertPool() + if err != nil { + pls.LogErrorNow(ctx, err) + return insecure.NewCredentials() + } + + return credentials.NewClientTLSFromCert(pool, "") + } + + return insecure.NewCredentials() +} + +func isTLS(ctx context.Context, host string) bool { + if host == "" { + return false + } + _, port, err := net.SplitHostPort(host) + if err != nil { + pls.LogError(ctx, err) + return false + } + return port == "443" +} diff --git a/pkg/authz/server.go b/pkg/authz/server.go new file mode 100644 index 00000000..0c680a65 --- /dev/null +++ b/pkg/authz/server.go @@ -0,0 +1,37 @@ +package authz + +import ( + "context" + + authzed "github.com/authzed/authzed-go/v1" + auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + xcontext "github.com/xlgmokha/x/pkg/context" + "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" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var Connection xcontext.Key[*authzed.Client] = xcontext.Key[*authzed.Client]("authzed_client") + +type Server struct { + *grpc.Server +} + +func New(ctx context.Context, options ...grpc.ServerOption) *Server { + logger := log.From(ctx) + + server := grpc.NewServer(x.Prepend( + options, + grpc.UnaryInterceptor(pls.LogGRPC(logger)), + grpc.StreamInterceptor(pls.LogGRPCStream(logger)), + )...) + + auth.RegisterAuthorizationServer(server, NewCheckService(Connection.From(ctx))) + reflection.Register(server) + + return &Server{ + Server: server, + } +} diff --git a/pkg/authz/server_test.go b/pkg/authz/server_test.go new file mode 100644 index 00000000..47f22191 --- /dev/null +++ b/pkg/authz/server_test.go @@ -0,0 +1,67 @@ +package authz + +import ( + "context" + "net" + "testing" + + auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +type HTTPRequest = auth.AttributeContext_HttpRequest + +func TestServer(t *testing.T) { + socket := bufconn.Listen(1024 * 1024) + srv := New(t.Context()) + + defer srv.GracefulStop() + go func() { + require.NoError(t, srv.Serve(socket)) + }() + + connection, err := grpc.DialContext( + t.Context(), + "bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return socket.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + require.NoError(t, err) + defer connection.Close() + + client := auth.NewAuthorizationClient(connection) + + t.Run("CheckRequest", func(t *testing.T) { + tt := []struct { + http *HTTPRequest + status codes.Code + }{ + {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/application.js"}}, + {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/favicon.ico"}}, + {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/favicon.png"}}, + {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/index.html"}}, + {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/application.css"}}, + } + + for _, example := range tt { + t.Run(example.http.Path, func(t *testing.T) { + response, err := client.Check(t.Context(), &auth.CheckRequest{ + Attributes: &auth.AttributeContext{ + Request: &auth.AttributeContext_Request{ + Http: example.http, + }, + }, + }) + require.NoError(t, err) + assert.Equal(t, int32(example.status), response.Status.Code) + }) + } + }) +} |
