Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a deploy command & improve expression translation #17

Merged
merged 26 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
684f5a1
better expression translation
xacrimon Oct 9, 2022
92c275e
remove uneeded expr
xacrimon Oct 9, 2022
28e277d
even better expression trans
xacrimon Oct 9, 2022
098ce50
translate nots
xacrimon Oct 9, 2022
32c2c6a
string set map translation
xacrimon Oct 9, 2022
d213ff3
translate label expressions
xacrimon Oct 9, 2022
027c93d
correctly translate nonliterals inside of set equalities
xacrimon Oct 9, 2022
f220a33
remove intermediary array
xacrimon Oct 9, 2022
89b933e
add deploy command
xacrimon Oct 10, 2022
b2a0267
sudo option for predicate deploy
xacrimon Oct 10, 2022
51ec329
fix stdin syntax
xacrimon Oct 10, 2022
285a9d3
comments + more expression translation code
xacrimon Oct 10, 2022
8d4ce86
remove unused policy bit
xacrimon Oct 10, 2022
8d4e382
fill in scopes for access request & review
xacrimon Oct 10, 2022
55a1539
map string set equals correctly
xacrimon Oct 10, 2022
5d6bb1c
format
xacrimon Oct 10, 2022
9722e2d
implement translation for basic logical operators and types
xacrimon Oct 10, 2022
f0e15f0
implement translation for concat + split
xacrimon Oct 10, 2022
ae820b1
implement translation of further primitives
xacrimon Oct 10, 2022
f7a0748
reformat
xacrimon Oct 11, 2022
36690d4
add select translation support for policy mapping
xacrimon Oct 11, 2022
feb9489
merge identical branches
xacrimon Oct 11, 2022
4a5786b
finish adding translation code
xacrimon Oct 11, 2022
0375d1a
(feedback): group scopes as subfields
xacrimon Oct 11, 2022
dc3b6e4
resolve isort conflict
xacrimon Oct 11, 2022
be30e66
replace explicit scope field with decorator
xacrimon Oct 11, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions predicate/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import subprocess
from runpy import run_path
from types import FunctionType

Expand Down Expand Up @@ -80,6 +81,25 @@ def export(policy_file):
click.echo(serialized)


@main.command()
@click.argument("policy-file")
@click.option("--sudo", "-s", is_flag=True)
def deploy(policy_file, sudo):
click.echo("parsing policy...")
module = run_path(policy_file, env)
policy = module["Teleport"].p
click.echo("translating policy...")
obj = policy.export()
serialized = yaml.dump(obj)
click.echo("deploying policy...")
args = ["tctl", "create", "-f"]
if sudo:
args.insert(0, "sudo")

subprocess.run(args, text=True, input=serialized, check=True)
click.echo(f'policy deployed as resource "{policy.name}"')


@main.command()
@click.argument("policy-file")
def test(policy_file):
Expand Down
33 changes: 21 additions & 12 deletions predicate/examples/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,36 @@ class Teleport:
name="access",
loud=False,
allow=Rules(
Node((Node.login == User.name)),
Node(
((Node.login == User.name) & (User.name != "root"))
| (User.traits["team"] == ("admins",))
),
),
options=OptionsSet(Options((Options.max_session_ttl < Duration.new(hours=10)))),
deny=Rules(
Node((Node.login == "mike")),
Node(
(Node.login == "mike")
| (Node.login == "jester")
| (Node.labels["env"] == "prod")
),
),
)

def test_access(self):
# Alice will be able to login to any machine as herself
ret, _ = self.p.check(Node((Node.login == "alice") & (User.name == "alice")))
ret, _ = self.p.check(
Node(
(Node.login == "alice")
& (User.name == "alice")
& (Node.labels["env"] == "dev")
)
)
assert ret is True, "Alice can login with her user to any node"

# We can verify that a strong invariant holds:
# Unless a username is root, a user can not access a server as
# root. This creates a problem though, can we deny access as root
# altogether?
ret, _ = self.p.check(Node((Node.login == "root") & (User.name != "root")))
assert (
ret is False
), "This role does not allow access as root unless a user name is root"

# No one is permitted to login as mike
ret, _ = self.p.query(Node((Node.login == "mike")))
assert ret is False, "This role does not allow access as mike"

# No one is permitted to login as jester
ret, _ = self.p.query(Node((Node.login == "jester")))
assert ret is False, "This role does not allow access as jester"
3 changes: 3 additions & 0 deletions predicate/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ source = "."
extra-requirements = "types-requests"
use-mypy = true

[tool.isort]
profile = "black"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
130 changes: 113 additions & 17 deletions predicate/solver/teleport.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
from . import ast
from .errors import ParameterError

def scoped(cls):
cls.scope = cls.__name__.lower()
return cls

class Options(ast.Predicate):
"""
Expand Down Expand Up @@ -55,6 +58,7 @@ def collect_like(self, other: ast.Predicate):
]


@scoped
class Node(ast.Predicate):
"""
Node is SSH node
Expand Down Expand Up @@ -179,15 +183,15 @@ class RequestPolicy:
# denials is a list of recorded approvals for policy
denials = ast.StringSetMap("policy.denials")


@scoped
class Request(ast.Predicate):
def __init__(self, expr):
ast.Predicate.__init__(self, expr)

def traverse(self):
return self.expr.traverse()


@scoped
class Review(ast.Predicate):
def __init__(self, expr):
ast.Predicate.__init__(self, expr)
Expand All @@ -205,22 +209,102 @@ def collect_like(self, other: ast.Predicate):
return [r for r in self.rules if r.__class__ == other.__class__]


