mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-18 19:47:22 +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