mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-30 09:27:19 +02:00
kubernetes apiserver integration (#1063)
* sessions: support bearer tokens in authorization * wip * remove dead code * refactor signed jwt code * use function * update per comments * fix test
This commit is contained in:
parent
5f6a67e6eb
commit
a70254ab76
10 changed files with 140 additions and 57 deletions
|
@ -9,6 +9,7 @@ import (
|
|||
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_type "github.com/envoyproxy/go-control-plane/envoy/type"
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
"google.golang.org/genproto/googleapis/rpc/status"
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
|
@ -25,7 +26,9 @@ func (a *Authorize) okResponse(reply *evaluator.Result) *envoy_service_auth_v2.C
|
|||
}
|
||||
|
||||
requestHeaders = append(requestHeaders,
|
||||
mkHeader(httputil.HeaderPomeriumJWTAssertion, reply.SignedJWT))
|
||||
mkHeader(httputil.HeaderPomeriumJWTAssertion, reply.SignedJWT, false))
|
||||
|
||||
requestHeaders = append(requestHeaders, getKubernetesHeaders(reply)...)
|
||||
|
||||
return &envoy_service_auth_v2.CheckResponse{
|
||||
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
||||
|
@ -82,10 +85,10 @@ func (a *Authorize) htmlDeniedResponse(code int32, reason string, headers map[st
|
|||
}
|
||||
|
||||
envoyHeaders := []*envoy_api_v2_core.HeaderValueOption{
|
||||
mkHeader("Content-Type", "text/html"),
|
||||
mkHeader("Content-Type", "text/html", false),
|
||||
}
|
||||
for k, v := range headers {
|
||||
envoyHeaders = append(envoyHeaders, mkHeader(k, v))
|
||||
envoyHeaders = append(envoyHeaders, mkHeader(k, v, false))
|
||||
}
|
||||
|
||||
return &envoy_service_auth_v2.CheckResponse{
|
||||
|
@ -104,10 +107,10 @@ func (a *Authorize) htmlDeniedResponse(code int32, reason string, headers map[st
|
|||
|
||||
func (a *Authorize) plainTextDeniedResponse(code int32, reason string, headers map[string]string) *envoy_service_auth_v2.CheckResponse {
|
||||
envoyHeaders := []*envoy_api_v2_core.HeaderValueOption{
|
||||
mkHeader("Content-Type", "text/plain"),
|
||||
mkHeader("Content-Type", "text/plain", false),
|
||||
}
|
||||
for k, v := range headers {
|
||||
envoyHeaders = append(envoyHeaders, mkHeader(k, v))
|
||||
envoyHeaders = append(envoyHeaders, mkHeader(k, v, false))
|
||||
}
|
||||
|
||||
return &envoy_service_auth_v2.CheckResponse{
|
||||
|
@ -138,11 +141,30 @@ func (a *Authorize) redirectResponse(in *envoy_service_auth_v2.CheckRequest) *en
|
|||
})
|
||||
}
|
||||
|
||||
func mkHeader(k, v string) *envoy_api_v2_core.HeaderValueOption {
|
||||
func getKubernetesHeaders(reply *evaluator.Result) []*envoy_api_v2_core.HeaderValueOption {
|
||||
var requestHeaders []*envoy_api_v2_core.HeaderValueOption
|
||||
if reply.MatchingPolicy != nil && 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 _, group := range reply.UserGroups {
|
||||
requestHeaders = append(requestHeaders, mkHeader("Impersonate-Group", group, true))
|
||||
}
|
||||
}
|
||||
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{
|
||||
Key: k,
|
||||
Value: v,
|
||||
},
|
||||
Append: &wrappers.BoolValue{
|
||||
Value: shouldAppend,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,8 +38,9 @@ const (
|
|||
|
||||
// Evaluator specifies the interface for a policy engine.
|
||||
type Evaluator struct {
|
||||
rego *rego.Rego
|
||||
query rego.PreparedEvalQuery
|
||||
rego *rego.Rego
|
||||
query rego.PreparedEvalQuery
|
||||
policies []config.Policy
|
||||
|
||||
clientCA string
|
||||
authenticateHost string
|
||||
|
@ -51,6 +52,7 @@ type Evaluator struct {
|
|||
func New(options *config.Options) (*Evaluator, error) {
|
||||
e := &Evaluator{
|
||||
authenticateHost: options.AuthenticateURL.Host,
|
||||
policies: options.Policies,
|
||||
}
|
||||
if options.ClientCA != "" {
|
||||
e.clientCA = options.ClientCA
|
||||
|
@ -129,33 +131,40 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error)
|
|||
return &deny[0], nil
|
||||
}
|
||||
|
||||
signedJWT, err := e.SignedJWT(req)
|
||||
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
|
||||
}
|
||||
|
||||
allow := allowed(res[0].Bindings.WithoutWildcards())
|
||||
if allow {
|
||||
return &Result{
|
||||
Status: http.StatusOK,
|
||||
Message: "OK",
|
||||
SignedJWT: signedJWT,
|
||||
}, nil
|
||||
evalResult.Status = http.StatusOK
|
||||
evalResult.Message = "OK"
|
||||
return evalResult, nil
|
||||
}
|
||||
|
||||
if req.Session.ID == "" {
|
||||
return &Result{
|
||||
Status: http.StatusUnauthorized,
|
||||
Message: "login required",
|
||||
SignedJWT: signedJWT,
|
||||
}, nil
|
||||
evalResult.Status = http.StatusUnauthorized
|
||||
evalResult.Message = "login required"
|
||||
return evalResult, nil
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Status: http.StatusForbidden,
|
||||
Message: "forbidden",
|
||||
SignedJWT: signedJWT,
|
||||
}, nil
|
||||
evalResult.Status = http.StatusForbidden
|
||||
evalResult.Message = "forbidden"
|
||||
return evalResult, nil
|
||||
}
|
||||
|
||||
// ParseSignedJWT parses the input signature and return its payload.
|
||||
|
@ -167,17 +176,8 @@ func (e *Evaluator) ParseSignedJWT(signature string) ([]byte, error) {
|
|||
return object.Verify(&(e.jwk.(*ecdsa.PrivateKey).PublicKey))
|
||||
}
|
||||
|
||||
// SignedJWT returns the signature of given request.
|
||||
func (e *Evaluator) SignedJWT(req *Request) (string, error) {
|
||||
signerOpt := &jose.SignerOptions{}
|
||||
signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.ES256,
|
||||
Key: e.jwk,
|
||||
}, signerOpt.WithHeader("kid", e.kid))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// JWTPayload returns the JWT payload for a request.
|
||||
func (e *Evaluator) JWTPayload(req *Request) map[string]interface{} {
|
||||
payload := map[string]interface{}{
|
||||
"iss": e.authenticateHost,
|
||||
}
|
||||
|
@ -200,6 +200,19 @@ func (e *Evaluator) SignedJWT(req *Request) (string, error) {
|
|||
payload["groups"] = du.GetGroups()
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
// SignedJWT returns the signature of given request.
|
||||
func (e *Evaluator) SignedJWT(payload map[string]interface{}) (string, error) {
|
||||
signerOpt := &jose.SignerOptions{}
|
||||
signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.ES256,
|
||||
Key: e.jwk,
|
||||
}, signerOpt.WithHeader("kid", e.kid))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
|
@ -266,9 +279,31 @@ type (
|
|||
|
||||
// Result is the result of evaluation.
|
||||
type Result struct {
|
||||
Status int
|
||||
Message string
|
||||
SignedJWT string
|
||||
Status int
|
||||
Message string
|
||||
SignedJWT string
|
||||
MatchingPolicy *config.Policy
|
||||
|
||||
UserEmail string
|
||||
UserGroups []string
|
||||
}
|
||||
|
||||
func getMatchingPolicy(vars rego.Vars, policies []config.Policy) *config.Policy {
|
||||
result, ok := vars["result"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx, err := strconv.Atoi(fmt.Sprint(result["route_policy_idx"]))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if idx >= len(policies) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &policies[idx]
|
||||
}
|
||||
|
||||
func allowed(vars rego.Vars) bool {
|
||||
|
|
|
@ -80,7 +80,7 @@ func TestEvaluator_SignedJWT(t *testing.T) {
|
|||
URL: "https://example.com",
|
||||
},
|
||||
}
|
||||
signedJWT, err := e.SignedJWT(req)
|
||||
signedJWT, err := e.SignedJWT(e.JWTPayload(req))
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, signedJWT)
|
||||
|
||||
|
@ -101,7 +101,7 @@ func TestEvaluator_JWTWithKID(t *testing.T) {
|
|||
URL: "https://example.com",
|
||||
},
|
||||
}
|
||||
signedJWT, err := e.SignedJWT(req)
|
||||
signedJWT, err := e.SignedJWT(e.JWTPayload(req))
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, signedJWT)
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ package pomerium.authz
|
|||
default allow = false
|
||||
|
||||
|
||||
route_policy := first_allowed_route_policy(input.http.url)
|
||||
route_policy_idx := first_allowed_route_policy_idx(input.http.url)
|
||||
route_policy := data.route_policies[route_policy_idx]
|
||||
session := input.databroker_data.session
|
||||
user := input.databroker_data.user
|
||||
directory_user := input.databroker_data.directory_user
|
||||
|
@ -84,8 +85,8 @@ deny[reason] {
|
|||
}
|
||||
|
||||
# returns the first matching route
|
||||
first_allowed_route_policy(input_url) = first_policy {
|
||||
first_policy := [policy | some i, policy; policy = data.route_policies[i]; allowed_route(input.http.url, policy)][0]
|
||||
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]
|
||||
}
|
||||
|
||||
allowed_route(input_url, policy){
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -173,7 +173,7 @@ func (a *Authorize) getEnvoyRequestHeaders(signedJWT string) ([]*envoy_api_v2_co
|
|||
return nil, err
|
||||
}
|
||||
for k, v := range hdrs {
|
||||
hvos = append(hvos, mkHeader(k, v))
|
||||
hvos = append(hvos, mkHeader(k, v, false))
|
||||
}
|
||||
|
||||
return hvos, nil
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue