pomerium/pkg/policy/criteria/criteria.go
Caleb Doxsey 3497c39b9b
authorize: add support for webauthn device policy enforcement (#2700)
* authorize: add support for webauthn device policy enforcement

* update docs

* group statuses
2021-10-25 09:41:03 -06:00

214 lines
5.7 KiB
Go

// Package criteria contains all the pre-defined criteria as well as a registry to add new criteria.
package criteria
import (
"sync"
"github.com/open-policy-agent/opa/ast"
"github.com/pomerium/pomerium/pkg/policy/generator"
)
// re-exported types
type (
// A Generator generates a rego script from a policy.
Generator = generator.Generator
// A Criterion generates rego rules based on data.
Criterion = generator.Criterion
// A CriterionConstructor is a function which returns a Criterion for a Generator.
CriterionConstructor = generator.CriterionConstructor
// The CriterionDataType indicates the expected type of data for the criterion.
CriterionDataType = generator.CriterionDataType
)
var allCriteria struct {
sync.Mutex
a []CriterionConstructor
}
// All returns all the known criterion constructors.
func All() []CriterionConstructor {
allCriteria.Lock()
a := allCriteria.a
allCriteria.Unlock()
return a
}
// Register registers a criterion.
func Register(criterionConstructor CriterionConstructor) {
allCriteria.Lock()
a := make([]CriterionConstructor, 0, len(allCriteria.a)+1)
a = append(a, allCriteria.a...)
a = append(a, criterionConstructor)
allCriteria.a = a
allCriteria.Unlock()
}
const (
// CriterionDataTypeStringListMatcher indicates the expected data type is a string list matcher.
CriterionDataTypeStringListMatcher CriterionDataType = "string_list_matcher"
// CriterionDataTypeStringMatcher indicates the expected data type is a string matcher.
CriterionDataTypeStringMatcher CriterionDataType = "string_matcher"
)
// NewCriterionRule generates a new rule for a criterion.
func NewCriterionRule(
g *generator.Generator,
name string,
passReason, failReason Reason,
body ast.Body,
) *ast.Rule {
r1 := g.NewRule(name)
r1.Head.Value = NewCriterionTerm(true, passReason)
r1.Body = body
r2 := &ast.Rule{
Head: &ast.Head{
Value: NewCriterionTerm(false, failReason),
},
Body: ast.Body{
ast.NewExpr(ast.BooleanTerm(true)),
},
}
r1.Else = r2
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.
func NewCriterionSessionRule(
g *generator.Generator,
name string,
passReason, failReason Reason,
body ast.Body,
) *ast.Rule {
r1 := g.NewRule(name)
r1.Head.Value = NewCriterionTerm(true, passReason)
r1.Body = body
r2 := &ast.Rule{
Head: &ast.Head{
Value: NewCriterionTerm(false, failReason),
},
Body: ast.Body{
ast.MustParseExpr(`session := get_session(input.session.id)`),
ast.MustParseExpr(`session.id != ""`),
},
}
r1.Else = r2
r3 := &ast.Rule{
Head: &ast.Head{
Value: NewCriterionTerm(false, ReasonUserUnauthenticated),
},
Body: ast.Body{
ast.NewExpr(ast.BooleanTerm(true)),
},
}
r2.Else = r3
return r1
}
// NewCriterionTerm creates a new rego term for a criterion:
//
// [true, {"reason"}]
//
func NewCriterionTerm(value bool, reasons ...Reason) *ast.Term {
var terms []*ast.Term
for _, r := range reasons {
terms = append(terms, ast.StringTerm(string(r)))
}
return ast.ArrayTerm(
ast.BooleanTerm(value),
ast.SetTerm(terms...),
)
}
// NewCriterionTermWithAdditionalData creates a new rego term for a criterion with additional data:
//
// [true, {"reason"}, {"key": "value"}]
//
func NewCriterionTermWithAdditionalData(value bool, reason Reason, additionalData map[string]interface{}) *ast.Term {
var kvs [][2]*ast.Term
for k, v := range additionalData {
kvs = append(kvs, [2]*ast.Term{
ast.StringTerm(k),
ast.NewTerm(ast.MustInterfaceToValue(v)),
})
}
var terms []*ast.Term
terms = append(terms, ast.StringTerm(string(reason)))
return ast.ArrayTerm(
ast.BooleanTerm(value),
ast.SetTerm(terms...),
ast.ObjectTerm(kvs...),
)
}