Skip to content

Unified label- and fieldSelector handling for both reads and writes #40

Open
@luxas

Description

@luxas

Category

Policy evaluation change

Describe the feature you'd like to request

I've been thinking if we somehow can make the UX for authorizing based on label and fieldSelectors more uniform.

Today, if I want to make it possible for someone to get pods with label foo=bar, I need to do the following for a read:

permit (
    principal == k8s::User::"lucas",
    action in [k8s::Action::"list", k8s::Action::"watch"],
    resource is k8s::Resource
) unless {
    resource has labelSelector &&
    resource.labelSelector.contains({
        "key": "owner",
        "operator": "=",
        "values": [principal.name]
    }, {
        "key": "owner",
        "operator": "==",
        "values": [principal.name]
    }, {
        "key": "owner",
        "operator": "in",
        "values": [principal.name]
    })
};

(we could fix fold the =, ==, and in maybe on the authorizer side to make things easier, however)

(also, the current state easily breaks, as we have a set within a set. If there was a permit rule with labelSelectors.contains({"key": "owner", "operator": "in", "values": ["a", "b"]}), then the policy author expects the principal to see objects with either owners. However, the latter policy actually isn't so easy to use, as if the user asks for ?labelSelector=owner=a, then the Cedar expression evaluates to false, and access is denied. Only if the user performed a ?labelSelector=owner in (a, b) would they be authorized. We could/should "flatten" the labelSelector on the Cedar side to two expressions with atomic key-value pairings, e.g. labelSelector = [{"key": "owner", "operator": "==", "value": "a"}, {"key": "owner", "operator": "==", "value": "b"], and then the policy can be written as labelSelectors.contains({"key": "owner", "operator": "==", "value": "a"} || labelSelectors.contains({"key": "owner", "operator": "==", "value": "b"}, which works both if the only one selector is applied)

If I want to make sure the same user can only create and update, we do the following right now (roughly):

permit (
    principal == k8s::User::"lucas",
    action in [
        k8s::admission::Action::"create",
        k8s::admission::Action::"update",
        k8s::admission::Action::"delete"],
    resource
) unless {
    resource has metadata &&
    resource.metadata has labels &&
    resource.metadata.labels.contains({"key": "owner", "value": principal.name})
};

One idea I got is: what if we kept the schema for labels (both for reads and writes) as an empty record, but guard with resource.labels has owner && resource.labels.owner == principal.name. This doesn't yield errors in e.g. VS Code, even though there isn't a owner property of the Labels type in the schema.

This means I could write the latter write policy like:

permit (
    principal == k8s::User::"lucas",
    action in [
        k8s::admission::Action::"create",
        k8s::admission::Action::"update",
        k8s::admission::Action::"delete"],
    resource
) when {
    resource.metadata.labels has owner && resource.metadata.labels.owner == principal.name
};

That seems a bit more natural, and works well for writes, where the object is concrete.

However, for reads, we don't have concrete objects. However, we maybe could do the same thing by internally (in the authorizer), running a check for every permutation of label and field selector paths in the original k8s request.

So for an example request of ?labelSelector=owner in (a, b),env in (dev, prod), there are four "archetypes" of objects matched:

  • owner=a && env=dev
  • owner=a && env=prod
  • owner=b && env=dev
  • owner=b && env=prod

thus, can we run 4 authorization checks against the policies, and enforce a logical AND between them, ensuring that whatever object could be selected by the read, is authorized

Now, the above read rule would apply like this:

permit (
    principal == k8s::User::"lucas",
    action in [k8s::Action::"list", k8s::Action::"watch"],
    resource is k8s::Resource
) when {
    resource.labels has owner && resource.labels.owner == principal.name
}

Notably, if there was another label (e.g. env, this rule would allow any value of the env label).

Now, notice that the read and write policies are the same! (and could even be folded in the same policy possibly).

Describe alternatives you've considered

Haven't had time to consider alternatives yet.

Additional context

No response

Is this something that you'd be interested in working on?

  • 👋 I may be able to implement this feature request
  • ⚠️ This feature might incur a breaking change

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions