mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 18:36:30 +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 {
|
||||
case result.Deny.Reasons.Has(criteria.ReasonRouteNotFound):
|
||||
denyStatusCode = http.StatusNotFound
|
||||
denyStatusText = http.StatusText(http.StatusNotFound)
|
||||
denyStatusText = httputil.DetailsText(http.StatusNotFound)
|
||||
case result.Deny.Reasons.Has(criteria.ReasonInvalidClientCertificate):
|
||||
denyStatusCode = httputil.StatusInvalidClientCertificate
|
||||
denyStatusText = httputil.StatusText(httputil.StatusInvalidClientCertificate)
|
||||
denyStatusText = httputil.DetailsText(httputil.StatusInvalidClientCertificate)
|
||||
}
|
||||
|
||||
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
|
||||
// logged in yet, so redirect to authenticate
|
||||
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 {
|
||||
|
@ -94,16 +103,6 @@ func (a *Authorize) deniedResponse(
|
|||
in *envoy_service_auth_v3.CheckRequest,
|
||||
code int32, reason string, headers map[string]string,
|
||||
) (*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
|
||||
w := httptest.NewRecorder()
|
||||
r := getHTTPRequestFromCheckRequest(in)
|
||||
|
@ -114,7 +113,7 @@ func (a *Authorize) deniedResponse(
|
|||
// run the request through our go error handler
|
||||
httpErr := httputil.HTTPError{
|
||||
Status: int(code),
|
||||
Err: errors.New(details),
|
||||
Err: errors.New(reason),
|
||||
DebugURL: debugEndpoint,
|
||||
RequestID: requestid.FromContext(ctx),
|
||||
}
|
||||
|
@ -172,10 +171,50 @@ func (a *Authorize) requireLoginResponse(
|
|||
q := signinURL.Query()
|
||||
|
||||
// always assume https scheme
|
||||
url := getCheckRequestURL(in)
|
||||
url.Scheme = "https"
|
||||
checkRequestURL := getCheckRequestURL(in)
|
||||
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()
|
||||
redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String()
|
||||
|
||||
|
|
|
@ -175,7 +175,8 @@ func TestRequireLogin(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
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.
|
||||
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.
|
||||
|
@ -55,7 +55,7 @@ func (e *HTTPError) ErrorResponse(w http.ResponseWriter, r *http.Request) {
|
|||
DebugURL *url.URL `json:",omitempty"`
|
||||
}{
|
||||
Status: e.Status,
|
||||
StatusText: http.StatusText(e.Status),
|
||||
StatusText: StatusText(e.Status),
|
||||
Error: e.Error(),
|
||||
RequestID: reqID,
|
||||
CanDebug: e.Status/100 == 4 && (e.DebugURL != nil || reqID != ""),
|
||||
|
|
|
@ -1,16 +1,41 @@
|
|||
package httputil
|
||||
|
||||
// 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.
|
||||
const StatusInvalidClientCertificate = 495
|
||||
import "net/http"
|
||||
|
||||
const (
|
||||
// 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{
|
||||
StatusDeviceUnauthorized: "device not authorized",
|
||||
StatusInvalidClientCertificate: "a valid client certificate is required to access this page",
|
||||
}
|
||||
|
||||
// StatusText returns a text for the HTTP status code. It returns the empty
|
||||
// string if the code is unknown.
|
||||
// StatusText returns a text for the HTTP status code. It returns http.StatusText if not found.
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// requires a session. If there is no session "user-unauthenticated"
|
||||
// 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.
|
||||
func GetDirectoryUser() *ast.Rule {
|
||||
return ast.MustParseRule(`
|
||||
|
|
Loading…
Add table
Reference in a new issue