package opa
// nolint:lll
//go:generate go-bindata -pkg $GOPACKAGE -o policy.bindata.go -ignore .*_test.rego -ignore Makefile -ignore README\.md policy/...
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/go-kratos/kratos/v2/log"
"github.com/pkg/errors"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/storage/inmem"
"github.com/open-policy-agent/opa/topdown"
"github.com/tx7do/kratos-authz/engine"
)
var _ engine.Engine = (*State)(nil)
type State struct {
store storage.Store
queries map[string]ast.Body
compiler *ast.Compiler
modules map[string]*ast.Module
preparedEvalProjects rego.PreparedEvalQuery
}
const (
authzProjectsQuery = "data.authz.authorized_project[project]"
filteredPairsQuery = "data.authz.introspection.authorized_pair[_]"
filteredProjectsQuery = "data.authz.introspection.authorized_project"
)
func New(_ context.Context, opts ...OptFunc) (*State, error) {
authzProjectsQueryParsed, err := ast.ParseBody(authzProjectsQuery)
if err != nil {
return nil, errors.Wrapf(err, "parse query %q", authzProjectsQuery)
}
filteredPairsQueryParsed, err := ast.ParseBody(filteredPairsQuery)
if err != nil {
return nil, errors.Wrapf(err, "parse query %q", filteredPairsQuery)
}
filteredProjectsQueryParsed, err := ast.ParseBody(filteredProjectsQuery)
if err != nil {
return nil, errors.Wrapf(err, "parse query %q", filteredProjectsQuery)
}
s := State{
store: inmem.New(),
queries: map[string]ast.Body{
authzProjectsQuery: authzProjectsQueryParsed,
filteredPairsQuery: filteredPairsQueryParsed,
filteredProjectsQuery: filteredProjectsQueryParsed,
},
}
for _, opt := range opts {
opt(&s)
}
if err := s.initModules(); err != nil {
return nil, errors.Wrap(err, "init OPA modules")
}
return &s, nil
}
func (s *State) ProjectsAuthorized(ctx context.Context, subjects engine.Subjects, action engine.Action, resource engine.Resource, projects engine.Projects) (engine.Projects, error) {
var subs []*ast.Term
for _, sub := range subjects {
subs = append(subs, ast.NewTerm(ast.String(sub)))
}
var projs []*ast.Term
for _, proj := range projects {
projs = append(projs, ast.NewTerm(ast.String(proj)))
}
input := ast.NewObject(
[2]*ast.Term{ast.NewTerm(ast.String("subjects")), ast.ArrayTerm(subs...)},
[2]*ast.Term{ast.NewTerm(ast.String("resource")), ast.NewTerm(ast.String(resource))},
[2]*ast.Term{ast.NewTerm(ast.String("action")), ast.NewTerm(ast.String(action))},
[2]*ast.Term{ast.NewTerm(ast.String("projects")), ast.ArrayTerm(projs...)},
)
resultSet, err := s.preparedEvalProjects.Eval(ctx, rego.EvalParsedInput(input))
if err != nil {
return engine.Projects{}, &EvaluationError{e: err}
}
return s.projectsFromPreparedEvalQuery(resultSet)
}
func (s *State) FilterAuthorizedPairs(ctx context.Context, subjects engine.Subjects, pairs engine.Pairs) (engine.Pairs, error) {
opaInput := map[string]interface{}{
"subjects": subjects,
"pairs": pairs,
}
rs, err := s.evalQuery(ctx, s.queries[filteredPairsQuery], opaInput, s.store)
if err != nil {
return nil, &EvaluationError{e: err}
}
return s.pairsFromResults(rs)
}
func (s *State) FilterAuthorizedProjects(ctx context.Context, subjects engine.Subjects) (engine.Projects, error) {
opaInput := map[string]interface{}{
"subjects": subjects,
}
rs, err := s.evalQuery(ctx, s.queries[filteredProjectsQuery], opaInput, s.store)
if err != nil {
return nil, &EvaluationError{e: err}
}
return s.projectsFromPartialResults(rs)
}
func (s *State) IsAuthorized(ctx context.Context, subject engine.Subject, action engine.Action, resource engine.Resource, project engine.Project) (bool, error) {
if len(project) > 0 {
input := ast.NewObject(
[2]*ast.Term{ast.NewTerm(ast.String("subjects")), ast.ArrayTerm(ast.NewTerm(ast.String(subject)))},
[2]*ast.Term{ast.NewTerm(ast.String("resource")), ast.NewTerm(ast.String(resource))},
[2]*ast.Term{ast.NewTerm(ast.String("action")), ast.NewTerm(ast.String(action))},
[2]*ast.Term{ast.NewTerm(ast.String("projects")), ast.ArrayTerm(ast.NewTerm(ast.String(project)))},
)
resultSet, err := s.preparedEvalProjects.Eval(ctx, rego.EvalParsedInput(input))
if err != nil {
return false, &EvaluationError{e: err}
}
return s.allowedFromPreparedEvalQuery(resultSet)
} else {
opaInput := map[string]interface{}{
"subjects": engine.MakeSubjects(subject),
"pairs": engine.MakePairs(engine.Pair{Resource: resource, Action: action}),
}
rs, err := s.evalQuery(ctx, s.queries[filteredPairsQuery], opaInput, s.store)
if err != nil {
return false, &EvaluationError{e: err}
}
return s.pairsFromAllowed(rs)
}
}
func (s *State) SetPolicies(ctx context.Context, policyMap engine.PolicyMap, roleMap engine.RoleMap) error {
s.store = inmem.NewFromObject(map[string]interface{}{
"policies": policyMap,
"roles": roleMap,
})
return s.makeAuthorizedProjectPreparedQuery(ctx)
}
func (s *State) initModules() error {
if len(s.modules) == 0 {
mods := map[string]*ast.Module{}
for _, name := range AssetNames() {
if !strings.HasSuffix(name, ".rego") {
continue
}
parsed, err := ast.ParseModule(name, string(MustAsset(name)))
if err != nil {
return errors.Wrapf(err, "parse policy file %q", name)
}
mods[name] = parsed
}
s.modules = mods
}
compiler, err := s.newCompiler()
if err != nil {
return errors.Wrap(err, "init compiler")
}
s.compiler = compiler
return nil
}
func (s *State) makeAuthorizedProjectPreparedQuery(ctx context.Context) error {
compiler, err := s.newCompiler()
if err != nil {
return err
}
r := rego.New(
rego.Store(s.store),
rego.Compiler(compiler),
rego.ParsedQuery(s.queries[authzProjectsQuery]),
rego.DisableInlining([]string{
"data.authz.denied_project",
}),
)
pq, err := r.Partial(ctx)
if err != nil {
return err
}
for i, module := range pq.Support {
compiler.Modules[fmt.Sprintf("support%d", i)] = module
}
main := &ast.Module{
Package: ast.MustParsePackage("package __partialauthz"),
}
for i := range pq.Queries {
rule := &ast.Rule{
Module: main,
Head: ast.NewHead("authorized_project", ast.VarTerm("project")),
Body: pq.Queries[i],
}
main.Rules = append(main.Rules, rule)
}
compiler.Modules["__partialauthz"] = main
compiler.Compile(compiler.Modules)
if compiler.Failed() {
return compiler.Errors
}
r2 := rego.New(
rego.Store(s.store),
rego.Compiler(compiler),
rego.Query("data.__partialauthz.authorized_project[project]"),
)
query, err := r2.PrepareForEval(ctx)
if err != nil {
return errors.Wrap(err, "prepare query for eval (authorized_project)")
}
s.preparedEvalProjects = query
return nil
}
func (s *State) newCompiler() (*ast.Compiler, error) {
compiler := ast.NewCompiler()
compiler.Compile(s.modules)
if compiler.Failed() {
return nil, errors.Wrap(compiler.Errors, "compile modules")
}
return compiler, nil
}
func (s *State) DumpData(ctx context.Context) error {
return dumpData(ctx, s.store)
}
func dumpData(ctx context.Context, store storage.Store) error {
txn, err := store.NewTransaction(ctx)
if err != nil {
return err
}
data, err := store.Read(ctx, txn, []string{})
if err != nil {
return err
}
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
log.Info("data: ", string(jsonData))
return store.Commit(ctx, txn)
}
func (s *State) evalQuery(ctx context.Context, query ast.Body, input interface{}, store storage.Store) (rego.ResultSet, error) {
var tracer *topdown.BufferTracer
rs, err := rego.New(
rego.ParsedQuery(query),
rego.Input(input),
rego.Compiler(s.compiler),
rego.Store(store),
rego.QueryTracer(tracer),
).Eval(ctx)
if err != nil {
return nil, err
}
if tracer.Enabled() {
topdown.PrettyTrace(os.Stderr, *tracer) //nolint: govet // tracer can be nil only if tracer.Enabled() == false
}
return rs, nil
}
func (s *State) pairsFromResults(rs rego.ResultSet) (engine.Pairs, error) {
pairs := make(engine.Pairs, len(rs))
for i, r := range rs {
if len(r.Expressions) != 1 {
return nil, &UnexpectedResultExpressionError{exps: r.Expressions}
}
m, ok := r.Expressions[0].Value.(map[string]interface{})
if !ok {
return nil, &UnexpectedResultExpressionError{exps: r.Expressions}
}
res, ok := m["resource"].(string)
if !ok {
return nil, &UnexpectedResultExpressionError{exps: r.Expressions}
}
act, ok := m["action"].(string)
if !ok {
return nil, &UnexpectedResultExpressionError{exps: r.Expressions}
}
pairs[i] = engine.Pair{Resource: engine.Resource(res), Action: engine.Action(act)}
}
return pairs, nil
}
func (s *State) pairsFromAllowed(rs rego.ResultSet) (bool, error) {
for _, r := range rs {
if len(r.Expressions) != 1 {
return false, &UnexpectedResultExpressionError{exps: r.Expressions}
}
m, ok := r.Expressions[0].Value.(map[string]interface{})
if !ok {
return false, &UnexpectedResultExpressionError{exps: r.Expressions}
}
_, ok = m["resource"].(string)
if !ok {
return false, &UnexpectedResultExpressionError{exps: r.Expressions}
}
_, ok = m["action"].(string)
if !ok {
return false, &UnexpectedResultExpressionError{exps: r.Expressions}
}
return true, nil
}
return false, nil
}
func (s *State) projectsFromPartialResults(rs rego.ResultSet) (engine.Projects, error) {
if len(rs) != 1 {
return nil, &UnexpectedResultSetError{set: rs}
}
r := rs[0]
if len(r.Expressions) != 1 {
return nil, &UnexpectedResultExpressionError{exps: r.Expressions}
}
projects, err := s.stringArrayFromResults(r.Expressions)
if err != nil {
return nil, &UnexpectedResultExpressionError{exps: r.Expressions}
}
return projects, nil
}
func (s *State) stringArrayFromResults(exps []*rego.ExpressionValue) (engine.Projects, error) {
rawArray, ok := exps[0].Value.([]interface{})
if !ok {
return nil, &UnexpectedResultExpressionError{exps: exps}
}
vals := make(engine.Projects, len(rawArray))
for i := range rawArray {
v, ok := rawArray[i].(string)
if !ok {
return nil, errors.New("error casting to string")
}
vals[i] = engine.Project(v)
}
return vals, nil
}
func (s *State) projectsFromPreparedEvalQuery(rs rego.ResultSet) (engine.Projects, error) {
projectsFound := make(map[string]bool, len(rs))
result := make(engine.Projects, 0, len(rs))
var ok bool
var proj string
for i := range rs {
proj, ok = rs[i].Bindings["project"].(string)
if !ok {
return nil, &UnexpectedResultExpressionError{exps: rs[i].Expressions}
}
if !projectsFound[proj] {
result = append(result, engine.Project(proj))
projectsFound[proj] = true
}
}
return result, nil
}
func (s *State) allowedFromPreparedEvalQuery(rs rego.ResultSet) (bool, error) {
var ok bool
for i := range rs {
_, ok = rs[i].Bindings["project"].(string)
if !ok {
return false, &UnexpectedResultExpressionError{exps: rs[i].Expressions}
}
return true, nil
}
return false, nil
}
评论