mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-19 12:07:18 +02:00
Pomerium Policy Language (#2202)
* policy: add parser and generator for Pomerium Policy Language * add criteria * add additional criteria
This commit is contained in:
parent
9fe941ccee
commit
e138054cb9
33 changed files with 2758 additions and 0 deletions
35
pkg/policy/criteria/accept.go
Normal file
35
pkg/policy/criteria/accept.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var acceptBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`v := true`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type acceptCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acceptCriterion) Names() []string {
|
||||||
|
return []string{"accept"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c acceptCriterion) GenerateRule(_ string, _ parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
rule := c.g.NewRule("accept")
|
||||||
|
rule.Head.Value = ast.VarTerm("v")
|
||||||
|
rule.Body = acceptBody
|
||||||
|
return rule, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept returns a Criterion which always returns true.
|
||||||
|
func Accept(generator *Generator) Criterion {
|
||||||
|
return acceptCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(Accept)
|
||||||
|
}
|
18
pkg/policy/criteria/accept_test.go
Normal file
18
pkg/policy/criteria/accept_test.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccept(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- accept: 1
|
||||||
|
`, []dataBrokerRecord{}, Input{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
}
|
37
pkg/policy/criteria/authenticated_user.go
Normal file
37
pkg/policy/criteria/authenticated_user.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
var authenticatedUserBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`session := get_session(input.session.id)`),
|
||||||
|
ast.MustParseExpr(`session.user_id != null`),
|
||||||
|
ast.MustParseExpr(`session.user_id != ""`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticatedUserCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (authenticatedUserCriterion) Names() []string {
|
||||||
|
return []string{"authenticated_user"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c authenticatedUserCriterion) GenerateRule(_ string, _ parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
rule := c.g.NewRule("authenticated_user")
|
||||||
|
rule.Body = authenticatedUserBody
|
||||||
|
return rule, []*ast.Rule{rules.GetSession()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticatedUser returns a Criterion which returns true if the current user is logged in.
|
||||||
|
func AuthenticatedUser(generator *Generator) Criterion {
|
||||||
|
return authenticatedUserCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(AuthenticatedUser)
|
||||||
|
}
|
39
pkg/policy/criteria/authenticated_user_test.go
Normal file
39
pkg/policy/criteria/authenticated_user_test.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthenticatedUser(t *testing.T) {
|
||||||
|
t.Run("no session", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- authenticated_user: 1
|
||||||
|
`, []dataBrokerRecord{}, Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by domain", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- authenticated_user: 1
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
64
pkg/policy/criteria/claims.go
Normal file
64
pkg/policy/criteria/claims.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
var claimsBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
session := get_session(input.session.id)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
session_claims := object.get(session, "claims", {})
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
user := get_user(session)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
user_claims := object.get(user, "claims", {})
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
all_claims := object.union(session_claims, user_claims)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
values := object_get(all_claims, rule_path, [])
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
rule_data == values[_]
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type claimsCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (claimsCriterion) Names() []string {
|
||||||
|
return []string{"claim", "claims"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c claimsCriterion) GenerateRule(subPath string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
r := c.g.NewRule("claims")
|
||||||
|
r.Body = append(r.Body,
|
||||||
|
ast.Assign.Expr(ast.VarTerm("rule_data"), ast.NewTerm(data.RegoValue())),
|
||||||
|
ast.Assign.Expr(ast.VarTerm("rule_path"), ast.NewTerm(ast.MustInterfaceToValue(subPath))),
|
||||||
|
)
|
||||||
|
r.Body = append(r.Body, claimsBody...)
|
||||||
|
|
||||||
|
return r, []*ast.Rule{
|
||||||
|
rules.GetSession(),
|
||||||
|
rules.GetUser(),
|
||||||
|
rules.ObjectGet(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claims returns a Criterion on allowed IDP claims.
|
||||||
|
func Claims(generator *Generator) Criterion {
|
||||||
|
return claimsCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(Claims)
|
||||||
|
}
|
72
pkg/policy/criteria/claims_test.go
Normal file
72
pkg/policy/criteria/claims_test.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClaims(t *testing.T) {
|
||||||
|
t.Run("no session", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- claim/family_name: Smith
|
||||||
|
`, []dataBrokerRecord{}, Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by session claim", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- claim/family_name: Smith
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
Claims: map[string]*structpb.ListValue{
|
||||||
|
"family_name": {Values: []*structpb.Value{structpb.NewStringValue("Smith")}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
Email: "test@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by user claim", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- claim/family_name: Smith
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Claims: map[string]*structpb.ListValue{
|
||||||
|
"family_name": {Values: []*structpb.Value{structpb.NewStringValue("Smith")}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
36
pkg/policy/criteria/cors_preflight.go
Normal file
36
pkg/policy/criteria/cors_preflight.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var corsPreflightBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`input.http.method == "OPTIONS"`),
|
||||||
|
ast.MustParseExpr(`count(object.get(input.http.headers, "Access-Control-Request-Method", [])) > 0`),
|
||||||
|
ast.MustParseExpr(`count(object.get(input.http.headers, "Origin", [])) > 0`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type corsPreflightCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (corsPreflightCriterion) Names() []string {
|
||||||
|
return []string{"cors_preflight"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c corsPreflightCriterion) GenerateRule(_ string, _ parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
rule := c.g.NewRule("cors_preflight")
|
||||||
|
rule.Body = corsPreflightBody
|
||||||
|
return rule, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORSPreflight returns a Criterion which returns true if the input request is a CORS preflight request.
|
||||||
|
func CORSPreflight(generator *Generator) Criterion {
|
||||||
|
return corsPreflightCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(CORSPreflight)
|
||||||
|
}
|
38
pkg/policy/criteria/cors_preflight_test.go
Normal file
38
pkg/policy/criteria/cors_preflight_test.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCORSPreflight(t *testing.T) {
|
||||||
|
t.Run("true", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- cors_preflight: 1
|
||||||
|
`, []dataBrokerRecord{}, Input{HTTP: InputHTTP{
|
||||||
|
Method: "OPTIONS",
|
||||||
|
Headers: map[string][]string{
|
||||||
|
"Access-Control-Request-Method": {"GET"},
|
||||||
|
"Origin": {"example.com"},
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("false", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- cors_preflight: 1
|
||||||
|
`, []dataBrokerRecord{}, Input{HTTP: InputHTTP{
|
||||||
|
Method: "OPTIONS",
|
||||||
|
}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
41
pkg/policy/criteria/criteria.go
Normal file
41
pkg/policy/criteria/criteria.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// Package criteria contains all the pre-defined criteria as well as a registry to add new criteria.
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
137
pkg/policy/criteria/criteria_test.go
Normal file
137
pkg/policy/criteria/criteria_test.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
"github.com/open-policy-agent/opa/format"
|
||||||
|
"github.com/open-policy-agent/opa/rego"
|
||||||
|
"github.com/open-policy-agent/opa/types"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/generator"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testingNow = time.Date(2021, 5, 11, 13, 43, 0, 0, time.Local)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Input struct {
|
||||||
|
HTTP InputHTTP `json:"http"`
|
||||||
|
Session InputSession `json:"session"`
|
||||||
|
}
|
||||||
|
InputHTTP struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Headers map[string][]string `json:"headers"`
|
||||||
|
}
|
||||||
|
InputSession struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateRegoFromYAML(raw string) (string, error) {
|
||||||
|
var options []generator.Option
|
||||||
|
for _, newMatcher := range All() {
|
||||||
|
options = append(options, generator.WithCriterion(newMatcher))
|
||||||
|
}
|
||||||
|
|
||||||
|
g := generator.New(options...)
|
||||||
|
p := parser.New()
|
||||||
|
policy, err := p.ParseYAML(strings.NewReader(raw))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
m, err := g.Generate(policy)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
bs, err := format.Ast(m)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataBrokerRecord interface {
|
||||||
|
proto.Message
|
||||||
|
GetId() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(t *testing.T,
|
||||||
|
rawPolicy string,
|
||||||
|
dataBrokerRecords []dataBrokerRecord,
|
||||||
|
input Input,
|
||||||
|
) (rego.Vars, error) {
|
||||||
|
regoPolicy, err := generateRegoFromYAML(rawPolicy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing policy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := rego.New(
|
||||||
|
rego.Module("policy.rego", regoPolicy),
|
||||||
|
rego.Query("result = data.pomerium.policy"),
|
||||||
|
rego.Function2(®o.Function{
|
||||||
|
Name: "get_databroker_record",
|
||||||
|
Decl: types.NewFunction([]types.Type{
|
||||||
|
types.S, types.S,
|
||||||
|
}, types.A),
|
||||||
|
}, func(bctx rego.BuiltinContext, op1, op2 *ast.Term) (*ast.Term, error) {
|
||||||
|
recordType, ok := op1.Value.(ast.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid type for record_type: %T", op1)
|
||||||
|
}
|
||||||
|
|
||||||
|
recordID, ok := op2.Value.(ast.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid type for record_id: %T", op2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range dataBrokerRecords {
|
||||||
|
any, err := anypb.New(record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(recordType) == any.GetTypeUrl() &&
|
||||||
|
string(recordID) == record.GetId() {
|
||||||
|
bs, _ := json.Marshal(record)
|
||||||
|
v, err := ast.ValueFromReader(bytes.NewReader(bs))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ast.NewTerm(v), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}),
|
||||||
|
rego.Input(input),
|
||||||
|
)
|
||||||
|
preparedQuery, err := r.PrepareForEval(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Log("source:", regoPolicy)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resultSet, err := preparedQuery.Eval(context.Background(),
|
||||||
|
// set the eval time so we get a consistent result
|
||||||
|
rego.EvalTime(testingNow))
|
||||||
|
if err != nil {
|
||||||
|
t.Log("source:", regoPolicy)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(resultSet) == 0 {
|
||||||
|
return make(rego.Vars), nil
|
||||||
|
}
|
||||||
|
vars, ok := resultSet[0].Bindings["result"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return make(rego.Vars), nil
|
||||||
|
}
|
||||||
|
return vars, nil
|
||||||
|
}
|
58
pkg/policy/criteria/domains.go
Normal file
58
pkg/policy/criteria/domains.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
var domainsBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
session := get_session(input.session.id)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
user := get_user(session)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
domain := split(get_user_email(session, user), "@")[1]
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type domainsCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (domainsCriterion) Names() []string {
|
||||||
|
return []string{"domain", "domains"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c domainsCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
r := c.g.NewRule("domains")
|
||||||
|
r.Body = append(r.Body, ast.Assign.Expr(ast.VarTerm("rule_data"), ast.NewTerm(data.RegoValue())))
|
||||||
|
r.Body = append(r.Body, domainsBody...)
|
||||||
|
|
||||||
|
switch data.(type) {
|
||||||
|
case parser.String:
|
||||||
|
r.Body = append(r.Body, ast.MustParseExpr(`domain = rule_data`))
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported value type: %T", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, []*ast.Rule{
|
||||||
|
rules.GetSession(),
|
||||||
|
rules.GetUser(),
|
||||||
|
rules.GetUserEmail(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domains returns a Criterion on a user's email address domain.
|
||||||
|
func Domains(generator *Generator) Criterion {
|
||||||
|
return domainsCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(Domains)
|
||||||
|
}
|
67
pkg/policy/criteria/domains_test.go
Normal file
67
pkg/policy/criteria/domains_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDomains(t *testing.T) {
|
||||||
|
t.Run("no session", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- domain: example.com
|
||||||
|
`, []dataBrokerRecord{}, Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by domain", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- domain: example.com
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
Email: "test@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by impersonate email", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- domain: example.com
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
ImpersonateEmail: proto.String("test2@example.com"),
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
Email: "test1@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
58
pkg/policy/criteria/emails.go
Normal file
58
pkg/policy/criteria/emails.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
var emailsBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
session := get_session(input.session.id)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
user := get_user(session)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
email := get_user_email(session, user)
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type emailsCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (emailsCriterion) Names() []string {
|
||||||
|
return []string{"email", "emails"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c emailsCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
r := c.g.NewRule("emails")
|
||||||
|
r.Body = append(r.Body, ast.Assign.Expr(ast.VarTerm("rule_data"), ast.NewTerm(data.RegoValue())))
|
||||||
|
r.Body = append(r.Body, emailsBody...)
|
||||||
|
|
||||||
|
switch data.(type) {
|
||||||
|
case parser.String:
|
||||||
|
r.Body = append(r.Body, ast.MustParseExpr(`email = rule_data`))
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported value type: %T", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, []*ast.Rule{
|
||||||
|
rules.GetSession(),
|
||||||
|
rules.GetUser(),
|
||||||
|
rules.GetUserEmail(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emails returns a Criterion on a user's email address.
|
||||||
|
func Emails(generator *Generator) Criterion {
|
||||||
|
return emailsCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(Emails)
|
||||||
|
}
|
67
pkg/policy/criteria/emails_test.go
Normal file
67
pkg/policy/criteria/emails_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmails(t *testing.T) {
|
||||||
|
t.Run("no session", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- email: test@example.com
|
||||||
|
`, []dataBrokerRecord{}, Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by email", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- email: test@example.com
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
Email: "test@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by impersonate email", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- email: test2@example.com
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
ImpersonateEmail: proto.String("test2@example.com"),
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
Email: "test1@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
84
pkg/policy/criteria/groups.go
Normal file
84
pkg/policy/criteria/groups.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
var groupsBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
session := get_session(input.session.id)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
directory_user := get_directory_user(session)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
group_ids := get_group_ids(session, directory_user)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
group_names := [directory_group.name |
|
||||||
|
some i
|
||||||
|
group_id := group_ids[i]
|
||||||
|
directory_group := get_directory_group(group_id)
|
||||||
|
directory_group != null
|
||||||
|
directory_group.name != null]
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
group_emails := [directory_group.email |
|
||||||
|
some i
|
||||||
|
group_id := group_ids[i]
|
||||||
|
directory_group := get_directory_group(group_id)
|
||||||
|
directory_group != null
|
||||||
|
directory_group.email != null]
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
groups = array.concat(group_ids, array.concat(group_names, group_emails))
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
some group
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
group = groups[_]
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupsCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (groupsCriterion) Names() []string {
|
||||||
|
return []string{"group", "groups"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c groupsCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
r := c.g.NewRule("groups")
|
||||||
|
r.Body = append(r.Body, ast.Assign.Expr(ast.VarTerm("rule_data"), ast.NewTerm(data.RegoValue())))
|
||||||
|
r.Body = append(r.Body, groupsBody...)
|
||||||
|
|
||||||
|
switch data.(type) {
|
||||||
|
case parser.String:
|
||||||
|
r.Body = append(r.Body, ast.MustParseExpr(`group = rule_data`))
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported value type: %T", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, []*ast.Rule{
|
||||||
|
rules.GetSession(),
|
||||||
|
rules.GetDirectoryUser(),
|
||||||
|
rules.GetDirectoryGroup(),
|
||||||
|
rules.GetGroupIDs(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups returns a Criterion on a user's group ids, names or emails.
|
||||||
|
func Groups(generator *Generator) Criterion {
|
||||||
|
return groupsCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(Groups)
|
||||||
|
}
|
95
pkg/policy/criteria/groups_test.go
Normal file
95
pkg/policy/criteria/groups_test.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGroups(t *testing.T) {
|
||||||
|
t.Run("no session", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- groups: group1
|
||||||
|
- groups: group2
|
||||||
|
`, []dataBrokerRecord{}, Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by id", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- groups: group1
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
},
|
||||||
|
&directory.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
GroupIds: []string{"group1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by email", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- groups: "group1@example.com"
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
},
|
||||||
|
&directory.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
GroupIds: []string{"group1"},
|
||||||
|
},
|
||||||
|
&directory.Group{
|
||||||
|
Id: "group1",
|
||||||
|
Email: "group1@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("by name", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- groups: "Group 1"
|
||||||
|
`,
|
||||||
|
[]dataBrokerRecord{
|
||||||
|
&session.Session{
|
||||||
|
Id: "SESSION_ID",
|
||||||
|
UserId: "USER_ID",
|
||||||
|
},
|
||||||
|
&directory.User{
|
||||||
|
Id: "USER_ID",
|
||||||
|
GroupIds: []string{"group1"},
|
||||||
|
},
|
||||||
|
&directory.Group{
|
||||||
|
Id: "group1",
|
||||||
|
Name: "Group 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input{Session: InputSession{ID: "SESSION_ID"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
37
pkg/policy/criteria/invalid_client_certificate.go
Normal file
37
pkg/policy/criteria/invalid_client_certificate.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var invalidClientCertificateBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`reason = [495, "invalid client certificate"]`),
|
||||||
|
ast.MustParseExpr(`is_boolean(input.is_valid_client_certificate)`),
|
||||||
|
ast.MustParseExpr(`not input.is_valid_client_certificate`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type invalidClientCertificateCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (invalidClientCertificateCriterion) Names() []string {
|
||||||
|
return []string{"invalid_client_certificate"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c invalidClientCertificateCriterion) GenerateRule(_ string, _ parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
rule := c.g.NewRule("invalid_client_certificate")
|
||||||
|
rule.Head.Value = ast.VarTerm("reason")
|
||||||
|
rule.Body = invalidClientCertificateBody
|
||||||
|
return rule, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidClientCertificate returns a Criterion which returns true if the client certificate is valid.
|
||||||
|
func InvalidClientCertificate(generator *Generator) Criterion {
|
||||||
|
return invalidClientCertificateCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(InvalidClientCertificate)
|
||||||
|
}
|
37
pkg/policy/criteria/pomerium_routes.go
Normal file
37
pkg/policy/criteria/pomerium_routes.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pomeriumRoutesBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
contains(input.http.url, "/.pomerium/")
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type pomeriumRoutesCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pomeriumRoutesCriterion) Names() []string {
|
||||||
|
return []string{"pomerium_routes"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c pomeriumRoutesCriterion) GenerateRule(_ string, _ parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
r := c.g.NewRule("pomerium_routes")
|
||||||
|
r.Body = append(r.Body, pomeriumRoutesBody...)
|
||||||
|
|
||||||
|
return r, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PomeriumRoutes returns a Criterion on that allows access to pomerium routes.
|
||||||
|
func PomeriumRoutes(generator *Generator) Criterion {
|
||||||
|
return pomeriumRoutesCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(PomeriumRoutes)
|
||||||
|
}
|
35
pkg/policy/criteria/reject.go
Normal file
35
pkg/policy/criteria/reject.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rejectBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`v := false`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type rejectMatcher struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rejectMatcher) Names() []string {
|
||||||
|
return []string{"reject"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m rejectMatcher) GenerateRule(_ string, _ parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
rule := m.g.NewRule("reject")
|
||||||
|
rule.Head.Value = ast.VarTerm("v")
|
||||||
|
rule.Body = rejectBody
|
||||||
|
return rule, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject returns a Criterion which always returns false.
|
||||||
|
func Reject(generator *Generator) Criterion {
|
||||||
|
return rejectMatcher{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(Reject)
|
||||||
|
}
|
18
pkg/policy/criteria/reject_test.go
Normal file
18
pkg/policy/criteria/reject_test.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReject(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- reject: 1
|
||||||
|
`, []dataBrokerRecord{}, Input{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, res["allow"])
|
||||||
|
require.Equal(t, false, res["deny"])
|
||||||
|
}
|
55
pkg/policy/criteria/users.go
Normal file
55
pkg/policy/criteria/users.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
var usersBody = ast.Body{
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
session := get_session(input.session.id)
|
||||||
|
`),
|
||||||
|
ast.MustParseExpr(`
|
||||||
|
user := get_user(session)
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
type usersCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (usersCriterion) Names() []string {
|
||||||
|
return []string{"user", "users"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c usersCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
r := c.g.NewRule("users")
|
||||||
|
r.Body = append(r.Body, ast.Assign.Expr(ast.VarTerm("rule_data"), ast.NewTerm(data.RegoValue())))
|
||||||
|
r.Body = append(r.Body, usersBody...)
|
||||||
|
|
||||||
|
switch data.(type) {
|
||||||
|
case parser.String:
|
||||||
|
r.Body = append(r.Body, ast.MustParseExpr(`user_id = rule_data`))
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported value type: %T", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, []*ast.Rule{
|
||||||
|
rules.GetSession(),
|
||||||
|
rules.GetUser(),
|
||||||
|
rules.GetUserEmail(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserIDs returns a Criterion on a user's id.
|
||||||
|
func UserIDs(generator *Generator) Criterion {
|
||||||
|
return usersCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(UserIDs)
|
||||||
|
}
|
132
pkg/policy/generator/conditionals.go
Normal file
132
pkg/policy/generator/conditionals.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type conditionalGenerator func(dst *ast.RuleSet, policyCriteria []parser.Criterion) (*ast.Rule, error)
|
||||||
|
|
||||||
|
func (g *Generator) generateAndRule(dst *ast.RuleSet, policyCriteria []parser.Criterion) (*ast.Rule, error) {
|
||||||
|
rule := g.NewRule("and")
|
||||||
|
|
||||||
|
if len(policyCriteria) == 0 {
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expressions, err := g.generateCriterionRules(dst, policyCriteria)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
g.fillViaAnd(rule, expressions)
|
||||||
|
dst.Add(rule)
|
||||||
|
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) generateNotRule(dst *ast.RuleSet, policyCriteria []parser.Criterion) (*ast.Rule, error) {
|
||||||
|
rule := g.NewRule("not")
|
||||||
|
|
||||||
|
if len(policyCriteria) == 0 {
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOT => (NOT A) AND (NOT B)
|
||||||
|
|
||||||
|
expressions, err := g.generateCriterionRules(dst, policyCriteria)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, expr := range expressions {
|
||||||
|
expr.Negated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
g.fillViaAnd(rule, expressions)
|
||||||
|
dst.Add(rule)
|
||||||
|
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) generateOrRule(dst *ast.RuleSet, policyCriteria []parser.Criterion) (*ast.Rule, error) {
|
||||||
|
rule := g.NewRule("or")
|
||||||
|
|
||||||
|
if len(policyCriteria) == 0 {
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expressions, err := g.generateCriterionRules(dst, policyCriteria)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
g.fillViaOr(rule, expressions)
|
||||||
|
dst.Add(rule)
|
||||||
|
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) generateNorRule(dst *ast.RuleSet, policyCriteria []parser.Criterion) (*ast.Rule, error) {
|
||||||
|
rule := g.NewRule("nor")
|
||||||
|
|
||||||
|
if len(policyCriteria) == 0 {
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOR => (NOT A) OR (NOT B)
|
||||||
|
|
||||||
|
expressions, err := g.generateCriterionRules(dst, policyCriteria)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, expr := range expressions {
|
||||||
|
expr.Negated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
g.fillViaOr(rule, expressions)
|
||||||
|
dst.Add(rule)
|
||||||
|
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) generateCriterionRules(dst *ast.RuleSet, policyCriteria []parser.Criterion) ([]*ast.Expr, error) {
|
||||||
|
var expressions []*ast.Expr
|
||||||
|
for _, policyCriterion := range policyCriteria {
|
||||||
|
criterion, ok := g.criteria[policyCriterion.Name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown policy criterion: %s", policyCriterion.Name)
|
||||||
|
}
|
||||||
|
mainRule, additionalRules, err := criterion.GenerateRule(policyCriterion.SubPath, policyCriterion.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating criterion rules: %w", err)
|
||||||
|
}
|
||||||
|
*dst = dst.Merge(additionalRules)
|
||||||
|
dst.Add(mainRule)
|
||||||
|
|
||||||
|
expr := ast.NewExpr(ast.VarTerm(string(mainRule.Head.Name)))
|
||||||
|
expressions = append(expressions, expr)
|
||||||
|
}
|
||||||
|
return expressions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) fillViaAnd(rule *ast.Rule, expressions []*ast.Expr) {
|
||||||
|
for _, expr := range expressions {
|
||||||
|
rule.Body = append(rule.Body, expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) fillViaOr(rule *ast.Rule, expressions []*ast.Expr) {
|
||||||
|
currentRule := rule
|
||||||
|
for i, expr := range expressions {
|
||||||
|
if i > 0 {
|
||||||
|
currentRule.Else = &ast.Rule{
|
||||||
|
Head: &ast.Head{},
|
||||||
|
}
|
||||||
|
currentRule = currentRule.Else
|
||||||
|
}
|
||||||
|
currentRule.Body = ast.Body{expr}
|
||||||
|
}
|
||||||
|
}
|
43
pkg/policy/generator/criterion.go
Normal file
43
pkg/policy/generator/criterion.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Criterion generates rego rules based on data.
|
||||||
|
type Criterion interface {
|
||||||
|
Names() []string
|
||||||
|
GenerateRule(subPath string, data parser.Value) (rule *ast.Rule, additionalRules []*ast.Rule, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A CriterionConstructor is a function which returns a Criterion for a Generator.
|
||||||
|
type CriterionConstructor func(*Generator) Criterion
|
||||||
|
|
||||||
|
// A criterionFunc is a criterion implemented as a function and a list of names.
|
||||||
|
type criterionFunc struct {
|
||||||
|
names []string
|
||||||
|
generateRule func(subPath string, data parser.Value) (rule *ast.Rule, additionalRules []*ast.Rule, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names returns the names of the criterion.
|
||||||
|
func (c criterionFunc) Names() []string {
|
||||||
|
return c.names
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRule calls the underlying generateRule function.
|
||||||
|
func (c criterionFunc) GenerateRule(subPath string, data parser.Value) (rule *ast.Rule, additionalRules []*ast.Rule, err error) {
|
||||||
|
return c.generateRule(subPath, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCriterionFunc creates a new Criterion from a function.
|
||||||
|
func NewCriterionFunc(
|
||||||
|
names []string,
|
||||||
|
f func(subPath string, data parser.Value) (rule *ast.Rule, additionalRules []*ast.Rule, err error),
|
||||||
|
) Criterion {
|
||||||
|
return criterionFunc{
|
||||||
|
names: names,
|
||||||
|
generateRule: f,
|
||||||
|
}
|
||||||
|
}
|
104
pkg/policy/generator/generator.go
Normal file
104
pkg/policy/generator/generator.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
// Package generator converts Pomerium Policy Language into Rego.
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Generator generates a rego script from a policy.
|
||||||
|
type Generator struct {
|
||||||
|
ids map[string]int
|
||||||
|
criteria map[string]Criterion
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Option configures the Generator.
|
||||||
|
type Option func(*Generator)
|
||||||
|
|
||||||
|
// WithCriterion adds a Criterion to the generator's known criteria.
|
||||||
|
func WithCriterion(criterionConstructor CriterionConstructor) Option {
|
||||||
|
return func(g *Generator) {
|
||||||
|
c := criterionConstructor(g)
|
||||||
|
for _, name := range c.Names() {
|
||||||
|
g.criteria[name] = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Generator.
|
||||||
|
func New(options ...Option) *Generator {
|
||||||
|
g := &Generator{
|
||||||
|
ids: make(map[string]int),
|
||||||
|
criteria: make(map[string]Criterion),
|
||||||
|
}
|
||||||
|
for _, o := range options {
|
||||||
|
o(g)
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCriterion gets a Criterion for the given name.
|
||||||
|
func (g *Generator) GetCriterion(name string) (Criterion, bool) {
|
||||||
|
c, ok := g.criteria[name]
|
||||||
|
return c, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate generates the rego module from a policy.
|
||||||
|
func (g *Generator) Generate(policy *parser.Policy) (*ast.Module, error) {
|
||||||
|
rules := ast.NewRuleSet()
|
||||||
|
rules.Add(ast.MustParseRule(`default allow = false`))
|
||||||
|
rules.Add(ast.MustParseRule(`default deny = false`))
|
||||||
|
|
||||||
|
for _, policyRule := range policy.Rules {
|
||||||
|
rule := &ast.Rule{
|
||||||
|
Head: &ast.Head{Name: ast.Var(policyRule.Action)},
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []struct {
|
||||||
|
criteria []parser.Criterion
|
||||||
|
generator conditionalGenerator
|
||||||
|
}{
|
||||||
|
{policyRule.And, g.generateAndRule},
|
||||||
|
{policyRule.Or, g.generateOrRule},
|
||||||
|
{policyRule.Not, g.generateNotRule},
|
||||||
|
{policyRule.Nor, g.generateNorRule},
|
||||||
|
}
|
||||||
|
for _, field := range fields {
|
||||||
|
if len(field.criteria) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subRule, err := field.generator(&rules, field.criteria)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rule.Body = append(rule.Body, ast.NewExpr(ast.VarTerm(string(subRule.Head.Name))))
|
||||||
|
}
|
||||||
|
|
||||||
|
rules.Add(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ast.Module{
|
||||||
|
Package: &ast.Package{
|
||||||
|
Path: ast.Ref{
|
||||||
|
ast.StringTerm("policy.rego"),
|
||||||
|
ast.StringTerm("pomerium"),
|
||||||
|
ast.StringTerm("policy"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Rules: rules,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRule creates a new rule with a dynamically generated name.
|
||||||
|
func (g *Generator) NewRule(name string) *ast.Rule {
|
||||||
|
id := g.ids[name]
|
||||||
|
g.ids[name]++
|
||||||
|
return &ast.Rule{
|
||||||
|
Head: &ast.Head{
|
||||||
|
Name: ast.Var(fmt.Sprintf("%s_%d", name, id)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
147
pkg/policy/generator/generator_test.go
Normal file
147
pkg/policy/generator/generator_test.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
"github.com/open-policy-agent/opa/format"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(t *testing.T) {
|
||||||
|
g := New(WithCriterion(func(g *Generator) Criterion {
|
||||||
|
return NewCriterionFunc([]string{"accept"}, func(subPath string, data parser.Value) (rule *ast.Rule, additionalRules []*ast.Rule, err error) {
|
||||||
|
rule = g.NewRule("accept")
|
||||||
|
rule.Body = append(rule.Body, ast.MustParseExpr("1 == 1"))
|
||||||
|
return rule, nil, nil
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
mod, err := g.Generate(&parser.Policy{
|
||||||
|
Rules: []parser.Rule{
|
||||||
|
{
|
||||||
|
Action: parser.ActionAllow,
|
||||||
|
And: []parser.Criterion{
|
||||||
|
{Name: "accept"},
|
||||||
|
{Name: "accept"},
|
||||||
|
{Name: "accept"},
|
||||||
|
},
|
||||||
|
Or: []parser.Criterion{
|
||||||
|
{Name: "accept"},
|
||||||
|
{Name: "accept"},
|
||||||
|
{Name: "accept"},
|
||||||
|
},
|
||||||
|
Not: []parser.Criterion{
|
||||||
|
{Name: "accept"},
|
||||||
|
{Name: "accept"},
|
||||||
|
{Name: "accept"},
|
||||||
|
},
|
||||||
|
Nor: []parser.Criterion{
|
||||||
|
{Name: "accept"},
|
||||||
|
{Name: "accept"},
|
||||||
|
{Name: "accept"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `package pomerium.policy
|
||||||
|
|
||||||
|
default allow = false
|
||||||
|
|
||||||
|
default deny = false
|
||||||
|
|
||||||
|
accept_0 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_1 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_2 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
and_0 {
|
||||||
|
accept_0
|
||||||
|
accept_1
|
||||||
|
accept_2
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_3 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_4 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_5 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
or_0 {
|
||||||
|
accept_3
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
accept_4
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
accept_5
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_6 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_7 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_8 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
not_0 {
|
||||||
|
not accept_6
|
||||||
|
not accept_7
|
||||||
|
not accept_8
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_9 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_10 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accept_11 {
|
||||||
|
1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
nor_0 {
|
||||||
|
not accept_9
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
not accept_10
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
not accept_11
|
||||||
|
}
|
||||||
|
|
||||||
|
allow {
|
||||||
|
and_0
|
||||||
|
or_0
|
||||||
|
not_0
|
||||||
|
nor_0
|
||||||
|
}
|
||||||
|
`, string(format.MustAst(mod)))
|
||||||
|
}
|
308
pkg/policy/parser/grammar.go
Normal file
308
pkg/policy/parser/grammar.go
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Policy is a policy made up of multiple allow or deny rules.
|
||||||
|
type Policy struct {
|
||||||
|
Rules []Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the policy as JSON.
|
||||||
|
func (p *Policy) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(p.ToJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
// String converts the policy to a string.
|
||||||
|
func (p *Policy) String() string {
|
||||||
|
str, _ := p.MarshalJSON()
|
||||||
|
return string(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON converts the policy to JSON.
|
||||||
|
func (p *Policy) ToJSON() Value {
|
||||||
|
var root Array
|
||||||
|
|
||||||
|
for _, r := range p.Rules {
|
||||||
|
root = append(root, r.ToJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyFromValue converts a value into a Policy.
|
||||||
|
func PolicyFromValue(v Value) (*Policy, error) {
|
||||||
|
rules, err := RulesFromValue(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid rules in policy: %w", err)
|
||||||
|
}
|
||||||
|
return &Policy{
|
||||||
|
Rules: rules,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Rule is a policy rule with a corresponding action ("allow" or "deny"),
|
||||||
|
// and conditionals to determine if the rule matches or not.
|
||||||
|
type Rule struct {
|
||||||
|
Action Action
|
||||||
|
And []Criterion
|
||||||
|
Or []Criterion
|
||||||
|
Not []Criterion
|
||||||
|
Nor []Criterion
|
||||||
|
}
|
||||||
|
|
||||||
|
// RulesFromValue converts a Value into a slice of Rules. Only Arrays or Objects
|
||||||
|
// are supported.
|
||||||
|
func RulesFromValue(v Value) ([]Rule, error) {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case Array:
|
||||||
|
return RulesFromArray(t)
|
||||||
|
case Object:
|
||||||
|
return RulesFromObject(t)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported type for rule: %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RulesFromArray converts an Array into a slice of Rules. Each element of the Array is
|
||||||
|
// converted using RulesFromObject and merged together.
|
||||||
|
func RulesFromArray(a Array) ([]Rule, error) {
|
||||||
|
var rules []Rule
|
||||||
|
for _, v := range a {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case Object:
|
||||||
|
inner, err := RulesFromObject(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rules = append(rules, inner...)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported type for rules array: %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RulesFromObject converts an Object into a slice of Rules.
|
||||||
|
//
|
||||||
|
// One form is supported:
|
||||||
|
//
|
||||||
|
// 1. An object where the keys are the actions and the values are an object with "and", "or", or "not" fields:
|
||||||
|
// `{ "allow": { "and": [ {"groups": "group1"} ] } }`
|
||||||
|
//
|
||||||
|
func RulesFromObject(o Object) ([]Rule, error) {
|
||||||
|
var rules []Rule
|
||||||
|
for k, v := range o {
|
||||||
|
action, err := ActionFromValue(String(k))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid action in rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oo, ok := v.(Object)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid value for action in rule, expected Object, got %T", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := Rule{
|
||||||
|
Action: action,
|
||||||
|
}
|
||||||
|
err = rule.fillConditionalsFromObject(oo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, rule)
|
||||||
|
}
|
||||||
|
// sort by action for deterministic ordering
|
||||||
|
sort.Slice(rules, func(i, j int) bool {
|
||||||
|
return rules[i].Action < rules[j].Action
|
||||||
|
})
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the rule as JSON.
|
||||||
|
func (r *Rule) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(r.ToJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
// String converts the rule to a string.
|
||||||
|
func (r *Rule) String() string {
|
||||||
|
str, _ := r.MarshalJSON()
|
||||||
|
return string(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON converts the rule to JSON.
|
||||||
|
func (r *Rule) ToJSON() Value {
|
||||||
|
body := Object{}
|
||||||
|
|
||||||
|
for _, op := range []struct {
|
||||||
|
operator string
|
||||||
|
criteria []Criterion
|
||||||
|
}{
|
||||||
|
{"and", r.And},
|
||||||
|
{"or", r.Or},
|
||||||
|
{"not", r.Not},
|
||||||
|
{"nor", r.Nor},
|
||||||
|
} {
|
||||||
|
if len(op.criteria) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var criteria Array
|
||||||
|
for _, c := range op.criteria {
|
||||||
|
criteria = append(criteria, c.ToJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
body[op.operator] = criteria
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object{
|
||||||
|
string(r.Action): body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) fillConditionalsFromObject(o Object) error {
|
||||||
|
conditionals := []struct {
|
||||||
|
Name string
|
||||||
|
Criteria *[]Criterion
|
||||||
|
}{
|
||||||
|
{"and", &r.And},
|
||||||
|
{"or", &r.Or},
|
||||||
|
{"not", &r.Not},
|
||||||
|
{"nor", &r.Nor},
|
||||||
|
}
|
||||||
|
for _, cond := range conditionals {
|
||||||
|
if rawCriteria, ok := o[cond.Name]; ok {
|
||||||
|
criteria, err := CriteriaFromValue(rawCriteria)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid criteria in \"%s\"): %w", cond.Name, err)
|
||||||
|
}
|
||||||
|
*cond.Criteria = criteria
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k := range o {
|
||||||
|
switch k {
|
||||||
|
case "and", "or", "not", "nor", "action":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported conditional \"%s\", only and, or, not, nor and action are allowed", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Criterion is used by a rule to determine if the rule matches or not.
|
||||||
|
//
|
||||||
|
// Criteria RegoRulesGenerators are registered based on the specified name.
|
||||||
|
// Data is arbitrary JSON data sent to the generator.
|
||||||
|
type Criterion struct {
|
||||||
|
Name string
|
||||||
|
SubPath string
|
||||||
|
Data Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// CriteriaFromValue converts a Value into Criteria. Only Arrays are supported.
|
||||||
|
func CriteriaFromValue(v Value) ([]Criterion, error) {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case Array:
|
||||||
|
return CriteriaFromArray(t)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported type for criteria: %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CriteriaFromArray converts an Array into Criteria. Each element of the Array is
|
||||||
|
// converted using CriterionFromObject.
|
||||||
|
func CriteriaFromArray(a Array) ([]Criterion, error) {
|
||||||
|
var criteria []Criterion
|
||||||
|
for _, v := range a {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case Object:
|
||||||
|
inner, err := CriterionFromObject(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
criteria = append(criteria, *inner)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported type for criteria array: %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return criteria, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CriterionFromObject converts an Object into a Criterion.
|
||||||
|
//
|
||||||
|
// One form is supported:
|
||||||
|
//
|
||||||
|
// 1. An object where the keys are the names with a sub path and the values are the corresponding
|
||||||
|
// data for each Criterion: `{ "groups": "group1" }`
|
||||||
|
//
|
||||||
|
func CriterionFromObject(o Object) (*Criterion, error) {
|
||||||
|
if len(o) != 1 {
|
||||||
|
return nil, fmt.Errorf("each criteria may only contain a single key and value")
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range o {
|
||||||
|
name := k
|
||||||
|
subPath := ""
|
||||||
|
if idx := strings.Index(k, "/"); idx >= 0 {
|
||||||
|
name, subPath = k[:idx], k[idx+1:]
|
||||||
|
}
|
||||||
|
return &Criterion{
|
||||||
|
Name: name,
|
||||||
|
SubPath: subPath,
|
||||||
|
Data: v,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// this can't happen
|
||||||
|
panic("each criteria may only contain a single key and value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the criterion as JSON.
|
||||||
|
func (c *Criterion) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(c.ToJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
// String converts the criterion to a string.
|
||||||
|
func (c *Criterion) String() string {
|
||||||
|
str, _ := c.MarshalJSON()
|
||||||
|
return string(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON converts the criterion to JSON.
|
||||||
|
func (c *Criterion) ToJSON() Value {
|
||||||
|
nm := c.Name
|
||||||
|
if c.SubPath != "" {
|
||||||
|
nm += "/" + c.SubPath
|
||||||
|
}
|
||||||
|
return Object{nm: c.Data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Action describe what to do when a rule matches, either "allow" or "deny".
|
||||||
|
type Action string
|
||||||
|
|
||||||
|
// ActionFromValue converts a Value into an Action.
|
||||||
|
func ActionFromValue(value Value) (Action, error) {
|
||||||
|
s, ok := value.(String)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unsupported type for action: %T", value)
|
||||||
|
}
|
||||||
|
switch Action(s) {
|
||||||
|
case ActionAllow:
|
||||||
|
return ActionAllow, nil
|
||||||
|
case ActionDeny:
|
||||||
|
return ActionDeny, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("unsupported action: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const (
|
||||||
|
ActionAllow Action = "allow"
|
||||||
|
ActionDeny Action = "deny"
|
||||||
|
)
|
272
pkg/policy/parser/json.go
Normal file
272
pkg/policy/parser/json.go
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Value is a JSON value. Either an object, array, string, number, boolean or null.
|
||||||
|
type Value interface {
|
||||||
|
isValue()
|
||||||
|
Clone() Value
|
||||||
|
RegoValue() ast.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseValue parses JSON into a value.
|
||||||
|
func ParseValue(r io.Reader) (Value, error) {
|
||||||
|
dec := json.NewDecoder(r)
|
||||||
|
dec.UseNumber()
|
||||||
|
|
||||||
|
tok, err := dec.Token()
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := parseValue(dec, tok)
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
if dec.More() {
|
||||||
|
return nil, fmt.Errorf("unexpected additional json value: offset=%d", dec.InputOffset())
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseValue(dec *json.Decoder, tok json.Token) (Value, error) {
|
||||||
|
if d, ok := tok.(json.Delim); ok {
|
||||||
|
switch d {
|
||||||
|
case '[':
|
||||||
|
return parseArray(dec)
|
||||||
|
case '{':
|
||||||
|
return parseObject(dec)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported json delimiter: %s", string(d))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseSimple(tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArray(dec *json.Decoder) (Value, error) {
|
||||||
|
var a Array
|
||||||
|
for {
|
||||||
|
tok, err := dec.Token()
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d, ok := tok.(json.Delim); ok && d == ']' {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := parseValue(dec, tok)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a = append(a, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseObject(dec *json.Decoder) (Value, error) {
|
||||||
|
o := make(Object)
|
||||||
|
k := ""
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
tok, err := dec.Token()
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d, ok := tok.(json.Delim); ok && d == '}' {
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := parseValue(dec, tok)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're handling a key
|
||||||
|
if i%2 == 0 {
|
||||||
|
s, ok := v.(String)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported object key type: %T", v)
|
||||||
|
}
|
||||||
|
k = string(s)
|
||||||
|
} else {
|
||||||
|
o[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSimple(tok json.Token) (Value, error) {
|
||||||
|
switch t := tok.(type) {
|
||||||
|
case bool:
|
||||||
|
return Boolean(t), nil
|
||||||
|
case json.Number:
|
||||||
|
return Number(t), nil
|
||||||
|
case string:
|
||||||
|
return String(t), nil
|
||||||
|
case nil:
|
||||||
|
return Null{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unsupported json token type: %T", tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Object is a map of strings to values.
|
||||||
|
type Object map[string]Value
|
||||||
|
|
||||||
|
func (Object) isValue() {}
|
||||||
|
|
||||||
|
// Clone clones the Object.
|
||||||
|
func (o Object) Clone() Value {
|
||||||
|
no := make(Object)
|
||||||
|
for k, v := range o {
|
||||||
|
no[k] = v
|
||||||
|
}
|
||||||
|
return no
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegoValue returns the Object as a rego Value.
|
||||||
|
func (o Object) RegoValue() ast.Value {
|
||||||
|
kvps := make([][2]*ast.Term, 0, len(o))
|
||||||
|
for k, v := range o {
|
||||||
|
kvps = append(kvps, [2]*ast.Term{
|
||||||
|
ast.StringTerm(k),
|
||||||
|
ast.NewTerm(v.RegoValue()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ast.NewObject(kvps...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the JSON representation of the Object.
|
||||||
|
func (o Object) String() string {
|
||||||
|
bs, _ := json.Marshal(o)
|
||||||
|
return string(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Array is a slice of values.
|
||||||
|
type Array []Value
|
||||||
|
|
||||||
|
func (Array) isValue() {}
|
||||||
|
|
||||||
|
// Clone clones the array.
|
||||||
|
func (a Array) Clone() Value {
|
||||||
|
na := make(Array, len(a))
|
||||||
|
copy(na, a)
|
||||||
|
return na
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegoValue returns the Array as a rego Value.
|
||||||
|
func (a Array) RegoValue() ast.Value {
|
||||||
|
var vs []*ast.Term
|
||||||
|
for _, v := range a {
|
||||||
|
vs = append(vs, ast.NewTerm(v.RegoValue()))
|
||||||
|
}
|
||||||
|
return ast.NewArray(vs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the JSON representation of the Array.
|
||||||
|
func (a Array) String() string {
|
||||||
|
bs, _ := json.Marshal(a)
|
||||||
|
return string(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A String is a wrapper around a string.
|
||||||
|
type String string
|
||||||
|
|
||||||
|
func (String) isValue() {}
|
||||||
|
|
||||||
|
// Clone clones the string.
|
||||||
|
func (s String) Clone() Value {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegoValue returns the String as a rego Value.
|
||||||
|
func (s String) RegoValue() ast.Value {
|
||||||
|
return ast.String(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the JSON representation of the String.
|
||||||
|
func (s String) String() string {
|
||||||
|
bs, _ := json.Marshal(s)
|
||||||
|
return string(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Number is an integer or a floating point value stored in string representation.
|
||||||
|
type Number string
|
||||||
|
|
||||||
|
func (Number) isValue() {}
|
||||||
|
|
||||||
|
// Clone clones the number.
|
||||||
|
func (n Number) Clone() Value {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegoValue returns the Number as a rego Value.
|
||||||
|
func (n Number) RegoValue() ast.Value {
|
||||||
|
return ast.Number(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the JSON representation of the Number.
|
||||||
|
func (n Number) String() string {
|
||||||
|
return string(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the number as JSON.
|
||||||
|
func (n Number) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(n), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Boolean is either true or false.
|
||||||
|
type Boolean bool
|
||||||
|
|
||||||
|
func (Boolean) isValue() {}
|
||||||
|
|
||||||
|
// Clone clones the boolean.
|
||||||
|
func (b Boolean) Clone() Value {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegoValue returns the Boolean as a rego Value.
|
||||||
|
func (b Boolean) RegoValue() ast.Value {
|
||||||
|
return ast.Boolean(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the JSON representation of the Boolean.
|
||||||
|
func (b Boolean) String() string {
|
||||||
|
if b {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Null is the nil value.
|
||||||
|
type Null struct{}
|
||||||
|
|
||||||
|
func (Null) isValue() {}
|
||||||
|
|
||||||
|
// Clone clones the null.
|
||||||
|
func (Null) Clone() Value {
|
||||||
|
return Null{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegoValue returns the Null as a rego Value.
|
||||||
|
func (Null) RegoValue() ast.Value {
|
||||||
|
return ast.Null{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns JSON null.
|
||||||
|
func (Null) String() string {
|
||||||
|
return "null"
|
||||||
|
}
|
117
pkg/policy/parser/json_test.go
Normal file
117
pkg/policy/parser/json_test.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArray(t *testing.T) {
|
||||||
|
var _ Value = Array{}
|
||||||
|
t.Run("Clone", func(t *testing.T) {
|
||||||
|
a1 := Array{Number("1"), Number("2"), Number("3")}
|
||||||
|
a2 := a1.Clone()
|
||||||
|
assert.Equal(t, a1, a2)
|
||||||
|
})
|
||||||
|
t.Run("RegoValue", func(t *testing.T) {
|
||||||
|
a := Array{Number("1"), Number("2")}
|
||||||
|
assert.Equal(t, ast.NewArray(
|
||||||
|
ast.NumberTerm("1"),
|
||||||
|
ast.NumberTerm("2"),
|
||||||
|
), a.RegoValue())
|
||||||
|
})
|
||||||
|
t.Run("String", func(t *testing.T) {
|
||||||
|
a := Array{Number("1"), Number("2"), Boolean(true)}
|
||||||
|
assert.Equal(t, `[1,2,true]`, a.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolean(t *testing.T) {
|
||||||
|
var _ Value = Boolean(true)
|
||||||
|
t.Run("Clone", func(t *testing.T) {
|
||||||
|
b1 := Boolean(true)
|
||||||
|
b2 := b1.Clone()
|
||||||
|
assert.Equal(t, b1, b2)
|
||||||
|
})
|
||||||
|
t.Run("RegoValue", func(t *testing.T) {
|
||||||
|
b := Boolean(true)
|
||||||
|
assert.Equal(t, ast.Boolean(true), b.RegoValue())
|
||||||
|
})
|
||||||
|
t.Run("String", func(t *testing.T) {
|
||||||
|
b := Boolean(true)
|
||||||
|
assert.Equal(t, `true`, b.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNull(t *testing.T) {
|
||||||
|
var _ Value = Null{}
|
||||||
|
t.Run("Clone", func(t *testing.T) {
|
||||||
|
n1 := Null{}
|
||||||
|
n2 := n1.Clone()
|
||||||
|
assert.Equal(t, n1, n2)
|
||||||
|
})
|
||||||
|
t.Run("RegoValue", func(t *testing.T) {
|
||||||
|
n := Null{}
|
||||||
|
assert.Equal(t, ast.Null{}, n.RegoValue())
|
||||||
|
})
|
||||||
|
t.Run("String", func(t *testing.T) {
|
||||||
|
n := Null{}
|
||||||
|
assert.Equal(t, `null`, n.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumber(t *testing.T) {
|
||||||
|
var _ Value = Number("1")
|
||||||
|
t.Run("Clone", func(t *testing.T) {
|
||||||
|
n1 := Number("1")
|
||||||
|
n2 := n1.Clone()
|
||||||
|
assert.Equal(t, n1, n2)
|
||||||
|
})
|
||||||
|
t.Run("RegoValue", func(t *testing.T) {
|
||||||
|
n := Number("1")
|
||||||
|
assert.Equal(t, ast.Number("1"), n.RegoValue())
|
||||||
|
})
|
||||||
|
t.Run("String", func(t *testing.T) {
|
||||||
|
n := Number("1")
|
||||||
|
assert.Equal(t, `1`, n.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObject(t *testing.T) {
|
||||||
|
var _ Value = Object{}
|
||||||
|
t.Run("Clone", func(t *testing.T) {
|
||||||
|
o1 := Object{"x": String("y")}
|
||||||
|
o2 := o1.Clone().(Object)
|
||||||
|
assert.Equal(t, o1, o2)
|
||||||
|
o2["x"] = String("z")
|
||||||
|
assert.NotEqual(t, o1, o2)
|
||||||
|
})
|
||||||
|
t.Run("RegoValue", func(t *testing.T) {
|
||||||
|
o := Object{"x": String("y")}
|
||||||
|
assert.Equal(t, ast.NewObject(
|
||||||
|
[2]*ast.Term{ast.StringTerm("x"), ast.StringTerm("y")},
|
||||||
|
), o.RegoValue())
|
||||||
|
})
|
||||||
|
t.Run("String", func(t *testing.T) {
|
||||||
|
o1 := Object{"x": String("y")}
|
||||||
|
assert.Equal(t, `{"x":"y"}`, o1.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
var _ Value = String("test")
|
||||||
|
t.Run("Clone", func(t *testing.T) {
|
||||||
|
s1 := String("test")
|
||||||
|
s2 := s1.Clone()
|
||||||
|
assert.Equal(t, s1, s2)
|
||||||
|
})
|
||||||
|
t.Run("RegoValue", func(t *testing.T) {
|
||||||
|
s := String("test")
|
||||||
|
assert.Equal(t, ast.String("test"), s.RegoValue())
|
||||||
|
})
|
||||||
|
t.Run("String", func(t *testing.T) {
|
||||||
|
s := String("test")
|
||||||
|
assert.Equal(t, `"test"`, s.String())
|
||||||
|
})
|
||||||
|
}
|
126
pkg/policy/parser/parser.go
Normal file
126
pkg/policy/parser/parser.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// Package parser contains a parser for Pomerium Policy Language.
|
||||||
|
//
|
||||||
|
// The Pomerium Policy Language is a JSON or YAML document containing rules,
|
||||||
|
// actions, logical operators and criteria.
|
||||||
|
//
|
||||||
|
// The document contains zero or more rules.
|
||||||
|
//
|
||||||
|
// A rule has an action and zero or more logical operators.
|
||||||
|
//
|
||||||
|
// An action is either "allow" or "deny".
|
||||||
|
//
|
||||||
|
// The logical operators are "and", "or" and "not" and contain zero or more criteria.
|
||||||
|
//
|
||||||
|
// A criterion has a name and arbitrary JSON data.
|
||||||
|
//
|
||||||
|
// An example policy:
|
||||||
|
//
|
||||||
|
// allow:
|
||||||
|
// and:
|
||||||
|
// - domain: example.com
|
||||||
|
// - group: admin
|
||||||
|
// deny:
|
||||||
|
// or:
|
||||||
|
// - user: user1@example.com
|
||||||
|
// - user: user2@example.com
|
||||||
|
//
|
||||||
|
// The JSON Schema for the language:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "$ref": "#/definitions/policy",
|
||||||
|
// "definitions": {
|
||||||
|
// "policy": {
|
||||||
|
// "anyOf": [
|
||||||
|
// { "$ref": "#/definitions/rules" },
|
||||||
|
// {
|
||||||
|
// "type": "array",
|
||||||
|
// "items": { "$ref": "#/definitions/rules" }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "rules": {
|
||||||
|
// "type": "object",
|
||||||
|
// "properties": {
|
||||||
|
// "allow": { "$ref": "#/definitions/rule_body" },
|
||||||
|
// "deny": { "$ref": "#/definitions/rule_body" }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "rule_body": {
|
||||||
|
// "type": "object",
|
||||||
|
// "properties": {
|
||||||
|
// "and": {
|
||||||
|
// "type": "array",
|
||||||
|
// "items": { "$ref": "#/definitions/criteria" }
|
||||||
|
// },
|
||||||
|
// "not": {
|
||||||
|
// "type": "array",
|
||||||
|
// "items": { "$ref": "#/definitions/criteria" }
|
||||||
|
// },
|
||||||
|
// "or": {
|
||||||
|
// "type": "array",
|
||||||
|
// "items": { "$ref": "#/definitions/criteria" }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "additionalProperties": false
|
||||||
|
// },
|
||||||
|
// "criteria": {
|
||||||
|
// "type": "object",
|
||||||
|
// "additionalProperties": true,
|
||||||
|
// "minProperties": 1,
|
||||||
|
// "maxProperties": 1
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Parser parses raw policy definitions into a Policy.
|
||||||
|
type Parser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Parser.
|
||||||
|
func New() *Parser {
|
||||||
|
p := &Parser{}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJSON parses a raw JSON document into a policy.
|
||||||
|
func (p *Parser) ParseJSON(r io.Reader) (*Policy, error) {
|
||||||
|
doc, err := ParseValue(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return PolicyFromValue(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseYAML parses a raw YAML document into a policy.
|
||||||
|
func (p *Parser) ParseYAML(r io.Reader) (*Policy, error) {
|
||||||
|
var obj interface{}
|
||||||
|
err := yaml.NewDecoder(r).Decode(&obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bs, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.ParseJSON(bytes.NewReader(bs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJSON creates a parser and calls ParseJSON on it.
|
||||||
|
func ParseJSON(r io.Reader) (*Policy, error) {
|
||||||
|
return New().ParseJSON(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseYAML creates a parser and calls ParseYAML on it.
|
||||||
|
func ParseYAML(r io.Reader) (*Policy, error) {
|
||||||
|
return New().ParseYAML(r)
|
||||||
|
}
|
172
pkg/policy/parser/parser_test.go
Normal file
172
pkg/policy/parser/parser_test.go
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseJSON(t *testing.T) {
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{}`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Rules, 0)
|
||||||
|
})
|
||||||
|
t.Run("allow", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{ "allow": {} }`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Rules, 1)
|
||||||
|
})
|
||||||
|
t.Run("deny", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{ "deny": {} }`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Rules, 1)
|
||||||
|
})
|
||||||
|
t.Run("invalid rule type", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`1`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
t.Run("invalid rule action", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{ "some-other-action": {} }`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
t.Run("rule array", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`[{ "deny": {} }]`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Rules, 1)
|
||||||
|
})
|
||||||
|
t.Run("invalid rule array", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`[{ "some-other-action": {} }]`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
t.Run("invalid rule array type", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`[1]`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
t.Run("logical operators", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{
|
||||||
|
"allow": {
|
||||||
|
"and": [],
|
||||||
|
"or": [],
|
||||||
|
"not": []
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, p.Rules, 1)
|
||||||
|
})
|
||||||
|
t.Run("invalid logical operator", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{
|
||||||
|
"allow": {
|
||||||
|
"iff": []
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
t.Run("criteria", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{
|
||||||
|
"allow": {
|
||||||
|
"and": [
|
||||||
|
{ "criterion1": 1 },
|
||||||
|
{ "criterion2": 2 },
|
||||||
|
{ "criterion3/sub": 3 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, &Policy{
|
||||||
|
Rules: []Rule{{
|
||||||
|
Action: ActionAllow,
|
||||||
|
And: []Criterion{
|
||||||
|
{Name: "criterion1", Data: Number("1")},
|
||||||
|
{Name: "criterion2", Data: Number("2")},
|
||||||
|
{Name: "criterion3", SubPath: "sub", Data: Number("3")},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}, p)
|
||||||
|
})
|
||||||
|
t.Run("empty criteria", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{
|
||||||
|
"allow": {
|
||||||
|
"and": [
|
||||||
|
{ }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
t.Run("invalid multiple criteria", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{
|
||||||
|
"allow": {
|
||||||
|
"and": [
|
||||||
|
{ "criterion1": 1, "criterion2": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
t.Run("invalid criteria type", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{
|
||||||
|
"allow": {
|
||||||
|
"and": { "criterion1": 1 }
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
t.Run("invalid criteria array type", func(t *testing.T) {
|
||||||
|
p, err := ParseJSON(strings.NewReader(`{
|
||||||
|
"allow": {
|
||||||
|
"and": [1]
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseYAML(t *testing.T) {
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
p, err := ParseYAML(strings.NewReader(`
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- criterion1: 1
|
||||||
|
- criterion2: 2
|
||||||
|
- criterion3/sub: 3
|
||||||
|
or:
|
||||||
|
- criterion4: 4
|
||||||
|
deny:
|
||||||
|
not:
|
||||||
|
- criterion5: 5
|
||||||
|
`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, &Policy{
|
||||||
|
Rules: []Rule{
|
||||||
|
{
|
||||||
|
Action: ActionAllow,
|
||||||
|
And: []Criterion{
|
||||||
|
{Name: "criterion1", Data: Number("1")},
|
||||||
|
{Name: "criterion2", Data: Number("2")},
|
||||||
|
{Name: "criterion3", SubPath: "sub", Data: Number("3")},
|
||||||
|
},
|
||||||
|
Or: []Criterion{
|
||||||
|
{Name: "criterion4", Data: Number("4")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Not: []Criterion{
|
||||||
|
{Name: "criterion5", Data: Number("5")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, p)
|
||||||
|
})
|
||||||
|
}
|
45
pkg/policy/parser/schema.json
Normal file
45
pkg/policy/parser/schema.json
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/policy",
|
||||||
|
"definitions": {
|
||||||
|
"policy": {
|
||||||
|
"anyOf": [
|
||||||
|
{ "$ref": "#/definitions/rules" },
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/rules" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allow": { "$ref": "#/definitions/rule_body" },
|
||||||
|
"deny": { "$ref": "#/definitions/rule_body" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rule_body": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"and": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/criteria" }
|
||||||
|
},
|
||||||
|
"not": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/criteria" }
|
||||||
|
},
|
||||||
|
"or": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/criteria" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"criteria": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"minProperties": 1,
|
||||||
|
"maxProperties": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
pkg/policy/policy.go
Normal file
47
pkg/policy/policy.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// Package policy contains an implementation of the Pomerium Policy Language.
|
||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/format"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/criteria"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/generator"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// re-exported types
|
||||||
|
type (
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateRegoFromPPL generates a rego script from raw Pomerium Policy Language.
|
||||||
|
func GenerateRegoFromPPL(r io.Reader) (string, error) {
|
||||||
|
p := parser.New()
|
||||||
|
var gOpts []generator.Option
|
||||||
|
for _, ctor := range criteria.All() {
|
||||||
|
gOpts = append(gOpts, generator.WithCriterion(ctor))
|
||||||
|
}
|
||||||
|
g := generator.New(gOpts...)
|
||||||
|
|
||||||
|
ppl, err := p.ParseYAML(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
mod, err := g.Generate(ppl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
bs, err := format.Ast(mod)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(bs), err
|
||||||
|
}
|
117
pkg/policy/rules/rules.go
Normal file
117
pkg/policy/rules/rules.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
// Package rules contains useful pre-defined rego AST rules.
|
||||||
|
package rules
|
||||||
|
|
||||||
|
import "github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
// GetSession the session for the given id.
|
||||||
|
func GetSession() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
get_session(id) = v {
|
||||||
|
v := get_databroker_record("type.googleapis.com/user.ServiceAccount", id)
|
||||||
|
} else = v {
|
||||||
|
v := get_databroker_record("type.googleapis.com/session.Session", id)
|
||||||
|
} else = v {
|
||||||
|
v := {}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns the user for the given session.
|
||||||
|
func GetUser() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
get_user(session) = v {
|
||||||
|
v := get_databroker_record("type.googleapis.com/user.User", session.impersonate_user_id)
|
||||||
|
} else = v {
|
||||||
|
v := get_databroker_record("type.googleapis.com/user.User", session.user_id)
|
||||||
|
} else = v {
|
||||||
|
v := {}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserEmail gets the user email, either the impersonate email, or the user email.
|
||||||
|
func GetUserEmail() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
get_user_email(session, user) = v {
|
||||||
|
v := session.impersonate_email
|
||||||
|
} else = v {
|
||||||
|
v := user.email
|
||||||
|
} else = v {
|
||||||
|
v := ""
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDirectoryUser returns the directory user for the given session.
|
||||||
|
func GetDirectoryUser() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
get_directory_user(session) = v {
|
||||||
|
v := get_databroker_record("type.googleapis.com/directory.User", session.impersonate_user_id)
|
||||||
|
} else = v {
|
||||||
|
v := get_databroker_record("type.googleapis.com/directory.User", session.user_id)
|
||||||
|
} else = v {
|
||||||
|
v := {}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDirectoryGroup returns the directory group for the given id.
|
||||||
|
func GetDirectoryGroup() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
get_directory_group(id) = v {
|
||||||
|
v := get_databroker_record("type.googleapis.com/directory.Group", id)
|
||||||
|
} else = v {
|
||||||
|
v := {}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroupIDs returns the group ids for the given session or directory user.
|
||||||
|
func GetGroupIDs() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
get_group_ids(session, directory_user) = v {
|
||||||
|
v := session.impersonate_groups
|
||||||
|
} else = v {
|
||||||
|
v := directory_user.group_ids
|
||||||
|
} else = v {
|
||||||
|
v := []
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectGet recursively gets a value from an object.
|
||||||
|
func ObjectGet() *ast.Rule {
|
||||||
|
return ast.MustParseRule(`
|
||||||
|
# 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 {
|
||||||
|
segments := split(key, "/")
|
||||||
|
count(segments) == 2
|
||||||
|
o1 := object.get(obj, segments[0], {})
|
||||||
|
value = object.get(o1, segments[1], def)
|
||||||
|
} else = value {
|
||||||
|
segments := split(key, "/")
|
||||||
|
count(segments) == 3
|
||||||
|
o1 := object.get(obj, segments[0], {})
|
||||||
|
o2 := object.get(o1, segments[1], {})
|
||||||
|
value = object.get(o2, segments[2], def)
|
||||||
|
} else = value {
|
||||||
|
segments := split(key, "/")
|
||||||
|
count(segments) == 4
|
||||||
|
o1 := object.get(obj, segments[0], {})
|
||||||
|
o2 := object.get(o1, segments[1], {})
|
||||||
|
o3 := object.get(o2, segments[2], {})
|
||||||
|
value = object.get(o3, segments[3], def)
|
||||||
|
} else = value {
|
||||||
|
segments := split(key, "/")
|
||||||
|
count(segments) == 5
|
||||||
|
o1 := object.get(obj, segments[0], {})
|
||||||
|
o2 := object.get(o1, segments[1], {})
|
||||||
|
o3 := object.get(o2, segments[2], {})
|
||||||
|
o4 := object.get(o3, segments[3], {})
|
||||||
|
value = object.get(o4, segments[4], def)
|
||||||
|
} else = value {
|
||||||
|
value = object.get(obj, key, def)
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue