summaryrefslogtreecommitdiff
path: root/pkg/authz
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/authz')
-rw-r--r--pkg/authz/check_service.go114
-rw-r--r--pkg/authz/check_service_test.go95
-rw-r--r--pkg/authz/server.go31
-rw-r--r--pkg/authz/server_test.go86
4 files changed, 326 insertions, 0 deletions
diff --git a/pkg/authz/check_service.go b/pkg/authz/check_service.go
new file mode 100644
index 0000000..ff4e92a
--- /dev/null
+++ b/pkg/authz/check_service.go
@@ -0,0 +1,114 @@
+package authz
+
+import (
+ "context"
+ "strings"
+
+ 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"
+ status "google.golang.org/genproto/googleapis/rpc/status"
+ "google.golang.org/grpc/codes"
+)
+
+type CheckService struct {
+ auth.UnimplementedAuthorizationServer
+}
+
+func (svc *CheckService) Check(ctx context.Context, request *auth.CheckRequest) (*auth.CheckResponse, error) {
+ if svc.isAllowed(ctx, request) {
+ return svc.OK(ctx), nil
+ }
+ return svc.Denied(ctx), nil
+}
+
+// TODOD:: Replace with a PaC language
+func (svc *CheckService) isPublic(ctx context.Context, r *auth.CheckRequest) bool {
+ allowed := map[string]bool{
+ "GET:/": true,
+ "GET:/application.js": true,
+ "GET:/callback": true,
+ "GET:/dashboard/nav": true,
+ "GET:/favicon.ico": true,
+ "GET:/favicon.png": true,
+ "GET:/health": true,
+ "GET:/index.html": true,
+ "GET:/logo.png": true,
+ "GET:/signout": true,
+ "GET:/sparkle": true,
+ "GET:/sparkles": true,
+ "POST:/sparkles/restore": true,
+ }
+ ok, _ := allowed[strings.Join([]string{
+ r.Attributes.Request.Http.Method,
+ r.Attributes.Request.Http.Path,
+ }, ":")]
+ return ok
+}
+
+func (svc *CheckService) isAllowed(ctx context.Context, r *auth.CheckRequest) bool {
+ if !svc.validRequest(ctx, r) {
+ return false
+ }
+
+ log.WithFields(ctx, svc.fieldsFor(r))
+ return svc.isLoggedIn(ctx, r) || svc.isPublic(ctx, r)
+}
+
+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)
+}
+
+// TODO:: Replace this naive implementation
+func (svc *CheckService) isLoggedIn(ctx context.Context, r *auth.CheckRequest) bool {
+ return x.IsPresent(r.Attributes.Request.Http.Headers["cookie"])
+}
+
+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{
+ "id": r.Attributes.Request.Http.Id,
+ "method": r.Attributes.Request.Http.Method,
+ "path": r.Attributes.Request.Http.Path,
+ "host": r.Attributes.Request.Http.Host,
+ "scheme": r.Attributes.Request.Http.Scheme,
+ "protocol": r.Attributes.Request.Http.Protocol,
+ }
+}
diff --git a/pkg/authz/check_service_test.go b/pkg/authz/check_service_test.go
new file mode 100644
index 0000000..2f640dc
--- /dev/null
+++ b/pkg/authz/check_service_test.go
@@ -0,0 +1,95 @@
+package authz
+
+import (
+ "strings"
+ "testing"
+
+ core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+ auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+func TestCheckService(t *testing.T) {
+ svc := CheckService{}
+
+ t.Run("allows access", func(t *testing.T) {
+ idToken := "eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19.TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w"
+ accessToken := "f88f60df11e458b594c80b299aee05f8e5805c65c3e779cc6fbc606c4ac36227"
+ refreshToken := "0847d325d6e4f021c4baaae0ddb425dbd8795807a4751cd2131bec8e8a9aee24"
+
+ cookies := []string{
+ "bearer_token=" + accessToken + ";",
+ "id_token=" + idToken + ";",
+ "refresh_token=" + refreshToken,
+ }
+
+ response, err := svc.Check(t.Context(), &auth.CheckRequest{
+ Attributes: &auth.AttributeContext{
+ Source: &auth.AttributeContext_Peer{
+ Address: &core.Address{
+ Address: &core.Address_SocketAddress{
+ SocketAddress: &core.SocketAddress{
+ Address: "127.0.0.1",
+ PortSpecifier: &core.SocketAddress_PortValue{
+ PortValue: 52358,
+ },
+ },
+ },
+ },
+ },
+ Destination: &auth.AttributeContext_Peer{
+ Address: &core.Address{
+ Address: &core.Address_SocketAddress{
+ SocketAddress: &core.SocketAddress{
+ Address: "127.0.0.1",
+ PortSpecifier: &core.SocketAddress_PortValue{
+ PortValue: 10000,
+ },
+ },
+ },
+ },
+ },
+ Request: &auth.AttributeContext_Request{
+ Time: &timestamppb.Timestamp{Seconds: 1747937928, Nanos: 476481000},
+ Http: &auth.AttributeContext_HttpRequest{
+ Id: "1248474133684962828",
+ Method: "GET",
+ Headers: map[string]string{
+ ":authority": "localhost:10000",
+ ":method": "GET",
+ ":path": "/health",
+ ":scheme": "http",
+ "accept": "*/*",
+ "accept-encoding": "gzip, deflate, br, zstd",
+ "accept-language": "en-US,en;q=0.9",
+ "cache-control": "max-age=0",
+ "content-length": "64",
+ "content-type": "application/json",
+ "cookie": strings.Join(cookies, "; "),
+ "origin": "http://localhost:10000",
+ "referer": "http://localhost:10000/dashboard",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "Linux",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-origin",
+ "x-forwarded-proto": "http",
+ "x-request-id": "7e064610-9e19-4a38-8354-0de0b5fbd7c6",
+ },
+ Path: "/health",
+ Host: "localhost:10000",
+ Scheme: "http",
+ Protocol: "HTTP/1.1",
+ },
+ },
+ MetadataContext: &core.Metadata{},
+ RouteMetadataContext: &core.Metadata{},
+ },
+ })
+
+ require.NoError(t, err)
+ assert.NotNil(t, response.GetOkResponse())
+ })
+}
diff --git a/pkg/authz/server.go b/pkg/authz/server.go
new file mode 100644
index 0000000..49bcd3d
--- /dev/null
+++ b/pkg/authz/server.go
@@ -0,0 +1,31 @@
+package authz
+
+import (
+ "context"
+
+ auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
+ "github.com/xlgmokha/x/pkg/log"
+ "github.com/xlgmokha/x/pkg/x"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/reflection"
+)
+
+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, &CheckService{})
+ 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 0000000..6fa4eee
--- /dev/null
+++ b/pkg/authz/server_test.go
@@ -0,0 +1,86 @@
+package authz
+
+import (
+ "context"
+ "net"
+ "strings"
+ "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"
+)
+
+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)
+ idToken := "eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19.TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w"
+ accessToken := "f88f60df11e458b594c80b299aee05f8e5805c65c3e779cc6fbc606c4ac36227"
+ refreshToken := "0847d325d6e4f021c4baaae0ddb425dbd8795807a4751cd2131bec8e8a9aee24"
+
+ cookies := []string{
+ "bearer_token=" + accessToken + ";",
+ "id_token=" + idToken + ";",
+ "refresh_token=" + refreshToken,
+ }
+ loggedInHeaders := map[string]string{"cookie": strings.Join(cookies, "; ")}
+
+ t.Run("CheckRequest", func(t *testing.T) {
+ tt := []struct {
+ http *auth.AttributeContext_HttpRequest
+ status codes.Code
+ }{
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/application.js"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/callback"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/dashboard", Headers: loggedInHeaders}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/dashboard/nav"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/favicon.ico"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/favicon.png"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/health"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/index.html"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/logo.png"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/signout"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/sparkles"}},
+ {status: codes.OK, http: &auth.AttributeContext_HttpRequest{Method: "POST", Path: "/sparkles/restore"}},
+ {status: codes.PermissionDenied, http: &auth.AttributeContext_HttpRequest{Method: "GET", Path: "/dashboard"}},
+ {status: codes.PermissionDenied, http: &auth.AttributeContext_HttpRequest{Method: "POST", Path: "/sparkles"}},
+ }
+
+ 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)
+ })
+ }
+ })
+}