Skip to content

Commit

Permalink
tags as versions for acceptable task rules
Browse files Browse the repository at this point in the history
This contains two major changes.

One is that the logic in `lib.bundles`
is written to in the form prose (or close enough) to allow for easier
readings of the logic. Instead of the notion of a task collection and
equality of task bundle references, the code now considers business
logic terms, like "is acceptable", "is out of date", "is expired" and
"newer version exists".

And the second is that tags provided in acceptable task bundle data are
now considered in rules. If tags are provided in acceptable task bundle
they further refine the set of records from acceptable task bundle data
that are examined, i.e. different versions do not interfere with each
other.

For task bundle references tags are computed from the acceptable task
bundle data records by matching against the digest.

reference: EC-223
  • Loading branch information
zregvart committed Nov 10, 2023
1 parent 1410405 commit 913c2b5
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 94 deletions.
177 changes: 131 additions & 46 deletions policy/lib/bundles.rego
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import data.lib.image
import data.lib.refs
import data.lib.time as time_lib

# Return the bundle reference as is
bundle(task) := refs.task_ref(task).bundle

# Returns a subset of tasks that do not use a bundle reference.
disallowed_task_reference(tasks) := {task |
some task in tasks
Expand All @@ -15,83 +18,165 @@ disallowed_task_reference(tasks) := {task |
# Returns a subset of tasks that use an empty bundle reference.
empty_task_bundle_reference(tasks) := {task |
some task in tasks
bundle(task) == ""
refs.task_ref(task).bundle == ""
}

# Returns a subset of tasks that use bundle references not pinned to a digest.
unpinned_task_bundle(tasks) := {task |
some task in tasks
ref := image.parse(bundle(task))
ref := image.parse(_bundle_ref(task, data["task-bundles"]))
ref.digest == ""
}

# Returns if the required task-bundles data is missing
default missing_task_bundles_data := false

missing_task_bundles_data {
count(data["task-bundles"]) == 0
}

# Returns a subset of tasks that use an acceptable bundle reference, but
# an updated bundle reference exists.
out_of_date_task_bundle(tasks) := {task |
some task in tasks
ref := image.parse(bundle(task))
collection := _collection(ref)

some match_index, out_of_date in collection
is_equal(out_of_date, ref)
match_index > 0
ref := image.parse(_bundle_ref(task, data["task-bundles"]))

_is_out_of_date(ref)
}

# Returns a subset of tasks that do not use an acceptable bundle reference.
unacceptable_task_bundle(tasks) := {task |
some task in tasks
ref := image.parse(bundle(task))
collection := _collection(ref)

matches := [record |
some record in collection
is_equal(record, ref)
]
ref := image.parse(_bundle_ref(task, data["task-bundles"]))

count(matches) == 0
_is_unacceptable(ref)
}

# Returns if the required task-bundles data is missing
default missing_task_bundles_data := false
_is_unacceptable(ref) {
not _is_acceptable(ref)
}

missing_task_bundles_data {
count(data["task-bundles"]) == 0
_is_unacceptable(ref) {
_newer_version_exists(ref, data["task-bundles"])
_is_expired(ref, data["task-bundles"])
}

# Returns true if the provided bundle reference is acceptable
is_acceptable(bundle_ref) {
ref := image.parse(bundle_ref)
collection := _collection(ref)
matches := [r |
some r in collection
is_equal(r, ref)
]
_is_acceptable(ref) {
# all records in acceptable task bundles for the given repository
records := data["task-bundles"][ref.repo]

some record in records

count(matches) > 0
# an acceptable task bundle reference is one that is recorded in the
# acceptable task bundles, this is done by matching it's digest; note no
# care is given to the expiry or freshness
record.digest == ref.digest
}

# Returns whether or not the ref matches the digest of the record.
is_equal(record, ref) := match {
ref.digest != ""
match := record.digest == ref.digest
# Out of date references are those that are acceptable, meaning that their
# reference is recorded in acceptable task bundles data and no newer version
# exists or the reference has expired
_is_out_of_date(ref) {
_is_acceptable(ref)
_newer_version_exists(ref, data["task-bundles"])
not _is_expired(ref, data["task-bundles"])
}

# Returns whether or not the ref matches the tag of the record as a fallback
# in case the digest is blank for the ref. This is a weaker comparison as,
# unlike digests, tags are not immutable entities. It is expected that a
# missing digest results in a warning whenever possible.
is_equal(record, ref) := match {
ref.digest == ""
match := record.tag == ref.tag
_is_out_of_date(ref) {
_is_acceptable(ref)
not _newer_version_exists(ref, data["task-bundles"])
_is_expired(ref, data["task-bundles"])
}

bundle(task) := refs.task_ref(task).bundle
# Evaluates to true if the tasks bundle reference is found in the acceptable
# task bundles data, matched by digest, and it is not in effect, meaning that
# it's effective_on is in the future, i.e. it has expired
_is_expired(ref, acceptable) {
# all records in acceptable task bundles for the given repository
records := acceptable[ref.repo]

some record in records

# consider all records, if a match is found via exact digest and it's
# effective_on is greater than current time in effect (remember it is
# configurable) we deem the bundle reference out of date
record.digest == ref.digest

time_lib.effective_current_time_ns > time.parse_rfc3339_ns(record.effective_on)
}

# _collection returns an array representing the full list of records to
# be taken into consideration when evaluating policy rules for bundle
# references. Any irrelevant records are filtered out from the array.
# (The else condition is for when data["task-bundles"][ref.repo] doesn't exist.)
_collection(ref) := items {
full_collection := data["task-bundles"][ref.repo]
items := time_lib.acceptable_items(full_collection)
} else := []
# Evaluates to true if the tasks bundle reference is found in the acceptable
# task bundles data, but also in the data there is a newer version of the task,
# i.e. has a effective_on that is newer than the provided reference's
# effective_on; two references are considered belonging to the same version if
# they have the same tag
_newer_version_exists(ref, acceptable) {
# all records in acceptable task bundles for the given repository
records := acceptable[ref.repo]

some record in records

# consider all records, if a match is found via exact digest and there
# exists a newer record for the same tag but it is newer, i.e. has greater
# effective_on value
record.digest == ref.digest

some other in records

record.tag == other.tag

time.parse_rfc3339_ns(other.effective_on) > time.parse_rfc3339_ns(record.effective_on)
}

# Determine the image reference of the task bundle, if the provided task bundle
# image reference doesn't have the tag within it try to lookup the tag from the
# acceptable task bundles data
_bundle_ref(task, _) := ref {
ref := refs.task_ref(task).bundle
img := image.parse(ref)
img.tag != ""
}

_bundle_ref(task, acceptable) := ref {
ref_no_tag := refs.task_ref(task).bundle
img := image.parse(ref_no_tag)
img.tag == ""

# try to find the tag for the reference based on it's digest
records := acceptable[img.repo]

some record in records
record.digest == img.digest
record.tag

ref := image.str({
"digest": img.digest,
"repo": img.repo,
"tag": record.tag,
})
}

_bundle_ref(task, acceptable) := ref {
ref_no_tag := refs.task_ref(task).bundle
img := image.parse(ref_no_tag)
img.tag == ""

records := acceptable[img.repo]

count([r | some r in records; r.digest == img.digest]) == 0

ref := ref_no_tag
}

_bundle_ref(task, acceptable) := ref {
ref_no_tag := refs.task_ref(task).bundle
img := image.parse(ref_no_tag)
img.tag == ""

not acceptable[img.repo]

ref := ref_no_tag
}
55 changes: 17 additions & 38 deletions policy/lib/bundles_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import future.keywords.in

import data.lib
import data.lib.bundles
import data.lib.image

# used as reference bundle data in tests
bundle_data := {"registry.img/acceptable": [{
Expand All @@ -21,7 +22,7 @@ test_disallowed_task_reference {
{"name": "my-task-2", "ref": {}},
]

expected := {task | some task in tasks}
expected := lib.to_set(tasks)
lib.assert_equal(bundles.disallowed_task_reference(tasks), expected)
}

Expand All @@ -31,7 +32,7 @@ test_empty_task_bundle_reference {
{"name": "my-task-2", "ref": {"bundle": ""}},
]

expected := {task | some task in tasks}
expected := lib.to_set(tasks)
lib.assert_equal(bundles.empty_task_bundle_reference(tasks), expected)
}

Expand All @@ -47,8 +48,8 @@ test_unpinned_task_bundle {
},
]

expected := {task | some task in tasks}
lib.assert_equal(bundles.unpinned_task_bundle(tasks), expected)
expected := lib.to_set(tasks)
lib.assert_equal(bundles.unpinned_task_bundle(tasks), expected) with data["task-bundles"] as []
}

# All good when the most recent bundle is used.
Expand All @@ -68,76 +69,54 @@ test_acceptable_bundle {
test_out_of_date_task_bundle {
tasks := [
{"name": "my-task-1", "taskRef": {"bundle": "reg.com/repo@sha256:bcd"}},
{"name": "my-task-2", "taskRef": {"bundle": "reg.com/repo@sha256:cde"}},
{"name": "my-task-3", "ref": {"bundle": "reg.com/repo@sha256:bcd"}},
{"name": "my-task-4", "ref": {"bundle": "reg.com/repo@sha256:cde"}},
]

expected := {task | some task in tasks}
expected := lib.to_set(tasks)
lib.assert_equal(bundles.out_of_date_task_bundle(tasks), expected) with data["task-bundles"] as task_bundles
}

test_unacceptable_task_bundles {
tasks := [
{"name": "my-task-1", "taskRef": {"bundle": "reg.com/repo@sha256:def"}},
{"name": "my-task-2", "ref": {"bundle": "reg.com/repo@sha256:def"}},
{"name": "my-task-1", "taskRef": {"bundle": "reg.com/repo@sha256:blah"}},
{"name": "my-task-2", "ref": {"bundle": "reg.com/repo@sha256:blah"}},
{"name": "my-task-3", "ref": {"bundle": "wat.com/repo@sha256:blah"}},
]

expected := {task | some task in tasks}
expected := lib.to_set(tasks)
lib.assert_equal(bundles.unacceptable_task_bundle(tasks), expected) with data["task-bundles"] as task_bundles
}

test_is_equal {
record := {"digest": "sha256:abc", "tag": "spam"}

# Exact match
lib.assert_equal(bundles.is_equal(record, {"digest": "sha256:abc", "tag": "spam"}), true)

# Tag is ignored if digest matches
lib.assert_equal(bundles.is_equal(record, {"digest": "sha256:abc", "tag": "not-spam"}), true)

# Tag is not required
lib.assert_equal(bundles.is_equal(record, {"digest": "sha256:abc", "tag": ""}), true)

# When digest is missing on ref, compare tag
lib.assert_equal(bundles.is_equal(record, {"digest": "", "tag": "spam"}), true)

# If digest does not match, tag is still ignored
lib.assert_equal(bundles.is_equal(record, {"digest": "sha256:bcd", "tag": "spam"}), false)

# No match is honored when digest is missing
lib.assert_equal(bundles.is_equal(record, {"digest": "", "tag": "not-spam"}), false)
}

task_bundles := {"reg.com/repo": [
{
"digest": "sha256:abc", # Allow
"tag": "903d49a833d22f359bce3d67b15b006e1197bae5",
"tag": "v1",
"effective_on": "2262-04-11T00:00:00Z",
},
{
"digest": "sha256:bcd", # Warn
"tag": "b7d8f6ae908641f5f2309ee6a9d6b2b83a56e1af",
"tag": "v1",
"effective_on": "2262-03-11T00:00:00Z",
},
{
"digest": "sha256:cde", # Warn
"tag": "120dda49a6cc3b89516b491e19fe1f3a07f1427f",
"tag": "v1",
"effective_on": "2022-02-01T00:00:00Z",
},
{
"digest": "sha256:def", # Warn
"tag": "903d49a833d22f359bce3d67b15b006e1197bae5",
"tag": "v1",
"effective_on": "2021-01-01T00:00:00Z",
},
]}

test_acceptable_bundle_is_acceptable {
bundles.is_acceptable(acceptable_bundle_ref) with data["task-bundles"] as bundle_data
bundles._is_acceptable(image.parse(acceptable_bundle_ref)) with data["task-bundles"] as bundle_data
}

test_unacceptable_bundle_is_unacceptable {
not bundles.is_acceptable("registry.img/unacceptable@sha256:digest") with data["task-bundles"] as bundle_data
ref := "registry.img/unacceptable@sha256:digest"
not bundles._is_acceptable(image.parse(ref)) with data["task-bundles"] as bundle_data
}

test_missing_required_data {
Expand Down
Loading

0 comments on commit 913c2b5

Please sign in to comment.