mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-30 19:06:33 +02:00
authorize: add support for webauthn device policy enforcement (#2700)
* authorize: add support for webauthn device policy enforcement * update docs * group statuses
This commit is contained in:
parent
9d4ebcf871
commit
3497c39b9b
8 changed files with 456 additions and 27 deletions
|
@ -45,10 +45,10 @@ func (a *Authorize) handleResultDenied(
|
||||||
switch {
|
switch {
|
||||||
case result.Deny.Reasons.Has(criteria.ReasonRouteNotFound):
|
case result.Deny.Reasons.Has(criteria.ReasonRouteNotFound):
|
||||||
denyStatusCode = http.StatusNotFound
|
denyStatusCode = http.StatusNotFound
|
||||||
denyStatusText = http.StatusText(http.StatusNotFound)
|
denyStatusText = httputil.DetailsText(http.StatusNotFound)
|
||||||
case result.Deny.Reasons.Has(criteria.ReasonInvalidClientCertificate):
|
case result.Deny.Reasons.Has(criteria.ReasonInvalidClientCertificate):
|
||||||
denyStatusCode = httputil.StatusInvalidClientCertificate
|
denyStatusCode = httputil.StatusInvalidClientCertificate
|
||||||
denyStatusText = httputil.StatusText(httputil.StatusInvalidClientCertificate)
|
denyStatusText = httputil.DetailsText(httputil.StatusInvalidClientCertificate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.deniedResponse(ctx, in, denyStatusCode, denyStatusText, nil)
|
return a.deniedResponse(ctx, in, denyStatusCode, denyStatusText, nil)
|
||||||
|
@ -65,9 +65,18 @@ func (a *Authorize) handleResultNotAllowed(
|
||||||
// when the user is unauthenticated it means they haven't
|
// when the user is unauthenticated it means they haven't
|
||||||
// logged in yet, so redirect to authenticate
|
// logged in yet, so redirect to authenticate
|
||||||
return a.requireLoginResponse(ctx, in, isForwardAuthVerify)
|
return a.requireLoginResponse(ctx, in, isForwardAuthVerify)
|
||||||
|
case result.Allow.Reasons.Has(criteria.ReasonDeviceUnauthenticated):
|
||||||
|
// when the user's device is unauthenticated it means they haven't
|
||||||
|
// registered a webauthn device yet, so redirect to the webauthn flow
|
||||||
|
return a.requireWebAuthnResponse(ctx, in, result, isForwardAuthVerify)
|
||||||
|
case result.Allow.Reasons.Has(criteria.ReasonDeviceUnauthorized):
|
||||||
|
return a.deniedResponse(ctx, in,
|
||||||
|
httputil.StatusDeviceUnauthorized,
|
||||||
|
httputil.DetailsText(httputil.StatusDeviceUnauthorized),
|
||||||
|
nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.deniedResponse(ctx, in, http.StatusForbidden, http.StatusText(http.StatusForbidden), nil)
|
return a.deniedResponse(ctx, in, http.StatusForbidden, httputil.DetailsText(http.StatusForbidden), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) okResponse(headers http.Header) *envoy_service_auth_v3.CheckResponse {
|
func (a *Authorize) okResponse(headers http.Header) *envoy_service_auth_v3.CheckResponse {
|
||||||
|
@ -94,16 +103,6 @@ func (a *Authorize) deniedResponse(
|
||||||
in *envoy_service_auth_v3.CheckRequest,
|
in *envoy_service_auth_v3.CheckRequest,
|
||||||
code int32, reason string, headers map[string]string,
|
code int32, reason string, headers map[string]string,
|
||||||
) (*envoy_service_auth_v3.CheckResponse, error) {
|
) (*envoy_service_auth_v3.CheckResponse, error) {
|
||||||
var details string
|
|
||||||
switch code {
|
|
||||||
case httputil.StatusInvalidClientCertificate:
|
|
||||||
details = httputil.StatusText(httputil.StatusInvalidClientCertificate)
|
|
||||||
case http.StatusForbidden:
|
|
||||||
details = http.StatusText(http.StatusForbidden)
|
|
||||||
default:
|
|
||||||
details = reason
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a http response writer recorder
|
// create a http response writer recorder
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r := getHTTPRequestFromCheckRequest(in)
|
r := getHTTPRequestFromCheckRequest(in)
|
||||||
|
@ -114,7 +113,7 @@ func (a *Authorize) deniedResponse(
|
||||||
// run the request through our go error handler
|
// run the request through our go error handler
|
||||||
httpErr := httputil.HTTPError{
|
httpErr := httputil.HTTPError{
|
||||||
Status: int(code),
|
Status: int(code),
|
||||||
Err: errors.New(details),
|
Err: errors.New(reason),
|
||||||
DebugURL: debugEndpoint,
|
DebugURL: debugEndpoint,
|
||||||
RequestID: requestid.FromContext(ctx),
|
RequestID: requestid.FromContext(ctx),
|
||||||
}
|
}
|
||||||
|
@ -172,10 +171,50 @@ func (a *Authorize) requireLoginResponse(
|
||||||
q := signinURL.Query()
|
q := signinURL.Query()
|
||||||
|
|
||||||
// always assume https scheme
|
// always assume https scheme
|
||||||
url := getCheckRequestURL(in)
|
checkRequestURL := getCheckRequestURL(in)
|
||||||
url.Scheme = "https"
|
checkRequestURL.Scheme = "https"
|
||||||
|
|
||||||
q.Set(urlutil.QueryRedirectURI, url.String())
|
q.Set(urlutil.QueryRedirectURI, checkRequestURL.String())
|
||||||
|
signinURL.RawQuery = q.Encode()
|
||||||
|
redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String()
|
||||||
|
|
||||||
|
return a.deniedResponse(ctx, in, http.StatusFound, "Login", map[string]string{
|
||||||
|
"Location": redirectTo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authorize) requireWebAuthnResponse(
|
||||||
|
ctx context.Context,
|
||||||
|
in *envoy_service_auth_v3.CheckRequest,
|
||||||
|
result *evaluator.Result,
|
||||||
|
isForwardAuthVerify bool,
|
||||||
|
) (*envoy_service_auth_v3.CheckResponse, error) {
|
||||||
|
opts := a.currentOptions.Load()
|
||||||
|
state := a.state.Load()
|
||||||
|
authenticateURL, err := opts.GetAuthenticateURL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.shouldRedirect(in) || isForwardAuthVerify {
|
||||||
|
return a.deniedResponse(ctx, in, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
signinURL := authenticateURL.ResolveReference(&url.URL{
|
||||||
|
Path: "/.pomerium/webauthn",
|
||||||
|
})
|
||||||
|
q := signinURL.Query()
|
||||||
|
|
||||||
|
// always assume https scheme
|
||||||
|
checkRequestURL := getCheckRequestURL(in)
|
||||||
|
checkRequestURL.Scheme = "https"
|
||||||
|
|
||||||
|
if deviceType, ok := result.Allow.AdditionalData["device_type"].(string); ok {
|
||||||
|
q.Set(urlutil.QueryDeviceType, deviceType)
|
||||||
|
} else {
|
||||||
|
q.Set(urlutil.QueryDeviceType, "default")
|
||||||
|
}
|
||||||
|
q.Set(urlutil.QueryRedirectURI, checkRequestURL.String())
|
||||||
signinURL.RawQuery = q.Encode()
|
signinURL.RawQuery = q.Encode()
|
||||||
redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String()
|
redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String()
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,8 @@ func TestRequireLogin(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("accept empty", func(t *testing.T) {
|
t.Run("accept empty", func(t *testing.T) {
|
||||||
res, err := a.requireLoginResponse(context.Background(), &envoy_service_auth_v3.CheckRequest{}, false)
|
res, err := a.requireLoginResponse(context.Background(), &envoy_service_auth_v3.CheckRequest{},
|
||||||
|
false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, http.StatusFound, int(res.GetDeniedResponse().GetStatus().GetCode()))
|
assert.Equal(t, http.StatusFound, int(res.GetDeniedResponse().GetStatus().GetCode()))
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,7 +30,7 @@ func NewError(status int, err error) error {
|
||||||
|
|
||||||
// Error implements the `error` interface.
|
// Error implements the `error` interface.
|
||||||
func (e *HTTPError) Error() string {
|
func (e *HTTPError) Error() string {
|
||||||
return http.StatusText(e.Status) + ": " + e.Err.Error()
|
return StatusText(e.Status) + ": " + e.Err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap implements the `error` Unwrap interface.
|
// Unwrap implements the `error` Unwrap interface.
|
||||||
|
@ -55,7 +55,7 @@ func (e *HTTPError) ErrorResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
DebugURL *url.URL `json:",omitempty"`
|
DebugURL *url.URL `json:",omitempty"`
|
||||||
}{
|
}{
|
||||||
Status: e.Status,
|
Status: e.Status,
|
||||||
StatusText: http.StatusText(e.Status),
|
StatusText: StatusText(e.Status),
|
||||||
Error: e.Error(),
|
Error: e.Error(),
|
||||||
RequestID: reqID,
|
RequestID: reqID,
|
||||||
CanDebug: e.Status/100 == 4 && (e.DebugURL != nil || reqID != ""),
|
CanDebug: e.Status/100 == 4 && (e.DebugURL != nil || reqID != ""),
|
||||||
|
|
|
@ -1,16 +1,41 @@
|
||||||
package httputil
|
package httputil
|
||||||
|
|
||||||
// StatusInvalidClientCertificate is the status code returned when a
|
import "net/http"
|
||||||
// client's certificate is invalid. This is the same status code used
|
|
||||||
// by nginx for this purpose.
|
const (
|
||||||
const StatusInvalidClientCertificate = 495
|
// StatusDeviceUnauthorized is the status code returned when a client's
|
||||||
|
// device credential is not authorized to access a page.
|
||||||
|
StatusDeviceUnauthorized = 450
|
||||||
|
// StatusInvalidClientCertificate is the status code returned when a
|
||||||
|
// client's certificate is invalid. This is the same status code used
|
||||||
|
// by nginx for this purpose.
|
||||||
|
StatusInvalidClientCertificate = 495
|
||||||
|
)
|
||||||
|
|
||||||
|
var detailsText = map[int]string{
|
||||||
|
StatusDeviceUnauthorized: "your device fails to meet the requirements necessary to access this page, please contact your administrator for assistance",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetailsText returns extra details for an HTTP status code. It returns StatusText if not found.
|
||||||
|
func DetailsText(code int) string {
|
||||||
|
txt, ok := detailsText[code]
|
||||||
|
if ok {
|
||||||
|
return txt
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusText(code)
|
||||||
|
}
|
||||||
|
|
||||||
var statusText = map[int]string{
|
var statusText = map[int]string{
|
||||||
|
StatusDeviceUnauthorized: "device not authorized",
|
||||||
StatusInvalidClientCertificate: "a valid client certificate is required to access this page",
|
StatusInvalidClientCertificate: "a valid client certificate is required to access this page",
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusText returns a text for the HTTP status code. It returns the empty
|
// StatusText returns a text for the HTTP status code. It returns http.StatusText if not found.
|
||||||
// string if the code is unknown.
|
|
||||||
func StatusText(code int) string {
|
func StatusText(code int) string {
|
||||||
return statusText[code]
|
txt, ok := statusText[code]
|
||||||
|
if ok {
|
||||||
|
return txt
|
||||||
|
}
|
||||||
|
return http.StatusText(code)
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,71 @@ func NewCriterionRule(
|
||||||
return r1
|
return r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCriterionDeviceRule generates a new rule for a criterion which
|
||||||
|
// requires a device and session. If there is no device "device-unauthenticated"
|
||||||
|
// is returned. If there is no session "user-unauthenticated" is returned.
|
||||||
|
func NewCriterionDeviceRule(
|
||||||
|
g *generator.Generator,
|
||||||
|
name string,
|
||||||
|
passReason, failReason Reason,
|
||||||
|
body ast.Body,
|
||||||
|
deviceType string,
|
||||||
|
) *ast.Rule {
|
||||||
|
r1 := g.NewRule(name)
|
||||||
|
|
||||||
|
additionalData := map[string]interface{}{
|
||||||
|
"device_type": deviceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedBody := ast.Body{
|
||||||
|
ast.Assign.Expr(ast.VarTerm("device_type_id"), ast.StringTerm(deviceType)),
|
||||||
|
ast.MustParseExpr(`session := get_session(input.session.id)`),
|
||||||
|
ast.MustParseExpr(`device_credential := get_device_credential(session, device_type_id)`),
|
||||||
|
ast.MustParseExpr(`device_enrollment := get_device_enrollment(device_credential)`),
|
||||||
|
}
|
||||||
|
|
||||||
|
// case 1: rule passes, session exists, device exists
|
||||||
|
r1.Head.Value = NewCriterionTermWithAdditionalData(true, passReason, additionalData)
|
||||||
|
r1.Body = append(sharedBody, body...)
|
||||||
|
|
||||||
|
// case 2: rule fails, session exists, device exists
|
||||||
|
r2 := &ast.Rule{
|
||||||
|
Head: &ast.Head{
|
||||||
|
Value: NewCriterionTermWithAdditionalData(false, failReason, additionalData),
|
||||||
|
},
|
||||||
|
Body: append(sharedBody, ast.Body{
|
||||||
|
ast.MustParseExpr(`session.id != ""`),
|
||||||
|
ast.MustParseExpr(`device_credential.id != ""`),
|
||||||
|
ast.MustParseExpr(`device_enrollment.id != ""`),
|
||||||
|
}...),
|
||||||
|
}
|
||||||
|
r1.Else = r2
|
||||||
|
|
||||||
|
// case 3: device not authenticated, session exists, device does not exist
|
||||||
|
r3 := &ast.Rule{
|
||||||
|
Head: &ast.Head{
|
||||||
|
Value: NewCriterionTermWithAdditionalData(false, ReasonDeviceUnauthenticated, additionalData),
|
||||||
|
},
|
||||||
|
Body: append(sharedBody, ast.Body{
|
||||||
|
ast.MustParseExpr(`session.id != ""`),
|
||||||
|
}...),
|
||||||
|
}
|
||||||
|
r2.Else = r3
|
||||||
|
|
||||||
|
// case 4: user not authenticated, session does not exist
|
||||||
|
r4 := &ast.Rule{
|
||||||
|
Head: &ast.Head{
|
||||||
|
Value: NewCriterionTermWithAdditionalData(false, ReasonUserUnauthenticated, additionalData),
|
||||||
|
},
|
||||||
|
Body: ast.Body{
|
||||||
|
ast.NewExpr(ast.BooleanTerm(true)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r3.Else = r4
|
||||||
|
|
||||||
|
return r1
|
||||||
|
}
|
||||||
|
|
||||||
// NewCriterionSessionRule generates a new rule for a criterion which
|
// NewCriterionSessionRule generates a new rule for a criterion which
|
||||||
// requires a session. If there is no session "user-unauthenticated"
|
// requires a session. If there is no session "user-unauthenticated"
|
||||||
// is returned.
|
// is returned.
|
||||||
|
|
106
pkg/policy/criteria/device.go
Normal file
106
pkg/policy/criteria/device.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/generator"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
deviceOperatorApproved = "approved"
|
||||||
|
deviceOperatorIs = "is"
|
||||||
|
deviceOperatorType = "type"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deviceOperatorLookup = map[string]struct{}{
|
||||||
|
deviceOperatorApproved: {},
|
||||||
|
deviceOperatorIs: {},
|
||||||
|
deviceOperatorType: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deviceCriterion) DataType() CriterionDataType {
|
||||||
|
return generator.CriterionDataTypeUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deviceCriterion) Name() string {
|
||||||
|
return "device"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c deviceCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
obj, ok := data.(parser.Object)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("expected object for device criterion, got: %T", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range obj {
|
||||||
|
_, ok := deviceOperatorLookup[k]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("unexpected field in device criterion: %s", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body ast.Body
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case obj.Truthy(deviceOperatorApproved):
|
||||||
|
// must be approved
|
||||||
|
body = append(body, ast.Body{
|
||||||
|
ast.MustParseExpr(`count([x|x:=device_enrollment.approved_by]) > 0`),
|
||||||
|
}...)
|
||||||
|
case obj.Falsy(deviceOperatorApproved):
|
||||||
|
// must *not* be approved
|
||||||
|
body = append(body, ast.Body{
|
||||||
|
ast.MustParseExpr(`count([x|x:=device_enrollment.approved_by]) == 0`),
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := obj[deviceOperatorIs]; ok {
|
||||||
|
s, ok := v.(parser.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("expected string for device criterion is operator, got %T", v)
|
||||||
|
}
|
||||||
|
body = append(body, ast.Body{
|
||||||
|
ast.Assign.Expr(ast.VarTerm("is_expect"), ast.StringTerm(string(s))),
|
||||||
|
ast.MustParseExpr(`is_expect == device_credential.id`),
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceType := "default"
|
||||||
|
if v, ok := obj[deviceOperatorType]; ok {
|
||||||
|
s, ok := v.(parser.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("expected string for device criterion type operator, got %T", v)
|
||||||
|
}
|
||||||
|
deviceType = string(s)
|
||||||
|
body = append(body, ast.Body{
|
||||||
|
ast.MustParseExpr(`device_credential.id != ""`),
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := NewCriterionDeviceRule(c.g, c.Name(),
|
||||||
|
ReasonDeviceOK, ReasonDeviceUnauthorized,
|
||||||
|
body, deviceType)
|
||||||
|
return rule, []*ast.Rule{
|
||||||
|
rules.GetDeviceCredential(),
|
||||||
|
rules.GetDeviceEnrollment(),
|
||||||
|
rules.GetSession(),
|
||||||
|
rules.ObjectGet(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device returns a Criterion based on the User's device state.
|
||||||
|
func Device(generator *Generator) Criterion {
|
||||||
|
return deviceCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(Device)
|
||||||
|
}
|
168
pkg/policy/criteria/device_test.go
Normal file
168
pkg/policy/criteria/device_test.go
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/device"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDevice(t *testing.T) {
|
||||||
|
mkDeviceSession := func(sessionID, deviceType, deviceCredentialID string) *session.Session {
|
||||||
|
return &session.Session{
|
||||||
|
Id: sessionID,
|
||||||
|
DeviceCredentials: []*session.Session_DeviceCredential{
|
||||||
|
{TypeId: deviceType, Credential: &session.Session_DeviceCredential_Id{Id: deviceCredentialID}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("no session", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
is: dc1
|
||||||
|
`, []dataBrokerRecord{}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{false, A{ReasonUserUnauthenticated}, M{"device_type": "default"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("no device credential", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
is: dc1
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "default", "dc1"),
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{false, A{ReasonDeviceUnauthenticated}, M{"device_type": "default"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("allowed by is", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
is: dc1
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "default", "dc1"),
|
||||||
|
&device.Credential{Id: "dc1", EnrollmentId: "de1"},
|
||||||
|
&device.Enrollment{Id: "de1"},
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{true, A{ReasonDeviceOK}, M{"device_type": "default"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("not allowed by is", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
is: dc2
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "default", "dc1"),
|
||||||
|
&device.Credential{Id: "dc1", EnrollmentId: "de1"},
|
||||||
|
&device.Enrollment{Id: "de1"},
|
||||||
|
&device.Credential{Id: "dc2", EnrollmentId: "de2"},
|
||||||
|
&device.Enrollment{Id: "de2"},
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{false, A{ReasonDeviceUnauthorized}, M{"device_type": "default"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("allowed by approved", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
approved: true
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "default", "dc1"),
|
||||||
|
&device.Credential{Id: "dc1", EnrollmentId: "de1"},
|
||||||
|
&device.Enrollment{Id: "de1", ApprovedBy: "u1"},
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{true, A{ReasonDeviceOK}, M{"device_type": "default"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("not allowed by approved", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
approved: true
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "default", "dc1"),
|
||||||
|
&device.Credential{Id: "dc1", EnrollmentId: "de1"},
|
||||||
|
&device.Enrollment{Id: "de1"},
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{false, A{ReasonDeviceUnauthorized}, M{"device_type": "default"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("allowed by not approved", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
approved: false
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "default", "dc1"),
|
||||||
|
&device.Credential{Id: "dc1", EnrollmentId: "de1"},
|
||||||
|
&device.Enrollment{Id: "de1"},
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{true, A{ReasonDeviceOK}, M{"device_type": "default"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("not allowed by not approved", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
approved: false
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "default", "dc1"),
|
||||||
|
&device.Credential{Id: "dc1", EnrollmentId: "de1"},
|
||||||
|
&device.Enrollment{Id: "de1", ApprovedBy: "u1"},
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{false, A{ReasonDeviceUnauthorized}, M{"device_type": "default"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("allowed by type", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
type: t1
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "t1", "dc1"),
|
||||||
|
&device.Credential{Id: "dc1", EnrollmentId: "de1", TypeId: "t1"},
|
||||||
|
&device.Enrollment{Id: "de1", ApprovedBy: "u1"},
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{true, A{ReasonDeviceOK}, M{"device_type": "t1"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("not allowed by type", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- device:
|
||||||
|
type: t2
|
||||||
|
`, []dataBrokerRecord{
|
||||||
|
mkDeviceSession("s1", "t1", "dc1"),
|
||||||
|
&device.Credential{Id: "dc1", EnrollmentId: "de1", TypeId: "t1"},
|
||||||
|
&device.Enrollment{Id: "de1", ApprovedBy: "u1"},
|
||||||
|
}, Input{Session: InputSession{ID: "s1"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{false, A{ReasonDeviceUnauthenticated}, M{"device_type": "t2"}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
|
@ -49,6 +49,31 @@ get_user_email(session, user) = v {
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDeviceCredential gets the device credential for the given session.
|
||||||
|
func GetDeviceCredential() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
get_device_credential(session, device_type_id) = v {
|
||||||
|
device_credential_id := [x.Credential.Id|x:=session.device_credentials[_];x.type_id==device_type_id][0]
|
||||||
|
v = get_databroker_record("type.googleapis.com/pomerium.device.Credential", device_credential_id)
|
||||||
|
v != null
|
||||||
|
} else = {} {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeviceEnrollment gets the device enrollment for the given device credential.
|
||||||
|
func GetDeviceEnrollment() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
get_device_enrollment(device_credential) = v {
|
||||||
|
v = get_databroker_record("type.googleapis.com/pomerium.device.Enrollment", device_credential.enrollment_id)
|
||||||
|
v != null
|
||||||
|
} else = {} {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDirectoryUser returns the directory user for the given session.
|
// GetDirectoryUser returns the directory user for the given session.
|
||||||
func GetDirectoryUser() *ast.Rule {
|
func GetDirectoryUser() *ast.Rule {
|
||||||
return ast.MustParseRule(`
|
return ast.MustParseRule(`
|
||||||
|
|
Loading…
Add table
Reference in a new issue