# TODO: not really sure how I want to structure this logic
# as I'd like to keep the logic for non-teleport specific ast elements close to their
# definitions, but I am not sure if this is doable without a lot of complexity as the output
# format is tied to whatever builds on top of the general ast
# this might do for now
def transform_expr(predicate):
# t_expr transforms a predicate-lang expression into a Teleport predicate expression which can be evaluated.
def t_expr(predicate):
if isinstance(predicate, ast.Predicate):
return transform_expr(predicate.expr)
return t_expr(predicate.expr)
elif isinstance(predicate, ast.Eq):
return f"({transform_expr(predicate.L)} == {transform_expr(predicate.R)})"
elif isinstance(predicate, ast.String):
return f"({t_expr(predicate.L)} == {t_expr(predicate.R)})"
elif isinstance(predicate, ast.Or):
return f"({t_expr(predicate.L)} || {t_expr(predicate.R)})"
elif isinstance(predicate, ast.And):
return f"({t_expr(predicate.L)} && {t_expr(predicate.R)})"
elif isinstance(predicate, ast.Xor):
return f"({t_expr(predicate.L)} ^ {t_expr(predicate.R)})"
elif isinstance(predicate, ast.Not):
return f"(!{t_expr(predicate.V)})"
elif isinstance(predicate, ast.Lt):
return f"({t_expr(predicate.L)} < {t_expr(predicate.R)})"
elif isinstance(predicate, ast.Gt):
return f"({t_expr(predicate.L)} > {t_expr(predicate.R)})"
elif isinstance(predicate, ast.MapIndex):
return f"{predicate.m.name}[{t_expr(predicate.key)}]"
elif isinstance(
predicate,
(
ast.String,
ast.Duration,
ast.StringList,
ast.StringEnum,
ast.Bool,
ast.Int,
ast.StringSetMap,
),
):
return predicate.name
elif isinstance(predicate, ast.StringLiteral):
return f'"{predicate.V}"'
elif isinstance(predicate, str):
return f'"{predicate}"'
elif isinstance(predicate, tuple):
return f"[{', '.join(t_expr(p) for p in predicate)}]"
elif isinstance(predicate, (ast.BoolLiteral, ast.IntLiteral, ast.DurationLiteral)):
return f"{predicate.V}"
elif isinstance(predicate, ast.Concat):
return f"({t_expr(predicate.L)} + {t_expr(predicate.R)})"
elif isinstance(predicate, ast.Split):
return f"split({t_expr(predicate.val)}, {t_expr(predicate.sep)})"
elif isinstance(predicate, ast.StringTuple):
return f"[{', '.join(t_expr(p) for p in predicate.vals)}]"
elif isinstance(predicate, ast.Upper):
return f"upper({t_expr(predicate.val)})"
elif isinstance(predicate, ast.Lower):
return f"lower({t_expr(predicate.val)})"
elif isinstance(
predicate,
(ast.StringListContains, ast.IterableContains, ast.StringSetMapIndexContains),
):
return f"contains({t_expr(predicate.E)}, {t_expr(predicate.V)})"
elif isinstance(predicate, ast.StringListFirst):
return f"first({t_expr(predicate.E)})"
elif isinstance(predicate, (ast.StringListAdd, ast.StringSetMapIndexAdd)):
return f"add({t_expr(predicate.E)}, {t_expr(predicate.V)})"
elif isinstance(predicate, (ast.StringListEquals, ast.StringSetMapIndexEquals)):
return f"equals({t_expr(predicate.E)}, {t_expr(predicate.V)})"
elif isinstance(
predicate, (ast.Replace, ast.StringListReplace, ast.StringSetMapIndexReplace)
):
return f"replace({t_expr(predicate.val)}, {t_expr(predicate.src)}, {t_expr(predicate.dst)})"
elif isinstance(predicate, ast.RegexConstraint):
return f"regex({t_expr(predicate.expr)})"
elif isinstance(predicate, ast.RegexTuple):
return f"[{', '.join(t_expr(p) for p in predicate.vals)}]"
elif isinstance(predicate, (ast.Matches, ast.IterableMatches)):
return f"matches({t_expr(predicate.E)}, {t_expr(predicate.V)})"
elif isinstance(
predicate, (ast.StringListContainsRegex, ast.StringSetMapIndexContainsRegex)
):
return f"contains_regex({t_expr(predicate.E)}, {t_expr(predicate.V)})"
elif isinstance(predicate, ast.If):
return f"if({t_expr(predicate.cond)}, {t_expr(predicate.on_true)}, {t_expr(predicate.on_false)})"
elif isinstance(predicate, ast.Select):
return f"select([{', '.join(t_expr(p) for p in predicate.cases)}], {t_expr(predicate.default)})"
elif isinstance(predicate, ast.Case):
return f"case({t_expr(predicate.when)}, {t_expr(predicate.then)})"
elif isinstance(predicate, ast.Default):
return f"default({t_expr(predicate.expr)})"
elif isinstance(predicate, ast.StringSetMapIndex):
return f"{predicate.m.name}[{t_expr(predicate.key)}]"
elif isinstance(predicate, ast.StringSetMapIndexLen):
return f"len({t_expr(predicate.E)})"
elif isinstance(predicate, ast.StringSetMapIndexFirst):
return f"first({t_expr(predicate.E)})"
elif isinstance(predicate, ast.StringSetMapAddValue):
return f"map_add({t_expr(predicate.m.name)}, {t_expr(predicate.K)}, {t_expr(predicate.V)})"
elif isinstance(predicate, ast.StringSetMapRemoveKeys):
return f"map_remove({t_expr(predicate.m.name)}, {t_expr(predicate.K)})"
else:
return str(predicate)
raise Exception(f"unknown predicate type: {type(predicate)}")


class Policy:
Expand Down Expand Up @@ -261,17 +345,29 @@ def export(self):
"spec": {},
}

def group_rules(operator, rules):
scopes = {}
for rule in rules:
if rule.scope not in scopes:
scopes[rule.scope] = []

scopes[rule.scope].append(rule)

for scope, rules in scopes.items():
expr = functools.reduce(operator, rules)
scopes[scope] = t_expr(expr)

return scopes

if self.options.options:
options_rules = functools.reduce(operator.and_, self.options.options)
out["spec"]["options"] = transform_expr(options_rules)
out["spec"]["options"] = t_expr(options_rules)

if self.allow.rules:
allow_rules = functools.reduce(operator.or_, self.allow.rules)
out["spec"]["allow"] = transform_expr(allow_rules)
out["spec"]["allow"] = group_rules(operator.or_, self.allow.rules)

if self.deny.rules:
deny_rules = functools.reduce(operator.and_, self.deny.rules)
out["spec"]["deny"] = transform_expr(deny_rules)
out["spec"]["deny"] = group_rules(operator.and_, self.deny.rules)

return out

Expand Down