mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 07:37:33 +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"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
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 {
|
||||
requestHeaders, err := a.getEnvoyRequestHeaders(reply.SignedJWT)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("authorize: error generating new request headers")
|
||||
var requestHeaders []*envoy_api_v2_core.HeaderValueOption
|
||||
for k, v := range reply.Headers {
|
||||
requestHeaders = append(requestHeaders, mkHeader(k, v, false))
|
||||
}
|
||||
|
||||
requestHeaders = append(requestHeaders,
|
||||
mkHeader(httputil.HeaderPomeriumJWTAssertion, reply.SignedJWT, false))
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// ensure request headers are sorted by key for deterministic output
|
||||
sort.Slice(requestHeaders, func(i, j int) bool {
|
||||
return requestHeaders[i].Header.Key < requestHeaders[j].Header.Value
|
||||
})
|
||||
return &envoy_service_auth_v2.CheckResponse{
|
||||
Status: &status.Status{Code: int32(codes.OK), Message: reply.Message},
|
||||
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 {
|
||||
return &envoy_api_v2_core.HeaderValueOption{
|
||||
Header: &envoy_api_v2_core.HeaderValue{
|
||||
|
|
|
@ -3,10 +3,8 @@ package authorize
|
|||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
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"
|
||||
|
@ -55,29 +53,6 @@ func TestAuthorize_okResponse(t *testing.T) {
|
|||
pe, err := newPolicyEvaluator(opt, a.store)
|
||||
require.NoError(t, err)
|
||||
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 {
|
||||
name string
|
||||
|
@ -86,107 +61,45 @@ func TestAuthorize_okResponse(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
"ok reply",
|
||||
&evaluator.Result{Status: 0, Message: "ok", SignedJWT: "valid-signed-jwt"},
|
||||
&evaluator.Result{Status: 0, Message: "ok"},
|
||||
&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),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"ok reply with k8s svc",
|
||||
&evaluator.Result{
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
SignedJWT: "valid-signed-jwt",
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
MatchingPolicy: &config.Policy{
|
||||
KubernetesServiceAccountToken: "k8s-svc-account",
|
||||
},
|
||||
},
|
||||
&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 k8s-svc-account", false),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"ok reply with k8s svc impersonate",
|
||||
&evaluator.Result{
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
SignedJWT: "valid-signed-jwt",
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
MatchingPolicy: &config.Policy{
|
||||
KubernetesServiceAccountToken: "k8s-svc-account",
|
||||
},
|
||||
UserEmail: "foo@example.com",
|
||||
UserGroups: []string{"admin", "test"},
|
||||
},
|
||||
&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 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",
|
||||
&evaluator.Result{
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
SignedJWT: validJWT,
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
},
|
||||
&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-claim-email", "foo@example.com", false),
|
||||
mkHeader("x-pomerium-jwt-assertion", validJWT, false),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ type CustomEvaluatorResponse struct {
|
|||
Allowed bool
|
||||
Denied bool
|
||||
Reason string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// A CustomEvaluator evaluates custom rego policies.
|
||||
|
@ -65,7 +66,9 @@ func (ce *CustomEvaluator) Evaluate(ctx context.Context, req *CustomEvaluatorReq
|
|||
vars = make(map[string]interface{})
|
||||
}
|
||||
|
||||
res := &CustomEvaluatorResponse{}
|
||||
res := &CustomEvaluatorResponse{
|
||||
Headers: getHeadersVar(resultSet[0].Bindings.WithoutWildcards()),
|
||||
}
|
||||
res.Allowed, _ = vars["allow"].(bool)
|
||||
if v, ok := vars["deny"]; ok {
|
||||
// support `deny = true`
|
||||
|
|
|
@ -5,8 +5,6 @@ package evaluator
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -26,22 +24,16 @@ type Evaluator struct {
|
|||
query rego.PreparedEvalQuery
|
||||
policies []config.Policy
|
||||
store *Store
|
||||
|
||||
authenticateHost string
|
||||
jwk *jose.JSONWebKey
|
||||
signer jose.Signer
|
||||
}
|
||||
|
||||
// New creates a new Evaluator.
|
||||
func New(options *config.Options, store *Store) (*Evaluator, error) {
|
||||
e := &Evaluator{
|
||||
custom: NewCustomEvaluator(store.opaStore),
|
||||
authenticateHost: options.AuthenticateURL.Host,
|
||||
policies: options.GetAllPolicies(),
|
||||
store: store,
|
||||
custom: NewCustomEvaluator(store.opaStore),
|
||||
policies: options.GetAllPolicies(),
|
||||
store: store,
|
||||
}
|
||||
var err error
|
||||
e.signer, e.jwk, err = newSigner(options)
|
||||
jwk, err := getJWK(options)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
store.UpdateIssuer(options.AuthenticateURL.Host)
|
||||
store.UpdateGoogleCloudServerlessAuthenticationServiceAccount(options.GoogleCloudServerlessAuthenticationServiceAccount)
|
||||
store.UpdateJWTClaimHeaders(options.JWTClaimsHeaders)
|
||||
store.UpdateRoutePolicies(options.GetAllPolicies())
|
||||
store.UpdateSigningKey(jwk)
|
||||
|
||||
e.rego = rego.New(
|
||||
rego.Store(store.opaStore),
|
||||
rego.Module("pomerium.authz", string(authzPolicy)),
|
||||
rego.Query("result = data.pomerium.authz"),
|
||||
getGoogleCloudServerlessHeadersRegoOption,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
payload := e.JWTPayload(req)
|
||||
|
||||
signedJWT, err := e.SignedJWT(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error signing JWT: %w", err)
|
||||
}
|
||||
|
||||
evalResult := &Result{
|
||||
MatchingPolicy: getMatchingPolicy(res[0].Bindings.WithoutWildcards(), e.policies),
|
||||
SignedJWT: signedJWT,
|
||||
}
|
||||
if e, ok := payload["email"].(string); ok {
|
||||
evalResult.UserEmail = e
|
||||
}
|
||||
if gs, ok := payload["groups"].([]string); ok {
|
||||
evalResult.UserGroups = gs
|
||||
Headers: getHeadersVar(res[0].Bindings.WithoutWildcards()),
|
||||
}
|
||||
|
||||
allow := allowed(res[0].Bindings.WithoutWildcards())
|
||||
allow := getAllowVar(res[0].Bindings.WithoutWildcards())
|
||||
// evaluate any custom policies
|
||||
if allow {
|
||||
for _, src := range req.CustomPolicies {
|
||||
|
@ -118,6 +102,9 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error)
|
|||
if cres.Reason != "" {
|
||||
evalResult.Message = cres.Reason
|
||||
}
|
||||
for k, v := range cres.Headers {
|
||||
evalResult.Headers[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if allow {
|
||||
|
@ -139,41 +126,23 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error)
|
|||
return evalResult, nil
|
||||
}
|
||||
|
||||
// ParseSignedJWT parses the input signature and return its payload.
|
||||
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) {
|
||||
func getJWK(options *config.Options) (*jose.JSONWebKey, error) {
|
||||
var decodedCert []byte
|
||||
// if we don't have a signing key, generate one
|
||||
if options.SigningKey == "" {
|
||||
key, err := cryptutil.NewSigningKey()
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("bad signing key: %w", err)
|
||||
return nil, fmt.Errorf("bad signing key: %w", err)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
decodedCert, err = base64.StdEncoding.DecodeString(options.SigningKey)
|
||||
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
|
||||
|
@ -183,41 +152,14 @@ func newSigner(options *config.Options) (jose.Signer, *jose.JSONWebKey, error) {
|
|||
|
||||
jwk, err := cryptutil.PrivateJWKFromBytes(decodedCert, jose.SignatureAlgorithm(signingKeyAlgorithm))
|
||||
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).
|
||||
Str("KeyID", jwk.KeyID).
|
||||
Interface("Public Key", jwk.Public()).
|
||||
Msg("authorize: signing key")
|
||||
|
||||
signerOpt := &jose.SignerOptions{}
|
||||
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()
|
||||
return jwk, nil
|
||||
}
|
||||
|
||||
type input struct {
|
||||
|
@ -238,11 +180,8 @@ func (e *Evaluator) newInput(req *Request, isValidClientCertificate bool) *input
|
|||
type Result struct {
|
||||
Status int
|
||||
Message string
|
||||
SignedJWT string
|
||||
Headers map[string]string
|
||||
MatchingPolicy *config.Policy
|
||||
|
||||
UserEmail string
|
||||
UserGroups []string
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
func allowed(vars rego.Vars) bool {
|
||||
func getAllowVar(vars rego.Vars) bool {
|
||||
result, ok := vars["result"].(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
|
@ -308,3 +247,23 @@ func getDenyVar(vars rego.Vars) []Result {
|
|||
}
|
||||
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/stretchr/testify/assert"
|
||||
"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/pkg/grpc/databroker"
|
||||
|
@ -73,212 +71,6 @@ func TestJSONMarshal(t *testing.T) {
|
|||
}`, 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) {
|
||||
sessionID := uuid.New().String()
|
||||
userID := uuid.New().String()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package authorize
|
||||
package evaluator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -12,19 +12,49 @@ import (
|
|||
"sync"
|
||||
"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/sync/singleflight"
|
||||
"google.golang.org/api/idtoken"
|
||||
|
||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
||||
)
|
||||
|
||||
// GCP pre-defined values.
|
||||
var (
|
||||
gpcIdentityTokenExpiration = time.Minute * 45 // tokens expire after one hour according to the GCP docs
|
||||
gcpIdentityDocURL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
|
||||
gcpIdentityNow = time.Now
|
||||
gcpIdentityMaxBodySize int64 = 1024 * 1024 * 10
|
||||
GCPIdentityTokenExpiration = time.Minute * 45 // tokens expire after one hour according to the GCP docs
|
||||
GCPIdentityDocURL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
|
||||
GCPIdentityNow = time.Now
|
||||
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 {
|
||||
|
@ -34,7 +64,7 @@ type gcpIdentityTokenSource struct {
|
|||
|
||||
func (src *gcpIdentityTokenSource) Token() (*oauth2.Token, 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"},
|
||||
"audience": {src.audience},
|
||||
}.Encode(), nil)
|
||||
|
@ -49,7 +79,7 @@ func (src *gcpIdentityTokenSource) Token() (*oauth2.Token, error) {
|
|||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -62,7 +92,7 @@ func (src *gcpIdentityTokenSource) Token() (*oauth2.Token, error) {
|
|||
return &oauth2.Token{
|
||||
AccessToken: strings.TrimSpace(res.(string)),
|
||||
TokenType: "bearer",
|
||||
Expiry: gcpIdentityNow().Add(gpcIdentityTokenExpiration),
|
||||
Expiry: GCPIdentityNow().Add(GCPIdentityTokenExpiration),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -127,18 +157,7 @@ func getGoogleCloudServerlessTokenSource(serviceAccount, audience string) (oauth
|
|||
return src, nil
|
||||
}
|
||||
|
||||
func (a *Authorize) getGoogleCloudServerlessAuthenticationHeaders(reply *evaluator.Result) ([]*envoy_api_v2_core.HeaderValueOption, 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)
|
||||
|
||||
func getGoogleCloudServerlessHeaders(serviceAccount, audience string) (map[string]string, error) {
|
||||
src, err := getGoogleCloudServerlessTokenSource(serviceAccount, audience)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -149,7 +168,7 @@ func (a *Authorize) getGoogleCloudServerlessAuthenticationHeaders(reply *evaluat
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return []*envoy_api_v2_core.HeaderValueOption{
|
||||
mkHeader("Authorization", "Bearer "+tok.AccessToken, false),
|
||||
return map[string]string{
|
||||
"Authorization": "Bearer " + tok.AccessToken,
|
||||
}, nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package authorize
|
||||
package evaluator
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -9,34 +9,38 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGCPIdentityTokenSource(t *testing.T) {
|
||||
originalGCPIdentityDocURL := gcpIdentityDocURL
|
||||
func withMockGCP(t *testing.T, f func()) {
|
||||
originalGCPIdentityDocURL := GCPIdentityDocURL
|
||||
defer func() {
|
||||
gcpIdentityDocURL = originalGCPIdentityDocURL
|
||||
gcpIdentityNow = time.Now
|
||||
GCPIdentityDocURL = originalGCPIdentityDocURL
|
||||
GCPIdentityNow = time.Now
|
||||
}()
|
||||
|
||||
now := time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)
|
||||
gcpIdentityNow = func() time.Time {
|
||||
GCPIdentityNow = func() time.Time {
|
||||
return now
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "Google", r.Header.Get("Metadata-Flavor"))
|
||||
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)))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
gcpIdentityDocURL = srv.URL
|
||||
GCPIdentityDocURL = srv.URL
|
||||
f()
|
||||
}
|
||||
|
||||
src, err := getGoogleCloudServerlessTokenSource("", "example")
|
||||
assert.NoError(t, err)
|
||||
func TestGCPIdentityTokenSource(t *testing.T) {
|
||||
withMockGCP(t, func() {
|
||||
src, err := getGoogleCloudServerlessTokenSource("", "example")
|
||||
assert.NoError(t, err)
|
||||
|
||||
token, err := src.Token()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2020-01-01T01:00:00Z", token.AccessToken)
|
||||
token, err := src.Token()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2020-01-01T01:00:00Z", token.AccessToken)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_normalizeServiceAccount(t *testing.T) {
|
|
@ -37,7 +37,7 @@ directory_user = du {
|
|||
}
|
||||
|
||||
group_ids = gs {
|
||||
gs = session.impersonate_group_ids
|
||||
gs = session.impersonate_groups
|
||||
gs != null
|
||||
} else = gs {
|
||||
gs = directory_user.group_ids
|
||||
|
@ -145,6 +145,138 @@ deny[reason] {
|
|||
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
|
||||
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]
|
||||
|
@ -195,12 +327,20 @@ allowed_route_regex(input_url_obj, policy) {
|
|||
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]
|
||||
|
||||
[hostname, _] = parse_host_port(host)
|
||||
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) = "/" {
|
||||
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]
|
||||
}
|
||||
|
||||
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
|
||||
# rego doesn't support recursion, so we hard code a limited number of /'s
|
||||
object_get(obj, key, def) = value {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
package pomerium.authz
|
||||
|
||||
get_google_cloud_serverless_headers(serviceAccount, audience) = h {
|
||||
h := {
|
||||
"Authorization": "Bearer xxx"
|
||||
}
|
||||
}
|
||||
|
||||
test_email_allowed {
|
||||
allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,13 +4,18 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"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/pkg/cryptutil"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
|
@ -20,15 +25,28 @@ func TestOPA(t *testing.T) {
|
|||
type A = []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 {
|
||||
authzPolicy, err := readPolicy("/authz.rego")
|
||||
require.NoError(t, err)
|
||||
store := NewStoreFromProtos(data...)
|
||||
store.UpdateIssuer("authenticate.example.com")
|
||||
store.UpdateJWTClaimHeaders([]string{"email", "groups", "user"})
|
||||
store.UpdateRoutePolicies(policies)
|
||||
store.UpdateSigningKey(privateJWK)
|
||||
r := rego.New(
|
||||
rego.Store(store.opaStore),
|
||||
rego.Module("pomerium.authz", string(authzPolicy)),
|
||||
rego.Query("result = data.pomerium.authz"),
|
||||
getGoogleCloudServerlessHeadersRegoOption,
|
||||
)
|
||||
q, err := r.PrepareForEval(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
@ -46,11 +64,168 @@ func TestOPA(t *testing.T) {
|
|||
A{A{json.Number("495"), "invalid client certificate"}},
|
||||
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("allowed", func(t *testing.T) {
|
||||
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{
|
||||
{URL: *mustParseURL("https://to.example.com")},
|
||||
},
|
||||
|
@ -71,7 +246,7 @@ func TestOPA(t *testing.T) {
|
|||
},
|
||||
HTTP: RequestHTTP{
|
||||
Method: "GET",
|
||||
URL: "https://from.example.com",
|
||||
URL: "https://from.example.com:8000",
|
||||
},
|
||||
}, true)
|
||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
||||
|
|
|
@ -1,15 +1,5 @@
|
|||
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 (
|
||||
// Request is the request data used for the evaluator.
|
||||
Request struct {
|
||||
|
@ -32,69 +22,3 @@ type (
|
|||
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/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
|
@ -89,6 +90,22 @@ func (s *Store) GetRecordData(typeURL, id string) proto.Message {
|
|||
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.
|
||||
func (s *Store) UpdateRoutePolicies(routePolicies []config.Policy) {
|
||||
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{}) {
|
||||
p, ok := storage.ParsePath(rawPath)
|
||||
if !ok {
|
||||
|
|
|
@ -23,7 +23,6 @@ import (
|
|||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
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")
|
||||
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")
|
||||
return nil, err
|
||||
}
|
||||
logAuthorizeCheck(ctx, in, reply)
|
||||
logAuthorizeCheck(ctx, in, reply, u)
|
||||
|
||||
switch {
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
defer span.End()
|
||||
if ss == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
s := a.forceSyncSession(ctx, ss.ID)
|
||||
if s == nil {
|
||||
return errors.New("session not found")
|
||||
return nil, errors.New("session not found")
|
||||
}
|
||||
a.forceSyncUser(ctx, s.GetUserId())
|
||||
return nil
|
||||
u := a.forceSyncUser(ctx, s.GetUserId())
|
||||
return u, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
urqQuery := r.URL.Query().Get("uri")
|
||||
u, _ := urlutil.ParseAndValidateURL(urqQuery)
|
||||
|
@ -334,6 +320,7 @@ func logAuthorizeCheck(
|
|||
ctx context.Context,
|
||||
in *envoy_service_auth_v2.CheckRequest,
|
||||
reply *evaluator.Result,
|
||||
u *user.User,
|
||||
) {
|
||||
hdrs := getCheckRequestHeaders(in)
|
||||
hattrs := in.GetAttributes().GetRequest().GetHttp()
|
||||
|
@ -350,8 +337,8 @@ func logAuthorizeCheck(
|
|||
evt = evt.Bool("allow", reply.Status == http.StatusOK)
|
||||
evt = evt.Int("status", reply.Status)
|
||||
evt = evt.Str("message", reply.Message)
|
||||
evt = evt.Str("user", reply.UserEmail)
|
||||
evt = evt.Strs("groups", reply.UserGroups)
|
||||
evt = evt.Str("user", u.GetId())
|
||||
evt = evt.Str("email", u.GetEmail())
|
||||
}
|
||||
|
||||
// potentially sensitive, only log if debug mode
|
||||
|
|
|
@ -437,7 +437,8 @@ func TestSync(t *testing.T) {
|
|||
a, err := New(&config.Config{Options: o})
|
||||
require.NoError(t, err)
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/encoding"
|
||||
|
@ -85,41 +83,3 @@ func getJWTSetCookieHeaders(cookieStore sessions.SessionStore, rawjwt []byte) (m
|
|||
}
|
||||
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"
|
||||
"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/internal/directory"
|
||||
"github.com/pomerium/pomerium/internal/encoding/jws"
|
||||
"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) {
|
||||
|
@ -105,71 +100,3 @@ func TestLoadSession(t *testing.T) {
|
|||
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