summaryrefslogtreecommitdiff
path: root/pkg/authz
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/authz')
-rw-r--r--pkg/authz/check_service.go91
-rw-r--r--pkg/authz/id_token.go40
-rw-r--r--pkg/authz/id_token_test.go30
-rw-r--r--pkg/authz/server.go3
-rw-r--r--pkg/authz/server_test.go60
5 files changed, 171 insertions, 53 deletions
diff --git a/pkg/authz/check_service.go b/pkg/authz/check_service.go
index ff4e92a..641ba92 100644
--- a/pkg/authz/check_service.go
+++ b/pkg/authz/check_service.go
@@ -2,6 +2,7 @@ package authz
import (
"context"
+ "net/http"
"strings"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
@@ -9,14 +10,35 @@ import (
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/sparkled/pkg/pls"
status "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
)
+var public map[string]bool = 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,
+}
+
type CheckService struct {
auth.UnimplementedAuthorizationServer
}
+func NewCheckService() *CheckService {
+ return &CheckService{}
+}
+
func (svc *CheckService) Check(ctx context.Context, request *auth.CheckRequest) (*auth.CheckResponse, error) {
if svc.isAllowed(ctx, request) {
return svc.OK(ctx), nil
@@ -24,27 +46,8 @@ func (svc *CheckService) Check(ctx context.Context, request *auth.CheckRequest)
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,
- }, ":")]
+ ok, _ := public[svc.keyFor(r.Attributes.Request.Http)]
return ok
}
@@ -54,7 +57,7 @@ func (svc *CheckService) isAllowed(ctx context.Context, r *auth.CheckRequest) bo
}
log.WithFields(ctx, svc.fieldsFor(r))
- return svc.isLoggedIn(ctx, r) || svc.isPublic(ctx, r)
+ return svc.isPublic(ctx, r) || svc.isLoggedIn(ctx, r)
}
func (svc *CheckService) validRequest(ctx context.Context, r *auth.CheckRequest) bool {
@@ -66,7 +69,34 @@ func (svc *CheckService) validRequest(ctx context.Context, r *auth.CheckRequest)
// 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"])
+ rawCookie := r.Attributes.Request.Http.Headers["cookie"]
+ if x.IsPresent(rawCookie) {
+ cookies, err := http.ParseCookie(rawCookie)
+ if err != nil {
+ pls.LogError(ctx, err)
+ return false
+ }
+ idTokenCookie := x.Find(cookies, func(cookie *http.Cookie) bool {
+ return cookie.Name == "id_token"
+ })
+ if x.IsZero(idTokenCookie) {
+ return false
+ }
+ segments := strings.SplitN(idTokenCookie.Value, ".", 3)
+ if len(segments) != 3 {
+ return false
+ }
+ idToken, err := NewIDToken(idTokenCookie.Value)
+ if err != nil {
+ pls.LogError(ctx, err)
+ return false
+ }
+ if x.IsZero(idToken) {
+ return false
+ }
+ return true
+ }
+ return false
}
func (svc *CheckService) OK(ctx context.Context) *auth.CheckResponse {
@@ -104,11 +134,16 @@ func (svc *CheckService) Denied(ctx context.Context) *auth.CheckResponse {
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,
+ "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,
}
}
+
+func (svc *CheckService) keyFor(r *auth.AttributeContext_HttpRequest) string {
+ return strings.Join([]string{r.Method, r.Path}, ":")
+}
diff --git a/pkg/authz/id_token.go b/pkg/authz/id_token.go
new file mode 100644
index 0000000..ccc96de
--- /dev/null
+++ b/pkg/authz/id_token.go
@@ -0,0 +1,40 @@
+package authz
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "strings"
+ "time"
+)
+
+type IDToken struct {
+ // Audience []string `json:"aud"`
+ Email string `json:"email"`
+ EmailVerified bool `json:"email_verified"`
+ ExpiredAt int64 `json:"exp"`
+ IssuedAt int64 `json:"iat"`
+ Issuer string `json:"iss"`
+ Name string `json:"name"`
+ Nickname string `json:"nickname"`
+ Picture string `json:"picture"`
+ Subject string `json:"sub"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+func NewIDToken(raw string) (*IDToken, error) {
+ sections := strings.SplitN(raw, ".", 3)
+ if len(sections) != 3 {
+ return nil, errors.New("Invalid token")
+ }
+ bytes, err := base64.RawURLEncoding.DecodeString(sections[1])
+ if err != nil {
+ return nil, err
+ }
+
+ token := &IDToken{}
+ if err := json.Unmarshal(bytes, token); err != nil {
+ return nil, err
+ }
+ return token, nil
+}
diff --git a/pkg/authz/id_token_test.go b/pkg/authz/id_token_test.go
new file mode 100644
index 0000000..22aabc4
--- /dev/null
+++ b/pkg/authz/id_token_test.go
@@ -0,0 +1,30 @@
+package authz
+
+import (
+ "testing"
+
+ "github.com/oauth2-proxy/mockoidc"
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web"
+)
+
+func TestIDToken(t *testing.T) {
+ idp := web.NewOIDCServer(t)
+ defer idp.Close()
+
+ t.Run("when the token is valid", func(t *testing.T) {
+ user := mockoidc.DefaultUser()
+ _, rawIDToken := idp.CreateTokensFor(user)
+ token, err := NewIDToken(rawIDToken)
+
+ require.NoError(t, err)
+ require.NotNil(t, token)
+ })
+
+ t.Run("when the token is invalid", func(t *testing.T) {
+ token, err := NewIDToken("invalid")
+
+ require.Error(t, err)
+ require.Nil(t, token)
+ })
+}
diff --git a/pkg/authz/server.go b/pkg/authz/server.go
index 49bcd3d..e1b0669 100644
--- a/pkg/authz/server.go
+++ b/pkg/authz/server.go
@@ -17,12 +17,13 @@ type Server struct {
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{})
+ auth.RegisterAuthorizationServer(server, NewCheckService())
reflection.Register(server)
return &Server{
diff --git a/pkg/authz/server_test.go b/pkg/authz/server_test.go
index 6fa4eee..e8f179e 100644
--- a/pkg/authz/server_test.go
+++ b/pkg/authz/server_test.go
@@ -7,15 +7,23 @@ import (
"testing"
auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
+ "github.com/oauth2-proxy/mockoidc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web"
"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) {
+ idp := web.NewOIDCServer(t)
+ defer idp.Close()
+
socket := bufconn.Listen(1024 * 1024)
srv := New(t.Context())
@@ -36,37 +44,41 @@ func TestServer(t *testing.T) {
defer connection.Close()
client := auth.NewAuthorizationClient(connection)
- idToken := "eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19.TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w"
- accessToken := "f88f60df11e458b594c80b299aee05f8e5805c65c3e779cc6fbc606c4ac36227"
- refreshToken := "0847d325d6e4f021c4baaae0ddb425dbd8795807a4751cd2131bec8e8a9aee24"
+ user := mockoidc.DefaultUser()
+ _, rawIDToken := idp.CreateTokensFor(user)
- cookies := []string{
- "bearer_token=" + accessToken + ";",
- "id_token=" + idToken + ";",
- "refresh_token=" + refreshToken,
+ loggedInHeaders := map[string]string{
+ "cookie": strings.Join([]string{
+ "bearer_token=" + pls.GenerateRandomHex(32),
+ "id_token=" + rawIDToken,
+ "refresh_token=" + pls.GenerateRandomHex(32),
+ }, "; "),
}
- loggedInHeaders := map[string]string{"cookie": strings.Join(cookies, "; ")}
+
+ invalidHeaders := map[string]string{"cookie": strings.Join([]string{"id_token=invalid"}, "; ")}
t.Run("CheckRequest", func(t *testing.T) {
tt := []struct {
- http *auth.AttributeContext_HttpRequest
+ http *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"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/application.js"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/callback"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/dashboard", Headers: loggedInHeaders}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/dashboard/nav"}},
+ {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: "/health"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/index.html"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/logo.png"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/signout"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/sparkles"}},
+ {status: codes.OK, http: &HTTPRequest{Method: "POST", Path: "/sparkles", Headers: loggedInHeaders}},
+ {status: codes.OK, http: &HTTPRequest{Method: "POST", Path: "/sparkles/restore"}},
+ {status: codes.PermissionDenied, http: &HTTPRequest{Method: "GET", Path: "/dashboard"}},
+ {status: codes.PermissionDenied, http: &HTTPRequest{Method: "GET", Path: "/dashboard", Headers: invalidHeaders}},
+ {status: codes.PermissionDenied, http: &HTTPRequest{Method: "POST", Path: "/sparkles"}},
}
for _, example := range tt {