mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 15:47:36 +02:00
authorize: move headers and jwt signing to rego (#1856)
* wip * wip * wip * remove SignedJWT field * set google_cloud_serverless_authentication_service_account * update jwt claim headers * add mock get_google_cloud_serverless_headers for opa test * swap issuer and audience * add comment * change default port in authz
This commit is contained in:
parent
2dc0be2ec9
commit
7d236ca1af
17 changed files with 492 additions and 675 deletions
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||||
|
@ -20,22 +21,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *Authorize) okResponse(reply *evaluator.Result) *envoy_service_auth_v2.CheckResponse {
|
func (a *Authorize) okResponse(reply *evaluator.Result) *envoy_service_auth_v2.CheckResponse {
|
||||||
requestHeaders, err := a.getEnvoyRequestHeaders(reply.SignedJWT)
|
var requestHeaders []*envoy_api_v2_core.HeaderValueOption
|
||||||
if err != nil {
|
for k, v := range reply.Headers {
|
||||||
log.Warn().Err(err).Msg("authorize: error generating new request headers")
|
requestHeaders = append(requestHeaders, mkHeader(k, v, false))
|
||||||
}
|
}
|
||||||
|
// ensure request headers are sorted by key for deterministic output
|
||||||
requestHeaders = append(requestHeaders,
|
sort.Slice(requestHeaders, func(i, j int) bool {
|
||||||
mkHeader(httputil.HeaderPomeriumJWTAssertion, reply.SignedJWT, false))
|
return requestHeaders[i].Header.Key < requestHeaders[j].Header.Value
|
||||||
|
})
|
||||||
requestHeaders = append(requestHeaders, getKubernetesHeaders(reply)...)
|
|
||||||
|
|
||||||
if hdrs, err := a.getGoogleCloudServerlessAuthenticationHeaders(reply); err == nil {
|
|
||||||
requestHeaders = append(requestHeaders, hdrs...)
|
|
||||||
} else {
|
|
||||||
log.Warn().Err(err).Msg("error getting google cloud serverless authentication headers")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &envoy_service_auth_v2.CheckResponse{
|
return &envoy_service_auth_v2.CheckResponse{
|
||||||
Status: &status.Status{Code: int32(codes.OK), Message: reply.Message},
|
Status: &status.Status{Code: int32(codes.OK), Message: reply.Message},
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
||||||
|
@ -181,22 +174,6 @@ func (a *Authorize) redirectResponse(in *envoy_service_auth_v2.CheckRequest) (*e
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKubernetesHeaders(reply *evaluator.Result) []*envoy_api_v2_core.HeaderValueOption {
|
|
||||||
var requestHeaders []*envoy_api_v2_core.HeaderValueOption
|
|
||||||
if reply.MatchingPolicy != nil && (reply.MatchingPolicy.KubernetesServiceAccountTokenFile != "" || reply.MatchingPolicy.KubernetesServiceAccountToken != "") {
|
|
||||||
requestHeaders = append(requestHeaders,
|
|
||||||
mkHeader("Authorization", "Bearer "+reply.MatchingPolicy.KubernetesServiceAccountToken, false))
|
|
||||||
|
|
||||||
if reply.UserEmail != "" {
|
|
||||||
requestHeaders = append(requestHeaders, mkHeader("Impersonate-User", reply.UserEmail, false))
|
|
||||||
}
|
|
||||||
for i, group := range reply.UserGroups {
|
|
||||||
requestHeaders = append(requestHeaders, mkHeader("Impersonate-Group", group, i > 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return requestHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
func mkHeader(k, v string, shouldAppend bool) *envoy_api_v2_core.HeaderValueOption {
|
func mkHeader(k, v string, shouldAppend bool) *envoy_api_v2_core.HeaderValueOption {
|
||||||
return &envoy_api_v2_core.HeaderValueOption{
|
return &envoy_api_v2_core.HeaderValueOption{
|
||||||
Header: &envoy_api_v2_core.HeaderValue{
|
Header: &envoy_api_v2_core.HeaderValue{
|
||||||
|
|
|
@ -3,10 +3,8 @@ package authorize
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||||
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
||||||
|
@ -55,29 +53,6 @@ func TestAuthorize_okResponse(t *testing.T) {
|
||||||
pe, err := newPolicyEvaluator(opt, a.store)
|
pe, err := newPolicyEvaluator(opt, a.store)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
a.state.Load().evaluator = pe
|
a.state.Load().evaluator = pe
|
||||||
validJWT, _ := pe.SignedJWT(pe.JWTPayload(&evaluator.Request{
|
|
||||||
HTTP: evaluator.RequestHTTP{URL: "https://example.com"},
|
|
||||||
Session: evaluator.RequestSession{
|
|
||||||
ID: "SESSION_ID",
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
originalGCPIdentityDocURL := gcpIdentityDocURL
|
|
||||||
defer func() {
|
|
||||||
gcpIdentityDocURL = originalGCPIdentityDocURL
|
|
||||||
gcpIdentityNow = time.Now
|
|
||||||
}()
|
|
||||||
|
|
||||||
now := time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)
|
|
||||||
gcpIdentityNow = func() time.Time {
|
|
||||||
return now
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_, _ = w.Write([]byte(now.Format(time.RFC3339)))
|
|
||||||
}))
|
|
||||||
defer srv.Close()
|
|
||||||
gcpIdentityDocURL = srv.URL
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -86,107 +61,45 @@ func TestAuthorize_okResponse(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"ok reply",
|
"ok reply",
|
||||||
&evaluator.Result{Status: 0, Message: "ok", SignedJWT: "valid-signed-jwt"},
|
&evaluator.Result{Status: 0, Message: "ok"},
|
||||||
&envoy_service_auth_v2.CheckResponse{
|
&envoy_service_auth_v2.CheckResponse{
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
Status: &status.Status{Code: 0, Message: "ok"},
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
|
||||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
|
||||||
Headers: []*envoy_api_v2_core.HeaderValueOption{
|
|
||||||
mkHeader("x-pomerium-jwt-assertion", "valid-signed-jwt", false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ok reply with k8s svc",
|
"ok reply with k8s svc",
|
||||||
&evaluator.Result{
|
&evaluator.Result{
|
||||||
Status: 0,
|
Status: 0,
|
||||||
Message: "ok",
|
Message: "ok",
|
||||||
SignedJWT: "valid-signed-jwt",
|
|
||||||
MatchingPolicy: &config.Policy{
|
MatchingPolicy: &config.Policy{
|
||||||
KubernetesServiceAccountToken: "k8s-svc-account",
|
KubernetesServiceAccountToken: "k8s-svc-account",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&envoy_service_auth_v2.CheckResponse{
|
&envoy_service_auth_v2.CheckResponse{
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
Status: &status.Status{Code: 0, Message: "ok"},
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
|
||||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
|
||||||
Headers: []*envoy_api_v2_core.HeaderValueOption{
|
|
||||||
mkHeader("x-pomerium-jwt-assertion", "valid-signed-jwt", false),
|
|
||||||
mkHeader("Authorization", "Bearer k8s-svc-account", false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ok reply with k8s svc impersonate",
|
"ok reply with k8s svc impersonate",
|
||||||
&evaluator.Result{
|
&evaluator.Result{
|
||||||
Status: 0,
|
Status: 0,
|
||||||
Message: "ok",
|
Message: "ok",
|
||||||
SignedJWT: "valid-signed-jwt",
|
|
||||||
MatchingPolicy: &config.Policy{
|
MatchingPolicy: &config.Policy{
|
||||||
KubernetesServiceAccountToken: "k8s-svc-account",
|
KubernetesServiceAccountToken: "k8s-svc-account",
|
||||||
},
|
},
|
||||||
UserEmail: "foo@example.com",
|
|
||||||
UserGroups: []string{"admin", "test"},
|
|
||||||
},
|
},
|
||||||
&envoy_service_auth_v2.CheckResponse{
|
&envoy_service_auth_v2.CheckResponse{
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
Status: &status.Status{Code: 0, Message: "ok"},
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
|
||||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
|
||||||
Headers: []*envoy_api_v2_core.HeaderValueOption{
|
|
||||||
mkHeader("x-pomerium-jwt-assertion", "valid-signed-jwt", false),
|
|
||||||
mkHeader("Authorization", "Bearer k8s-svc-account", false),
|
|
||||||
mkHeader("Impersonate-User", "foo@example.com", false),
|
|
||||||
mkHeader("Impersonate-Group", "admin", false),
|
|
||||||
mkHeader("Impersonate-Group", "test", true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ok reply with google cloud serverless",
|
|
||||||
&evaluator.Result{
|
|
||||||
Status: 0,
|
|
||||||
Message: "ok",
|
|
||||||
SignedJWT: "valid-signed-jwt",
|
|
||||||
MatchingPolicy: &config.Policy{
|
|
||||||
EnableGoogleCloudServerlessAuthentication: true,
|
|
||||||
To: mustParseWeightedURLs(t, "https://example.com"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&envoy_service_auth_v2.CheckResponse{
|
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
|
||||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
|
||||||
Headers: []*envoy_api_v2_core.HeaderValueOption{
|
|
||||||
mkHeader("x-pomerium-jwt-assertion", "valid-signed-jwt", false),
|
|
||||||
mkHeader("Authorization", "Bearer 2020-01-01T01:00:00Z", false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ok reply with jwt claims header",
|
"ok reply with jwt claims header",
|
||||||
&evaluator.Result{
|
&evaluator.Result{
|
||||||
Status: 0,
|
Status: 0,
|
||||||
Message: "ok",
|
Message: "ok",
|
||||||
SignedJWT: validJWT,
|
|
||||||
},
|
},
|
||||||
&envoy_service_auth_v2.CheckResponse{
|
&envoy_service_auth_v2.CheckResponse{
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
Status: &status.Status{Code: 0, Message: "ok"},
|
||||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
|
||||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
|
||||||
Headers: []*envoy_api_v2_core.HeaderValueOption{
|
|
||||||
mkHeader("x-pomerium-claim-email", "foo@example.com", false),
|
|
||||||
mkHeader("x-pomerium-jwt-assertion", validJWT, false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ type CustomEvaluatorResponse struct {
|
||||||
Allowed bool
|
Allowed bool
|
||||||
Denied bool
|
Denied bool
|
||||||
Reason string
|
Reason string
|
||||||
|
Headers map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// A CustomEvaluator evaluates custom rego policies.
|
// A CustomEvaluator evaluates custom rego policies.
|
||||||
|
@ -65,7 +66,9 @@ func (ce *CustomEvaluator) Evaluate(ctx context.Context, req *CustomEvaluatorReq
|
||||||
vars = make(map[string]interface{})
|
vars = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &CustomEvaluatorResponse{}
|
res := &CustomEvaluatorResponse{
|
||||||
|
Headers: getHeadersVar(resultSet[0].Bindings.WithoutWildcards()),
|
||||||
|
}
|
||||||
res.Allowed, _ = vars["allow"].(bool)
|
res.Allowed, _ = vars["allow"].(bool)
|
||||||
if v, ok := vars["deny"]; ok {
|
if v, ok := vars["deny"]; ok {
|
||||||
// support `deny = true`
|
// support `deny = true`
|
||||||
|
|
|
@ -5,8 +5,6 @@ package evaluator
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -26,22 +24,16 @@ type Evaluator struct {
|
||||||
query rego.PreparedEvalQuery
|
query rego.PreparedEvalQuery
|
||||||
policies []config.Policy
|
policies []config.Policy
|
||||||
store *Store
|
store *Store
|
||||||
|
|
||||||
authenticateHost string
|
|
||||||
jwk *jose.JSONWebKey
|
|
||||||
signer jose.Signer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Evaluator.
|
// New creates a new Evaluator.
|
||||||
func New(options *config.Options, store *Store) (*Evaluator, error) {
|
func New(options *config.Options, store *Store) (*Evaluator, error) {
|
||||||
e := &Evaluator{
|
e := &Evaluator{
|
||||||
custom: NewCustomEvaluator(store.opaStore),
|
custom: NewCustomEvaluator(store.opaStore),
|
||||||
authenticateHost: options.AuthenticateURL.Host,
|
policies: options.GetAllPolicies(),
|
||||||
policies: options.GetAllPolicies(),
|
store: store,
|
||||||
store: store,
|
|
||||||
}
|
}
|
||||||
var err error
|
jwk, err := getJWK(options)
|
||||||
e.signer, e.jwk, err = newSigner(options)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("authorize: couldn't create signer: %w", err)
|
return nil, fmt.Errorf("authorize: couldn't create signer: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -51,12 +43,17 @@ func New(options *config.Options, store *Store) (*Evaluator, error) {
|
||||||
return nil, fmt.Errorf("error loading rego policy: %w", err)
|
return nil, fmt.Errorf("error loading rego policy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.UpdateIssuer(options.AuthenticateURL.Host)
|
||||||
|
store.UpdateGoogleCloudServerlessAuthenticationServiceAccount(options.GoogleCloudServerlessAuthenticationServiceAccount)
|
||||||
|
store.UpdateJWTClaimHeaders(options.JWTClaimsHeaders)
|
||||||
store.UpdateRoutePolicies(options.GetAllPolicies())
|
store.UpdateRoutePolicies(options.GetAllPolicies())
|
||||||
|
store.UpdateSigningKey(jwk)
|
||||||
|
|
||||||
e.rego = rego.New(
|
e.rego = rego.New(
|
||||||
rego.Store(store.opaStore),
|
rego.Store(store.opaStore),
|
||||||
rego.Module("pomerium.authz", string(authzPolicy)),
|
rego.Module("pomerium.authz", string(authzPolicy)),
|
||||||
rego.Query("result = data.pomerium.authz"),
|
rego.Query("result = data.pomerium.authz"),
|
||||||
|
getGoogleCloudServerlessHeadersRegoOption,
|
||||||
)
|
)
|
||||||
|
|
||||||
e.query, err = e.rego.PrepareForEval(context.Background())
|
e.query, err = e.rego.PrepareForEval(context.Background())
|
||||||
|
@ -84,25 +81,12 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error)
|
||||||
return &deny[0], nil
|
return &deny[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := e.JWTPayload(req)
|
|
||||||
|
|
||||||
signedJWT, err := e.SignedJWT(payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error signing JWT: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
evalResult := &Result{
|
evalResult := &Result{
|
||||||
MatchingPolicy: getMatchingPolicy(res[0].Bindings.WithoutWildcards(), e.policies),
|
MatchingPolicy: getMatchingPolicy(res[0].Bindings.WithoutWildcards(), e.policies),
|
||||||
SignedJWT: signedJWT,
|
Headers: getHeadersVar(res[0].Bindings.WithoutWildcards()),
|
||||||
}
|
|
||||||
if e, ok := payload["email"].(string); ok {
|
|
||||||
evalResult.UserEmail = e
|
|
||||||
}
|
|
||||||
if gs, ok := payload["groups"].([]string); ok {
|
|
||||||
evalResult.UserGroups = gs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allow := allowed(res[0].Bindings.WithoutWildcards())
|
allow := getAllowVar(res[0].Bindings.WithoutWildcards())
|
||||||
// evaluate any custom policies
|
// evaluate any custom policies
|
||||||
if allow {
|
if allow {
|
||||||
for _, src := range req.CustomPolicies {
|
for _, src := range req.CustomPolicies {
|
||||||
|
@ -118,6 +102,9 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error)
|
||||||
if cres.Reason != "" {
|
if cres.Reason != "" {
|
||||||
evalResult.Message = cres.Reason
|
evalResult.Message = cres.Reason
|
||||||
}
|
}
|
||||||
|
for k, v := range cres.Headers {
|
||||||
|
evalResult.Headers[k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allow {
|
if allow {
|
||||||
|
@ -139,41 +126,23 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error)
|
||||||
return evalResult, nil
|
return evalResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseSignedJWT parses the input signature and return its payload.
|
func getJWK(options *config.Options) (*jose.JSONWebKey, error) {
|
||||||
func (e *Evaluator) ParseSignedJWT(signature string) ([]byte, error) {
|
|
||||||
object, err := jose.ParseSigned(signature)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return object.Verify(e.jwk.Public())
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWTPayload returns the JWT payload for a request.
|
|
||||||
func (e *Evaluator) JWTPayload(req *Request) map[string]interface{} {
|
|
||||||
payload := map[string]interface{}{
|
|
||||||
"iss": e.authenticateHost,
|
|
||||||
}
|
|
||||||
req.fillJWTPayload(e.store, payload)
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSigner(options *config.Options) (jose.Signer, *jose.JSONWebKey, error) {
|
|
||||||
var decodedCert []byte
|
var decodedCert []byte
|
||||||
// if we don't have a signing key, generate one
|
// if we don't have a signing key, generate one
|
||||||
if options.SigningKey == "" {
|
if options.SigningKey == "" {
|
||||||
key, err := cryptutil.NewSigningKey()
|
key, err := cryptutil.NewSigningKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
return nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
||||||
}
|
}
|
||||||
decodedCert, err = cryptutil.EncodePrivateKey(key)
|
decodedCert, err = cryptutil.EncodePrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("bad signing key: %w", err)
|
return nil, fmt.Errorf("bad signing key: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
decodedCert, err = base64.StdEncoding.DecodeString(options.SigningKey)
|
decodedCert, err = base64.StdEncoding.DecodeString(options.SigningKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("bad signing key: %w", err)
|
return nil, fmt.Errorf("bad signing key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
signingKeyAlgorithm := options.SigningKeyAlgorithm
|
signingKeyAlgorithm := options.SigningKeyAlgorithm
|
||||||
|
@ -183,41 +152,14 @@ func newSigner(options *config.Options) (jose.Signer, *jose.JSONWebKey, error) {
|
||||||
|
|
||||||
jwk, err := cryptutil.PrivateJWKFromBytes(decodedCert, jose.SignatureAlgorithm(signingKeyAlgorithm))
|
jwk, err := cryptutil.PrivateJWKFromBytes(decodedCert, jose.SignatureAlgorithm(signingKeyAlgorithm))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
return nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
||||||
}
|
}
|
||||||
log.Info().Str("Algorithm", jwk.Algorithm).
|
log.Info().Str("Algorithm", jwk.Algorithm).
|
||||||
Str("KeyID", jwk.KeyID).
|
Str("KeyID", jwk.KeyID).
|
||||||
Interface("Public Key", jwk.Public()).
|
Interface("Public Key", jwk.Public()).
|
||||||
Msg("authorize: signing key")
|
Msg("authorize: signing key")
|
||||||
|
|
||||||
signerOpt := &jose.SignerOptions{}
|
return jwk, nil
|
||||||
signer, err := jose.NewSigner(jose.SigningKey{
|
|
||||||
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
|
||||||
Key: jwk,
|
|
||||||
}, signerOpt.WithHeader("kid", jwk.KeyID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("couldn't create signer: %w", err)
|
|
||||||
}
|
|
||||||
return signer, jwk, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignedJWT returns the signature of given request.
|
|
||||||
func (e *Evaluator) SignedJWT(payload map[string]interface{}) (string, error) {
|
|
||||||
if e.signer == nil {
|
|
||||||
return "", errors.New("evaluator: signer cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
bs, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
jws, err := e.signer.Sign(bs)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return jws.CompactSerialize()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type input struct {
|
type input struct {
|
||||||
|
@ -238,11 +180,8 @@ func (e *Evaluator) newInput(req *Request, isValidClientCertificate bool) *input
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Status int
|
Status int
|
||||||
Message string
|
Message string
|
||||||
SignedJWT string
|
Headers map[string]string
|
||||||
MatchingPolicy *config.Policy
|
MatchingPolicy *config.Policy
|
||||||
|
|
||||||
UserEmail string
|
|
||||||
UserGroups []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMatchingPolicy(vars rego.Vars, policies []config.Policy) *config.Policy {
|
func getMatchingPolicy(vars rego.Vars, policies []config.Policy) *config.Policy {
|
||||||
|
@ -263,7 +202,7 @@ func getMatchingPolicy(vars rego.Vars, policies []config.Policy) *config.Policy
|
||||||
return &policies[idx]
|
return &policies[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
func allowed(vars rego.Vars) bool {
|
func getAllowVar(vars rego.Vars) bool {
|
||||||
result, ok := vars["result"].(map[string]interface{})
|
result, ok := vars["result"].(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
|
@ -308,3 +247,23 @@ func getDenyVar(vars rego.Vars) []Result {
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getHeadersVar(vars rego.Vars) map[string]string {
|
||||||
|
headers := make(map[string]string)
|
||||||
|
|
||||||
|
result, ok := vars["result"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
m, ok := result["identity_headers"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range m {
|
||||||
|
headers[k] = fmt.Sprint(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
|
@ -12,8 +12,6 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"gopkg.in/square/go-jose.v2/jwt"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||||
|
@ -73,212 +71,6 @@ func TestJSONMarshal(t *testing.T) {
|
||||||
}`, string(bs))
|
}`, string(bs))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvaluator_SignedJWT(t *testing.T) {
|
|
||||||
opt := config.NewDefaultOptions()
|
|
||||||
opt.AuthenticateURL = mustParseURL("https://authenticate.example.com")
|
|
||||||
e, err := New(opt, NewStore())
|
|
||||||
require.NoError(t, err)
|
|
||||||
req := &Request{
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
URL: "https://example.com",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
signedJWT, err := e.SignedJWT(e.JWTPayload(req))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, signedJWT)
|
|
||||||
|
|
||||||
payload, err := e.ParseSignedJWT(signedJWT)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvaluator_JWTWithKID(t *testing.T) {
|
|
||||||
opt := config.NewDefaultOptions()
|
|
||||||
opt.AuthenticateURL = mustParseURL("https://authenticate.example.com")
|
|
||||||
opt.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUpCMFZkbko1VjEvbVlpYUlIWHhnd2Q0Yzd5YWRTeXMxb3Y0bzA1b0F3ekdvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVUc1eENQMEpUVDFINklvbDhqS3VUSVBWTE0wNENnVzlQbEV5cE5SbVdsb29LRVhSOUhUMwpPYnp6aktZaWN6YjArMUt3VjJmTVRFMTh1dy82MXJVQ0JBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
|
|
||||||
e, err := New(opt, NewStore())
|
|
||||||
require.NoError(t, err)
|
|
||||||
req := &Request{
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
URL: "https://example.com",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
signedJWT, err := e.SignedJWT(e.JWTPayload(req))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, signedJWT)
|
|
||||||
|
|
||||||
tok, err := jwt.ParseSigned(signedJWT)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, tok.Headers, 1)
|
|
||||||
assert.Equal(t, "5b419ade1895fec2d2def6cd33b1b9a018df60db231dc5ecb85cbed6d942813c", tok.Headers[0].KeyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvaluator_JWTPayload(t *testing.T) {
|
|
||||||
nowPb := ptypes.TimestampNow()
|
|
||||||
now, _ := ptypes.Timestamp(nowPb)
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
store *Store
|
|
||||||
req *Request
|
|
||||||
want map[string]interface{}
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"iss and aud",
|
|
||||||
NewStore(),
|
|
||||||
&Request{
|
|
||||||
HTTP: RequestHTTP{URL: "https://example.com"},
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"iss": "authn.example.com",
|
|
||||||
"aud": "example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"with session",
|
|
||||||
NewStoreFromProtos(&session.Session{
|
|
||||||
Id: "SESSION_ID",
|
|
||||||
IdToken: &session.IDToken{
|
|
||||||
ExpiresAt: nowPb,
|
|
||||||
IssuedAt: nowPb,
|
|
||||||
},
|
|
||||||
ExpiresAt: nowPb,
|
|
||||||
}),
|
|
||||||
&Request{
|
|
||||||
HTTP: RequestHTTP{URL: "https://example.com"},
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "SESSION_ID",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"iss": "authn.example.com",
|
|
||||||
"jti": "SESSION_ID",
|
|
||||||
"aud": "example.com",
|
|
||||||
"exp": now.Unix(),
|
|
||||||
"iat": now.Unix(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"with service account",
|
|
||||||
NewStoreFromProtos(&user.ServiceAccount{
|
|
||||||
Id: "SERVICE_ACCOUNT_ID",
|
|
||||||
IssuedAt: nowPb,
|
|
||||||
ExpiresAt: nowPb,
|
|
||||||
}),
|
|
||||||
&Request{
|
|
||||||
HTTP: RequestHTTP{URL: "https://example.com"},
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "SERVICE_ACCOUNT_ID",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"iss": "authn.example.com",
|
|
||||||
"jti": "SERVICE_ACCOUNT_ID",
|
|
||||||
"aud": "example.com",
|
|
||||||
"exp": now.Unix(),
|
|
||||||
"iat": now.Unix(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"with user",
|
|
||||||
NewStoreFromProtos(&session.Session{
|
|
||||||
Id: "SESSION_ID",
|
|
||||||
UserId: "USER_ID",
|
|
||||||
}, &user.User{
|
|
||||||
Id: "USER_ID",
|
|
||||||
Name: "foo",
|
|
||||||
Email: "foo@example.com",
|
|
||||||
}),
|
|
||||||
&Request{
|
|
||||||
HTTP: RequestHTTP{URL: "https://example.com"},
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "SESSION_ID",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"iss": "authn.example.com",
|
|
||||||
"jti": "SESSION_ID",
|
|
||||||
"aud": "example.com",
|
|
||||||
"sub": "USER_ID",
|
|
||||||
"user": "USER_ID",
|
|
||||||
"email": "foo@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"with directory user",
|
|
||||||
NewStoreFromProtos(
|
|
||||||
&session.Session{
|
|
||||||
Id: "SESSION_ID",
|
|
||||||
UserId: "USER_ID",
|
|
||||||
},
|
|
||||||
&directory.User{
|
|
||||||
Id: "USER_ID",
|
|
||||||
GroupIds: []string{"group1", "group2"},
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group1",
|
|
||||||
Name: "admin",
|
|
||||||
Email: "admin@example.com",
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group2",
|
|
||||||
Name: "test",
|
|
||||||
Email: "test@example.com",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
&Request{
|
|
||||||
HTTP: RequestHTTP{URL: "https://example.com"},
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "SESSION_ID",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"iss": "authn.example.com",
|
|
||||||
"jti": "SESSION_ID",
|
|
||||||
"aud": "example.com",
|
|
||||||
"groups": []string{"group1", "group2", "admin", "test"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"with impersonate",
|
|
||||||
NewStoreFromProtos(
|
|
||||||
&session.Session{
|
|
||||||
Id: "SESSION_ID",
|
|
||||||
UserId: "USER_ID",
|
|
||||||
ImpersonateEmail: proto.String("user@example.com"),
|
|
||||||
ImpersonateGroups: []string{"admin", "test"},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
&Request{
|
|
||||||
HTTP: RequestHTTP{URL: "https://example.com"},
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "SESSION_ID",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"iss": "authn.example.com",
|
|
||||||
"jti": "SESSION_ID",
|
|
||||||
"aud": "example.com",
|
|
||||||
"email": "user@example.com",
|
|
||||||
"groups": []string{"admin", "test"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
e, err := New(&config.Options{
|
|
||||||
AuthenticateURL: mustParseURL("https://authn.example.com"),
|
|
||||||
}, tc.store)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.want, e.JWTPayload(tc.req))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvaluator_Evaluate(t *testing.T) {
|
func TestEvaluator_Evaluate(t *testing.T) {
|
||||||
sessionID := uuid.New().String()
|
sessionID := uuid.New().String()
|
||||||
userID := uuid.New().String()
|
userID := uuid.New().String()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package authorize
|
package evaluator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -12,19 +12,49 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
"github.com/open-policy-agent/opa/rego"
|
||||||
|
"github.com/open-policy-agent/opa/types"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
"google.golang.org/api/idtoken"
|
"google.golang.org/api/idtoken"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GCP pre-defined values.
|
||||||
var (
|
var (
|
||||||
gpcIdentityTokenExpiration = time.Minute * 45 // tokens expire after one hour according to the GCP docs
|
GCPIdentityTokenExpiration = time.Minute * 45 // tokens expire after one hour according to the GCP docs
|
||||||
gcpIdentityDocURL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
|
GCPIdentityDocURL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
|
||||||
gcpIdentityNow = time.Now
|
GCPIdentityNow = time.Now
|
||||||
gcpIdentityMaxBodySize int64 = 1024 * 1024 * 10
|
GCPIdentityMaxBodySize int64 = 1024 * 1024 * 10
|
||||||
|
|
||||||
|
getGoogleCloudServerlessHeadersRegoOption = rego.Function2(®o.Function{
|
||||||
|
Name: "get_google_cloud_serverless_headers",
|
||||||
|
Decl: types.NewFunction(
|
||||||
|
types.Args(types.S, types.S),
|
||||||
|
types.NewObject(nil, types.NewDynamicProperty(types.S, types.S)),
|
||||||
|
),
|
||||||
|
}, func(bctx rego.BuiltinContext, op1 *ast.Term, op2 *ast.Term) (*ast.Term, error) {
|
||||||
|
serviceAccount, ok := op1.Value.(ast.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid service account type: %T", op1)
|
||||||
|
}
|
||||||
|
|
||||||
|
audience, ok := op2.Value.(ast.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid audience type: %T", op2)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers, err := getGoogleCloudServerlessHeaders(string(serviceAccount), string(audience))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get google cloud serverless headers: %w", err)
|
||||||
|
}
|
||||||
|
var kvs [][2]*ast.Term
|
||||||
|
for k, v := range headers {
|
||||||
|
kvs = append(kvs, [2]*ast.Term{ast.StringTerm(k), ast.StringTerm(v)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.ObjectTerm(kvs...), nil
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
type gcpIdentityTokenSource struct {
|
type gcpIdentityTokenSource struct {
|
||||||
|
@ -34,7 +64,7 @@ type gcpIdentityTokenSource struct {
|
||||||
|
|
||||||
func (src *gcpIdentityTokenSource) Token() (*oauth2.Token, error) {
|
func (src *gcpIdentityTokenSource) Token() (*oauth2.Token, error) {
|
||||||
res, err, _ := src.singleflight.Do("", func() (interface{}, error) {
|
res, err, _ := src.singleflight.Do("", func() (interface{}, error) {
|
||||||
req, err := http.NewRequestWithContext(context.Background(), "GET", gcpIdentityDocURL+"?"+url.Values{
|
req, err := http.NewRequestWithContext(context.Background(), "GET", GCPIdentityDocURL+"?"+url.Values{
|
||||||
"format": {"full"},
|
"format": {"full"},
|
||||||
"audience": {src.audience},
|
"audience": {src.audience},
|
||||||
}.Encode(), nil)
|
}.Encode(), nil)
|
||||||
|
@ -49,7 +79,7 @@ func (src *gcpIdentityTokenSource) Token() (*oauth2.Token, error) {
|
||||||
}
|
}
|
||||||
defer func() { _ = res.Body.Close() }()
|
defer func() { _ = res.Body.Close() }()
|
||||||
|
|
||||||
bs, err := ioutil.ReadAll(io.LimitReader(res.Body, gcpIdentityMaxBodySize))
|
bs, err := ioutil.ReadAll(io.LimitReader(res.Body, GCPIdentityMaxBodySize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -62,7 +92,7 @@ func (src *gcpIdentityTokenSource) Token() (*oauth2.Token, error) {
|
||||||
return &oauth2.Token{
|
return &oauth2.Token{
|
||||||
AccessToken: strings.TrimSpace(res.(string)),
|
AccessToken: strings.TrimSpace(res.(string)),
|
||||||
TokenType: "bearer",
|
TokenType: "bearer",
|
||||||
Expiry: gcpIdentityNow().Add(gpcIdentityTokenExpiration),
|
Expiry: GCPIdentityNow().Add(GCPIdentityTokenExpiration),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,18 +157,7 @@ func getGoogleCloudServerlessTokenSource(serviceAccount, audience string) (oauth
|
||||||
return src, nil
|
return src, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) getGoogleCloudServerlessAuthenticationHeaders(reply *evaluator.Result) ([]*envoy_api_v2_core.HeaderValueOption, error) {
|
func getGoogleCloudServerlessHeaders(serviceAccount, audience string) (map[string]string, error) {
|
||||||
if reply.MatchingPolicy == nil || !reply.MatchingPolicy.EnableGoogleCloudServerlessAuthentication {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceAccount := a.currentOptions.Load().GoogleCloudServerlessAuthenticationServiceAccount
|
|
||||||
var hostname string
|
|
||||||
if len(reply.MatchingPolicy.To) > 0 {
|
|
||||||
hostname = reply.MatchingPolicy.To[0].URL.Hostname()
|
|
||||||
}
|
|
||||||
audience := fmt.Sprintf("https://%s", hostname)
|
|
||||||
|
|
||||||
src, err := getGoogleCloudServerlessTokenSource(serviceAccount, audience)
|
src, err := getGoogleCloudServerlessTokenSource(serviceAccount, audience)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -149,7 +168,7 @@ func (a *Authorize) getGoogleCloudServerlessAuthenticationHeaders(reply *evaluat
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return []*envoy_api_v2_core.HeaderValueOption{
|
return map[string]string{
|
||||||
mkHeader("Authorization", "Bearer "+tok.AccessToken, false),
|
"Authorization": "Bearer " + tok.AccessToken,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package authorize
|
package evaluator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -9,34 +9,38 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGCPIdentityTokenSource(t *testing.T) {
|
func withMockGCP(t *testing.T, f func()) {
|
||||||
originalGCPIdentityDocURL := gcpIdentityDocURL
|
originalGCPIdentityDocURL := GCPIdentityDocURL
|
||||||
defer func() {
|
defer func() {
|
||||||
gcpIdentityDocURL = originalGCPIdentityDocURL
|
GCPIdentityDocURL = originalGCPIdentityDocURL
|
||||||
gcpIdentityNow = time.Now
|
GCPIdentityNow = time.Now
|
||||||
}()
|
}()
|
||||||
|
|
||||||
now := time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)
|
now := time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)
|
||||||
gcpIdentityNow = func() time.Time {
|
GCPIdentityNow = func() time.Time {
|
||||||
return now
|
return now
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
assert.Equal(t, "Google", r.Header.Get("Metadata-Flavor"))
|
assert.Equal(t, "Google", r.Header.Get("Metadata-Flavor"))
|
||||||
assert.Equal(t, "full", r.URL.Query().Get("format"))
|
assert.Equal(t, "full", r.URL.Query().Get("format"))
|
||||||
assert.Equal(t, "example", r.URL.Query().Get("audience"))
|
|
||||||
_, _ = w.Write([]byte(now.Format(time.RFC3339)))
|
_, _ = w.Write([]byte(now.Format(time.RFC3339)))
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
gcpIdentityDocURL = srv.URL
|
GCPIdentityDocURL = srv.URL
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
src, err := getGoogleCloudServerlessTokenSource("", "example")
|
func TestGCPIdentityTokenSource(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
withMockGCP(t, func() {
|
||||||
|
src, err := getGoogleCloudServerlessTokenSource("", "example")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
token, err := src.Token()
|
token, err := src.Token()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "2020-01-01T01:00:00Z", token.AccessToken)
|
assert.Equal(t, "2020-01-01T01:00:00Z", token.AccessToken)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_normalizeServiceAccount(t *testing.T) {
|
func Test_normalizeServiceAccount(t *testing.T) {
|
|
@ -37,7 +37,7 @@ directory_user = du {
|
||||||
}
|
}
|
||||||
|
|
||||||
group_ids = gs {
|
group_ids = gs {
|
||||||
gs = session.impersonate_group_ids
|
gs = session.impersonate_groups
|
||||||
gs != null
|
gs != null
|
||||||
} else = gs {
|
} else = gs {
|
||||||
gs = directory_user.group_ids
|
gs = directory_user.group_ids
|
||||||
|
@ -145,6 +145,138 @@ deny[reason] {
|
||||||
not input.is_valid_client_certificate
|
not input.is_valid_client_certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jwt_headers = {
|
||||||
|
"typ": "JWT",
|
||||||
|
"alg": data.signing_key.alg,
|
||||||
|
"kid": data.signing_key.kid,
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_aud = v {
|
||||||
|
v = parse_url(input.http.url).hostname
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_iss = data.issuer
|
||||||
|
|
||||||
|
jwt_payload_jti = v {
|
||||||
|
v = session.id
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_exp = v {
|
||||||
|
v = session.expires_at.seconds
|
||||||
|
} else = null {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_iat = v {
|
||||||
|
# sessions store the issued_at on the id_token
|
||||||
|
v = session.id_token.issued_at.seconds
|
||||||
|
} else = v {
|
||||||
|
# service accounts store the issued at directly
|
||||||
|
v = session.issued_at.seconds
|
||||||
|
} else = null {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_sub = v {
|
||||||
|
v = user.id
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_user = v {
|
||||||
|
v = user.id
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_email = v {
|
||||||
|
v = session.impersonate_email
|
||||||
|
} else = v {
|
||||||
|
v = directory_user.email
|
||||||
|
} else = v {
|
||||||
|
v = user.email
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_groups = v {
|
||||||
|
v = array.concat(group_ids, get_databroker_group_names(group_ids))
|
||||||
|
} else = [] {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_claims := [
|
||||||
|
["iss", jwt_payload_iss],
|
||||||
|
["aud", jwt_payload_aud],
|
||||||
|
["jti", jwt_payload_jti],
|
||||||
|
["exp", jwt_payload_exp],
|
||||||
|
["iat", jwt_payload_iat],
|
||||||
|
["sub", jwt_payload_sub],
|
||||||
|
["user", jwt_payload_user],
|
||||||
|
["email", jwt_payload_email],
|
||||||
|
["groups", jwt_payload_groups],
|
||||||
|
]
|
||||||
|
|
||||||
|
jwt_payload = {key: value |
|
||||||
|
# use a comprehension over an array to remove nil values
|
||||||
|
[key, value] := jwt_claims[_]
|
||||||
|
value != null
|
||||||
|
}
|
||||||
|
|
||||||
|
signed_jwt = io.jwt.encode_sign(jwt_headers, jwt_payload, data.signing_key)
|
||||||
|
|
||||||
|
kubernetes_headers = h {
|
||||||
|
route_policy.KubernetesServiceAccountToken != ""
|
||||||
|
h := [
|
||||||
|
["Authorization", concat(" ", ["Bearer", route_policy.KubernetesServiceAccountToken])],
|
||||||
|
["Impersonate-User", jwt_payload_email],
|
||||||
|
["Impersonate-Group", get_header_string_value(jwt_payload_groups)],
|
||||||
|
]
|
||||||
|
} else = [] {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
google_cloud_serverless_authentication_service_account = s {
|
||||||
|
s := data.google_cloud_serverless_authentication_service_account
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
google_cloud_serverless_headers = h {
|
||||||
|
route_policy.EnableGoogleCloudServerlessAuthentication
|
||||||
|
[hostname, _] := parse_host_port(route_policy.To[0].URL.Host)
|
||||||
|
audience := concat("", ["https://", hostname])
|
||||||
|
h := get_google_cloud_serverless_headers(google_cloud_serverless_authentication_service_account, audience)
|
||||||
|
} else = {} {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
identity_headers := {key: value |
|
||||||
|
h1 := [["x-pomerium-jwt-assertion", signed_jwt]]
|
||||||
|
h2 := [[k, v] |
|
||||||
|
[claim_key, claim_value] := jwt_claims[_]
|
||||||
|
claim_value != null
|
||||||
|
|
||||||
|
# only include those headers requested by the user
|
||||||
|
available := data.jwt_claim_headers[_]
|
||||||
|
available == claim_key
|
||||||
|
|
||||||
|
# create the header key and value
|
||||||
|
k := concat("", ["x-pomerium-claim-", claim_key])
|
||||||
|
v := get_header_string_value(claim_value)
|
||||||
|
]
|
||||||
|
|
||||||
|
h3 := kubernetes_headers
|
||||||
|
h4 := [[k, v] | v := google_cloud_serverless_headers[k]]
|
||||||
|
|
||||||
|
h := array.concat(array.concat(array.concat(h1, h2), h3), h4)
|
||||||
|
[key, value] := h[_]
|
||||||
|
}
|
||||||
|
|
||||||
# returns the first matching route
|
# returns the first matching route
|
||||||
first_allowed_route_policy_idx(input_url) = first_policy_idx {
|
first_allowed_route_policy_idx(input_url) = first_policy_idx {
|
||||||
first_policy_idx := [idx | some idx, policy; policy = data.route_policies[idx]; allowed_route(input.http.url, policy)][0]
|
first_policy_idx := [idx | some idx, policy; policy = data.route_policies[idx]; allowed_route(input.http.url, policy)][0]
|
||||||
|
@ -195,12 +327,20 @@ allowed_route_regex(input_url_obj, policy) {
|
||||||
re_match(policy.regex, input_url_obj.path)
|
re_match(policy.regex, input_url_obj.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_url(str) = {"scheme": scheme, "host": host, "path": path} {
|
parse_url(str) = {"scheme": scheme, "host": host, "hostname": hostname, "path": path} {
|
||||||
[_, scheme, host, rawpath] = regex.find_all_string_submatch_n(`(?:((?:tcp[+])?http[s]?)://)?([^/]+)([^?#]*)`, str, 1)[0]
|
[_, scheme, host, rawpath] = regex.find_all_string_submatch_n(`(?:((?:tcp[+])?http[s]?)://)?([^/]+)([^?#]*)`, str, 1)[0]
|
||||||
|
[hostname, _] = parse_host_port(host)
|
||||||
path = normalize_url_path(rawpath)
|
path = normalize_url_path(rawpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parse_host_port(str) = [host, port] {
|
||||||
|
contains(str, ":")
|
||||||
|
[host, port] = split(str, ":")
|
||||||
|
} else = [host, port] {
|
||||||
|
host = str
|
||||||
|
port = "443"
|
||||||
|
}
|
||||||
|
|
||||||
normalize_url_path(str) = "/" {
|
normalize_url_path(str) = "/" {
|
||||||
str == ""
|
str == ""
|
||||||
}
|
}
|
||||||
|
@ -255,6 +395,13 @@ get_databroker_group_emails(ids) = gs {
|
||||||
gs := [email | id := ids[i]; group := data.databroker_data["type.googleapis.com"]["directory.Group"][id]; email := group.email]
|
gs := [email | id := ids[i]; group := data.databroker_data["type.googleapis.com"]["directory.Group"][id]; email := group.email]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_header_string_value(obj) = s {
|
||||||
|
is_array(obj)
|
||||||
|
s := concat(",", obj)
|
||||||
|
} else = s {
|
||||||
|
s := concat(",", [obj])
|
||||||
|
}
|
||||||
|
|
||||||
# object_get is like object.get, but supports converting "/" in keys to separate lookups
|
# object_get is like object.get, but supports converting "/" in keys to separate lookups
|
||||||
# rego doesn't support recursion, so we hard code a limited number of /'s
|
# rego doesn't support recursion, so we hard code a limited number of /'s
|
||||||
object_get(obj, key, def) = value {
|
object_get(obj, key, def) = value {
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
package pomerium.authz
|
package pomerium.authz
|
||||||
|
|
||||||
|
get_google_cloud_serverless_headers(serviceAccount, audience) = h {
|
||||||
|
h := {
|
||||||
|
"Authorization": "Bearer xxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test_email_allowed {
|
test_email_allowed {
|
||||||
allow with data.route_policies as [{
|
allow with data.route_policies as [{
|
||||||
"source": "example.com",
|
"source": "example.com",
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,13 +4,18 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/open-policy-agent/opa/rego"
|
"github.com/open-policy-agent/opa/rego"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
@ -20,15 +25,28 @@ func TestOPA(t *testing.T) {
|
||||||
type A = []interface{}
|
type A = []interface{}
|
||||||
type M = map[string]interface{}
|
type M = map[string]interface{}
|
||||||
|
|
||||||
|
signingKey, err := cryptutil.NewSigningKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
encodedSigningKey, err := cryptutil.EncodePrivateKey(signingKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
privateJWK, err := cryptutil.PrivateJWKFromBytes(encodedSigningKey, jose.ES256)
|
||||||
|
require.NoError(t, err)
|
||||||
|
publicJWK, err := cryptutil.PublicJWKFromBytes(encodedSigningKey, jose.ES256)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
eval := func(policies []config.Policy, data []proto.Message, req *Request, isValidClientCertificate bool) rego.Result {
|
eval := func(policies []config.Policy, data []proto.Message, req *Request, isValidClientCertificate bool) rego.Result {
|
||||||
authzPolicy, err := readPolicy("/authz.rego")
|
authzPolicy, err := readPolicy("/authz.rego")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
store := NewStoreFromProtos(data...)
|
store := NewStoreFromProtos(data...)
|
||||||
|
store.UpdateIssuer("authenticate.example.com")
|
||||||
|
store.UpdateJWTClaimHeaders([]string{"email", "groups", "user"})
|
||||||
store.UpdateRoutePolicies(policies)
|
store.UpdateRoutePolicies(policies)
|
||||||
|
store.UpdateSigningKey(privateJWK)
|
||||||
r := rego.New(
|
r := rego.New(
|
||||||
rego.Store(store.opaStore),
|
rego.Store(store.opaStore),
|
||||||
rego.Module("pomerium.authz", string(authzPolicy)),
|
rego.Module("pomerium.authz", string(authzPolicy)),
|
||||||
rego.Query("result = data.pomerium.authz"),
|
rego.Query("result = data.pomerium.authz"),
|
||||||
|
getGoogleCloudServerlessHeadersRegoOption,
|
||||||
)
|
)
|
||||||
q, err := r.PrepareForEval(context.Background())
|
q, err := r.PrepareForEval(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -46,11 +64,168 @@ func TestOPA(t *testing.T) {
|
||||||
A{A{json.Number("495"), "invalid client certificate"}},
|
A{A{json.Number("495"), "invalid client certificate"}},
|
||||||
res.Bindings["result"].(M)["deny"])
|
res.Bindings["result"].(M)["deny"])
|
||||||
})
|
})
|
||||||
|
t.Run("identity_headers", func(t *testing.T) {
|
||||||
|
t.Run("kubernetes", func(t *testing.T) {
|
||||||
|
res := eval([]config.Policy{{
|
||||||
|
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
||||||
|
To: config.WeightedURLs{
|
||||||
|
{URL: *mustParseURL("https://to.example.com")},
|
||||||
|
},
|
||||||
|
KubernetesServiceAccountToken: "KUBERNETES",
|
||||||
|
}}, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateGroups: []string{"i1", "i2"},
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
headers := res.Bindings["result"].(M)["identity_headers"].(M)
|
||||||
|
assert.NotEmpty(t, headers["Authorization"])
|
||||||
|
assert.Equal(t, "a@example.com", headers["Impersonate-User"])
|
||||||
|
assert.Equal(t, "i1,i2", headers["Impersonate-Group"])
|
||||||
|
})
|
||||||
|
t.Run("google_cloud_serverless", func(t *testing.T) {
|
||||||
|
withMockGCP(t, func() {
|
||||||
|
res := eval([]config.Policy{{
|
||||||
|
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
||||||
|
To: config.WeightedURLs{
|
||||||
|
{URL: *mustParseURL("https://to.example.com")},
|
||||||
|
},
|
||||||
|
EnableGoogleCloudServerlessAuthentication: true,
|
||||||
|
}}, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateGroups: []string{"i1", "i2"},
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
headers := res.Bindings["result"].(M)["identity_headers"].(M)
|
||||||
|
assert.NotEmpty(t, headers["Authorization"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("jwt", func(t *testing.T) {
|
||||||
|
evalJWT := func(msgs ...proto.Message) M {
|
||||||
|
res := eval([]config.Policy{{
|
||||||
|
Source: &config.StringURL{URL: mustParseURL("https://from.example.com:8000")},
|
||||||
|
To: config.WeightedURLs{
|
||||||
|
{URL: *mustParseURL("https://to.example.com")},
|
||||||
|
},
|
||||||
|
}}, msgs, &Request{
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com:8000",
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
signedCompactJWTStr := res.Bindings["result"].(M)["signed_jwt"].(string)
|
||||||
|
authJWT, err := jwt.ParseSigned(signedCompactJWTStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var claims M
|
||||||
|
err = authJWT.Claims(publicJWK, &claims)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return claims
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("impersonate groups", func(t *testing.T) {
|
||||||
|
payload := evalJWT(
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateGroups: []string{"i1", "i2"},
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
&directory.User{
|
||||||
|
Id: "user1",
|
||||||
|
GroupIds: []string{"group1"},
|
||||||
|
},
|
||||||
|
&directory.Group{
|
||||||
|
Id: "group1",
|
||||||
|
Name: "group1name",
|
||||||
|
Email: "group1@example.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert.Equal(t, M{
|
||||||
|
"aud": "from.example.com",
|
||||||
|
"iss": "authenticate.example.com",
|
||||||
|
"jti": "session1",
|
||||||
|
"sub": "user1",
|
||||||
|
"user": "user1",
|
||||||
|
"email": "a@example.com",
|
||||||
|
"groups": []interface{}{"i1", "i2"},
|
||||||
|
}, payload)
|
||||||
|
})
|
||||||
|
t.Run("directory", func(t *testing.T) {
|
||||||
|
payload := evalJWT(
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ExpiresAt: timestamppb.New(time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)),
|
||||||
|
IdToken: &session.IDToken{
|
||||||
|
IssuedAt: timestamppb.New(time.Date(2021, 2, 1, 1, 1, 1, 1, time.UTC)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
&directory.User{
|
||||||
|
Id: "user1",
|
||||||
|
GroupIds: []string{"group1"},
|
||||||
|
},
|
||||||
|
&directory.Group{
|
||||||
|
Id: "group1",
|
||||||
|
Name: "group1name",
|
||||||
|
Email: "group1@example.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert.Equal(t, M{
|
||||||
|
"aud": "from.example.com",
|
||||||
|
"iss": "authenticate.example.com",
|
||||||
|
"jti": "session1",
|
||||||
|
"exp": 1609462861.0,
|
||||||
|
"iat": 1612141261.0,
|
||||||
|
"sub": "user1",
|
||||||
|
"user": "user1",
|
||||||
|
"email": "a@example.com",
|
||||||
|
"groups": A{"group1", "group1name"},
|
||||||
|
}, payload)
|
||||||
|
})
|
||||||
|
})
|
||||||
t.Run("email", func(t *testing.T) {
|
t.Run("email", func(t *testing.T) {
|
||||||
t.Run("allowed", func(t *testing.T) {
|
t.Run("allowed", func(t *testing.T) {
|
||||||
res := eval([]config.Policy{
|
res := eval([]config.Policy{
|
||||||
{
|
{
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
Source: &config.StringURL{URL: mustParseURL("https://from.example.com:8000")},
|
||||||
To: config.WeightedURLs{
|
To: config.WeightedURLs{
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
{URL: *mustParseURL("https://to.example.com")},
|
||||||
},
|
},
|
||||||
|
@ -71,7 +246,7 @@ func TestOPA(t *testing.T) {
|
||||||
},
|
},
|
||||||
HTTP: RequestHTTP{
|
HTTP: RequestHTTP{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: "https://from.example.com",
|
URL: "https://from.example.com:8000",
|
||||||
},
|
},
|
||||||
}, true)
|
}, true)
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
||||||
|
|
|
@ -1,15 +1,5 @@
|
||||||
package evaluator
|
package evaluator
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/directory"
|
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Request is the request data used for the evaluator.
|
// Request is the request data used for the evaluator.
|
||||||
Request struct {
|
Request struct {
|
||||||
|
@ -32,69 +22,3 @@ type (
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type sessionOrServiceAccount interface {
|
|
||||||
GetId() string
|
|
||||||
GetExpiresAt() *timestamppb.Timestamp
|
|
||||||
GetIssuedAt() *timestamppb.Timestamp
|
|
||||||
GetUserId() string
|
|
||||||
GetImpersonateEmail() string
|
|
||||||
GetImpersonateGroups() []string
|
|
||||||
GetImpersonateUserId() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (req *Request) fillJWTPayload(store *Store, payload map[string]interface{}) {
|
|
||||||
if u, err := url.Parse(req.HTTP.URL); err == nil {
|
|
||||||
payload["aud"] = u.Hostname()
|
|
||||||
}
|
|
||||||
|
|
||||||
if s, ok := store.GetRecordData("type.googleapis.com/session.Session", req.Session.ID).(*session.Session); ok {
|
|
||||||
req.fillJWTPayloadSessionOrServiceAccount(store, payload, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sa, ok := store.GetRecordData("type.googleapis.com/user.ServiceAccount", req.Session.ID).(*user.ServiceAccount); ok {
|
|
||||||
req.fillJWTPayloadSessionOrServiceAccount(store, payload, sa)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (req *Request) fillJWTPayloadSessionOrServiceAccount(store *Store, payload map[string]interface{}, s sessionOrServiceAccount) {
|
|
||||||
payload["jti"] = s.GetId()
|
|
||||||
if s.GetExpiresAt().IsValid() {
|
|
||||||
payload["exp"] = s.GetExpiresAt().AsTime().Unix()
|
|
||||||
}
|
|
||||||
if s.GetIssuedAt().IsValid() {
|
|
||||||
payload["iat"] = s.GetIssuedAt().AsTime().Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := s.GetUserId()
|
|
||||||
if s.GetImpersonateUserId() != "" {
|
|
||||||
userID = s.GetImpersonateUserId()
|
|
||||||
}
|
|
||||||
if u, ok := store.GetRecordData("type.googleapis.com/user.User", userID).(*user.User); ok {
|
|
||||||
payload["sub"] = u.GetId()
|
|
||||||
payload["user"] = u.GetId()
|
|
||||||
payload["email"] = u.GetEmail()
|
|
||||||
}
|
|
||||||
if du, ok := store.GetRecordData("type.googleapis.com/directory.User", userID).(*directory.User); ok {
|
|
||||||
if du.GetEmail() != "" {
|
|
||||||
payload["email"] = du.GetEmail()
|
|
||||||
}
|
|
||||||
var groupNames []string
|
|
||||||
for _, groupID := range du.GetGroupIds() {
|
|
||||||
if dg, ok := store.GetRecordData("type.googleapis.com/directory.Group", groupID).(*directory.Group); ok {
|
|
||||||
groupNames = append(groupNames, dg.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var groups []string
|
|
||||||
groups = append(groups, du.GetGroupIds()...)
|
|
||||||
groups = append(groups, groupNames...)
|
|
||||||
payload["groups"] = groups
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.GetImpersonateEmail() != "" {
|
|
||||||
payload["email"] = s.GetImpersonateEmail()
|
|
||||||
}
|
|
||||||
if len(s.GetImpersonateGroups()) > 0 {
|
|
||||||
payload["groups"] = s.GetImpersonateGroups()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
@ -89,6 +90,22 @@ func (s *Store) GetRecordData(typeURL, id string) proto.Message {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateIssuer updates the issuer in the store. The issuer is used as part of JWT construction.
|
||||||
|
func (s *Store) UpdateIssuer(issuer string) {
|
||||||
|
s.write("/issuer", issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateGoogleCloudServerlessAuthenticationServiceAccount updates the google cloud serverless authentication
|
||||||
|
// service account in the store.
|
||||||
|
func (s *Store) UpdateGoogleCloudServerlessAuthenticationServiceAccount(serviceAccount string) {
|
||||||
|
s.write("/google_cloud_serverless_authentication_service_account", serviceAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJWTClaimHeaders updates the jwt claim headers in the store.
|
||||||
|
func (s *Store) UpdateJWTClaimHeaders(jwtClaimHeaders []string) {
|
||||||
|
s.write("/jwt_claim_headers", jwtClaimHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateRoutePolicies updates the route policies in the store.
|
// UpdateRoutePolicies updates the route policies in the store.
|
||||||
func (s *Store) UpdateRoutePolicies(routePolicies []config.Policy) {
|
func (s *Store) UpdateRoutePolicies(routePolicies []config.Policy) {
|
||||||
s.write("/route_policies", routePolicies)
|
s.write("/route_policies", routePolicies)
|
||||||
|
@ -144,6 +161,12 @@ func (s *Store) delete(rawPath string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSigningKey updates the signing key stored in the database. Signing operations
|
||||||
|
// in rego use JWKs, so we take in that format.
|
||||||
|
func (s *Store) UpdateSigningKey(signingKey *jose.JSONWebKey) {
|
||||||
|
s.write("/signing_key", signingKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) get(rawPath string) (value interface{}) {
|
func (s *Store) get(rawPath string) (value interface{}) {
|
||||||
p, ok := storage.ParsePath(rawPath)
|
p, ok := storage.ParsePath(rawPath)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -23,7 +23,6 @@ import (
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
|
||||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
|
||||||
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,7 +57,8 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe
|
||||||
rawJWT, _ := loadRawSession(hreq, a.currentOptions.Load(), state.encoder)
|
rawJWT, _ := loadRawSession(hreq, a.currentOptions.Load(), state.encoder)
|
||||||
sessionState, _ := loadSession(state.encoder, rawJWT)
|
sessionState, _ := loadSession(state.encoder, rawJWT)
|
||||||
|
|
||||||
if err := a.forceSync(ctx, sessionState); err != nil {
|
u, err := a.forceSync(ctx, sessionState)
|
||||||
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("clearing session due to force sync failed")
|
log.Warn().Err(err).Msg("clearing session due to force sync failed")
|
||||||
sessionState = nil
|
sessionState = nil
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe
|
||||||
log.Error().Err(err).Msg("error during OPA evaluation")
|
log.Error().Err(err).Msg("error during OPA evaluation")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
logAuthorizeCheck(ctx, in, reply)
|
logAuthorizeCheck(ctx, in, reply, u)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case reply.Status == http.StatusOK:
|
case reply.Status == http.StatusOK:
|
||||||
|
@ -88,18 +88,18 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe
|
||||||
return a.deniedResponse(in, int32(reply.Status), reply.Message, nil)
|
return a.deniedResponse(in, int32(reply.Status), reply.Message, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) forceSync(ctx context.Context, ss *sessions.State) error {
|
func (a *Authorize) forceSync(ctx context.Context, ss *sessions.State) (*user.User, error) {
|
||||||
ctx, span := trace.StartSpan(ctx, "authorize.forceSync")
|
ctx, span := trace.StartSpan(ctx, "authorize.forceSync")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if ss == nil {
|
if ss == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
s := a.forceSyncSession(ctx, ss.ID)
|
s := a.forceSyncSession(ctx, ss.ID)
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return errors.New("session not found")
|
return nil, errors.New("session not found")
|
||||||
}
|
}
|
||||||
a.forceSyncUser(ctx, s.GetUserId())
|
u := a.forceSyncUser(ctx, s.GetUserId())
|
||||||
return nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) forceSyncSession(ctx context.Context, sessionID string) interface{ GetUserId() string } {
|
func (a *Authorize) forceSyncSession(ctx context.Context, sessionID string) interface{ GetUserId() string } {
|
||||||
|
@ -163,20 +163,6 @@ func (a *Authorize) forceSyncUser(ctx context.Context, userID string) *user.User
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) getEnvoyRequestHeaders(signedJWT string) ([]*envoy_api_v2_core.HeaderValueOption, error) {
|
|
||||||
var hvos []*envoy_api_v2_core.HeaderValueOption
|
|
||||||
|
|
||||||
hdrs, err := a.getJWTClaimHeaders(a.currentOptions.Load(), signedJWT)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for k, v := range hdrs {
|
|
||||||
hvos = append(hvos, mkHeader(k, v, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
return hvos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getForwardAuthURL(r *http.Request) *url.URL {
|
func getForwardAuthURL(r *http.Request) *url.URL {
|
||||||
urqQuery := r.URL.Query().Get("uri")
|
urqQuery := r.URL.Query().Get("uri")
|
||||||
u, _ := urlutil.ParseAndValidateURL(urqQuery)
|
u, _ := urlutil.ParseAndValidateURL(urqQuery)
|
||||||
|
@ -334,6 +320,7 @@ func logAuthorizeCheck(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
in *envoy_service_auth_v2.CheckRequest,
|
in *envoy_service_auth_v2.CheckRequest,
|
||||||
reply *evaluator.Result,
|
reply *evaluator.Result,
|
||||||
|
u *user.User,
|
||||||
) {
|
) {
|
||||||
hdrs := getCheckRequestHeaders(in)
|
hdrs := getCheckRequestHeaders(in)
|
||||||
hattrs := in.GetAttributes().GetRequest().GetHttp()
|
hattrs := in.GetAttributes().GetRequest().GetHttp()
|
||||||
|
@ -350,8 +337,8 @@ func logAuthorizeCheck(
|
||||||
evt = evt.Bool("allow", reply.Status == http.StatusOK)
|
evt = evt.Bool("allow", reply.Status == http.StatusOK)
|
||||||
evt = evt.Int("status", reply.Status)
|
evt = evt.Int("status", reply.Status)
|
||||||
evt = evt.Str("message", reply.Message)
|
evt = evt.Str("message", reply.Message)
|
||||||
evt = evt.Str("user", reply.UserEmail)
|
evt = evt.Str("user", u.GetId())
|
||||||
evt = evt.Strs("groups", reply.UserGroups)
|
evt = evt.Str("email", u.GetEmail())
|
||||||
}
|
}
|
||||||
|
|
||||||
// potentially sensitive, only log if debug mode
|
// potentially sensitive, only log if debug mode
|
||||||
|
|
|
@ -437,7 +437,8 @@ func TestSync(t *testing.T) {
|
||||||
a, err := New(&config.Config{Options: o})
|
a, err := New(&config.Config{Options: o})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
a.state.Load().dataBrokerClient = dbdClient
|
a.state.Load().dataBrokerClient = dbdClient
|
||||||
assert.True(t, (a.forceSync(ctx, tc.sessionState) != nil) == tc.wantErr)
|
_, err = a.forceSync(ctx, tc.sessionState)
|
||||||
|
assert.True(t, (err != nil) == tc.wantErr)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package authorize
|
package authorize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/encoding"
|
"github.com/pomerium/pomerium/internal/encoding"
|
||||||
|
@ -85,41 +83,3 @@ func getJWTSetCookieHeaders(cookieStore sessions.SessionStore, rawjwt []byte) (m
|
||||||
}
|
}
|
||||||
return hdrs, nil
|
return hdrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) getJWTClaimHeaders(options *config.Options, signedJWT string) (map[string]string, error) {
|
|
||||||
if len(signedJWT) == 0 {
|
|
||||||
return make(map[string]string), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
state := a.state.Load()
|
|
||||||
|
|
||||||
var claims map[string]interface{}
|
|
||||||
payload, err := state.evaluator.ParseSignedJWT(signedJWT)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hdrs := make(map[string]string)
|
|
||||||
for _, name := range options.JWTClaimsHeaders {
|
|
||||||
if claim, ok := claims[name]; ok {
|
|
||||||
switch value := claim.(type) {
|
|
||||||
case string:
|
|
||||||
hdrs["x-pomerium-claim-"+name] = value
|
|
||||||
case []interface{}:
|
|
||||||
hdrs["x-pomerium-claim-"+name] = strings.Join(toSliceStrings(value), ",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hdrs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toSliceStrings(sliceIfaces []interface{}) []string {
|
|
||||||
sliceStrings := make([]string, 0, len(sliceIfaces))
|
|
||||||
for _, e := range sliceIfaces {
|
|
||||||
sliceStrings = append(sliceStrings, fmt.Sprint(e))
|
|
||||||
}
|
|
||||||
return sliceStrings
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,15 +7,10 @@ import (
|
||||||
|
|
||||||
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/directory"
|
|
||||||
"github.com/pomerium/pomerium/internal/encoding/jws"
|
"github.com/pomerium/pomerium/internal/encoding/jws"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
"github.com/pomerium/pomerium/internal/sessions"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadSession(t *testing.T) {
|
func TestLoadSession(t *testing.T) {
|
||||||
|
@ -105,71 +100,3 @@ func TestLoadSession(t *testing.T) {
|
||||||
assert.NotNil(t, sess)
|
assert.NotNil(t, sess)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthorize_getJWTClaimHeaders(t *testing.T) {
|
|
||||||
opt := &config.Options{
|
|
||||||
AuthenticateURL: mustParseURL("https://authenticate.example.com"),
|
|
||||||
Policies: []config.Policy{{
|
|
||||||
Source: &config.StringURL{URL: &url.URL{Host: "example.com"}},
|
|
||||||
SubPolicies: []config.SubPolicy{{
|
|
||||||
Rego: []string{"allow = true"},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
a := &Authorize{currentOptions: config.NewAtomicOptions(), state: newAtomicAuthorizeState(new(authorizeState))}
|
|
||||||
encoder, _ := jws.NewHS256Signer([]byte{0, 0, 0, 0})
|
|
||||||
a.state.Load().encoder = encoder
|
|
||||||
a.currentOptions.Store(opt)
|
|
||||||
a.store = evaluator.NewStoreFromProtos(
|
|
||||||
&session.Session{
|
|
||||||
Id: "SESSION_ID",
|
|
||||||
UserId: "USER_ID",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "USER_ID",
|
|
||||||
Name: "foo",
|
|
||||||
Email: "foo@example.com",
|
|
||||||
},
|
|
||||||
&directory.User{
|
|
||||||
Id: "USER_ID",
|
|
||||||
GroupIds: []string{"admin_id", "test_id"},
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "admin_id",
|
|
||||||
Name: "admin",
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "test_id",
|
|
||||||
Name: "test",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
pe, err := newPolicyEvaluator(opt, a.store)
|
|
||||||
require.NoError(t, err)
|
|
||||||
a.state.Load().evaluator = pe
|
|
||||||
signedJWT, _ := pe.SignedJWT(pe.JWTPayload(&evaluator.Request{
|
|
||||||
HTTP: evaluator.RequestHTTP{URL: "https://example.com"},
|
|
||||||
Session: evaluator.RequestSession{
|
|
||||||
ID: "SESSION_ID",
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
signedJWT string
|
|
||||||
jwtHeaders []string
|
|
||||||
expectedHeaders map[string]string
|
|
||||||
}{
|
|
||||||
{"good with email", signedJWT, []string{"email"}, map[string]string{"x-pomerium-claim-email": "foo@example.com"}},
|
|
||||||
{"good with groups", signedJWT, []string{"groups"}, map[string]string{"x-pomerium-claim-groups": "admin_id,test_id,admin,test"}},
|
|
||||||
{"empty signed JWT", "", nil, make(map[string]string)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
opt.JWTClaimsHeaders = tc.jwtHeaders
|
|
||||||
gotHeaders, err := a.getJWTClaimHeaders(opt, tc.signedJWT)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.expectedHeaders, gotHeaders)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue