riam
riam copied to clipboard
Add Support for Conditions
Design interface and types for supporting policy conditions.
See examples and descriptions in AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
I've started implementing this in the conditions branch and I'm going to layout a few design considerations here for posterity. I'll preface this by saying the goal of riam is not to be 100% the same as AWS IAM policies. We couldn't if we tried since AWS is a closed system where they control the request context, have known actions, resources, condition keys, etc. Instead you could build a system like AWS IAM on top of riam (hopefully).
We are going to deviate a bit from standard AWS like conditions in the following ways:
-
The serialization format will not be the same.
- AWS conditions use a map structure
"conditions": { "StringEquals": {"k1": "v1"} }
- Riam will use a sequence of conditions instead
"conditions": [ {"StringEquals": {"k1": "v1"}} ]
- Rationale: The serialization and de-serialization becomes MUCH simpler in Rust/serde. We don't require nearly as much custom serialization logic and I don't see the resulting change in writing policies to add complexity or hinder understanding.
-
I am going to leave off
IfExists
variants andNull
condition to start off with. I have ideas on how to generically supportIfExists
but I'm not in love with it any of them yet so I'm going to hold off for now. -
I have no plans of implementing Arn* conditions. It wouldn't be hard to add but I don't think they really make sense outside of AWS's closed environment.
-
Currently the behavior of
StringLike/StringNotLike
does not support the single character?
wildcard. This will likely be fixed at some point but we levered the action/resource wildcard matching to bootstrap an initial implementation. I think have?
support for those types would be a welcome addition and something (I believe) is not supported by AWS IAM matching of those policy elements.
Other considerations at the library level:
I don't necessarily expect this library will be used by anyone other than myself but nonetheless I could see it being useful embedded into an application to simplify decision logic. To that end:
- To ease serialization we have chosen to implement conditions as an enum. The primary fallout/drawback of this decision is we have a closed system (violates open-closed principle). In other words library users cannot extend with a custom condition type.
- The alternative would be some a
Condition
trait and policy statements would beVec<Box<dyn Condition>>
or something. The issue with this is serialization. We would have to put serialize/deserialize trait bounds onCondition
as well as have some kind of type registry probably. It's entirely possible I missed something here and would enertain revisiting if we can come up with something elegant. - We are making use of the enum_dispatch crate to derive the
Eval
trait for theCondition
enum (basically devirtualization).
- The alternative would be some a
Questions:
- Should we support known condition keys? If so what set of known keys can/do we support?
- I think at a minimum
riam:CurrentTime
andriam:EpochTime
to support the date conditions - I think at an HTTP API level where we are responsible for gathering (at least parts of the request context). We could also support e.g.
riam:SecureTransport
,riam:UserAgent
,riam:SourceIP
etc. where we control what goes into those keys.
- I think at a minimum
I think on second thought we need support for IfExists
, and the set operators ForAnyValue
/ForAllValues
(see here).
If not out of the gate, immediately following.
The description for these operators is:
-
ForAllValues
– Tests whether the value of every member of the request set is a subset of the condition key set. The condition returns true if every key value in the request matches at least one value in the policy. It also returns true if there are no keys in the request, or if the key values resolve to a null data set, such as an empty string. -
ForAnyValue
– Tests whether at least one member of the set of request values matches at least one member of the set of condition key values. The condition returns true if any one of the key values in the request matches any one of the condition values in the policy. For no matching key or a null dataset, the condition returns false.
These basically allow us to deal with when a context value has more than one value for a key and the condition has more than one value for a key. These are set operators that either test if all of the values are in a set or at least one of the values is in the set (of permissible values defined in the condition).
An example of where this is useful: Let's say we have a policy where we want to allow unrestricted access if a principal belongs to some group. Without a set operator the caller to riam would not be able to express this (for one thing context currently doesn't allow sequences but for now let's say it did).
e.g.
{
"statements": [
{
"effect": "allow",
"action": "view",
"resource": "resource:foo:*",
"conditions": [
{"StringEquals": {"group": "admin"}}
]
}
]
}
The caller would have to formulate the request as:
{
"subject": "johndoe",
"action": "view",
"resource": "resource:foo:bar",
"context": {
"group": "admin"
}
}
This is too simplistic though. It's entirely likely that a person in any given system could belong to more than one group. They would have to formulate multiple requests for each group and test if at least one of them passes if we don't have support for set operations.
With set operators we would write it something like follows (note serialization format TBD):
Policy:
{
"statements": [
{
"effect": "allow",
"action": "view",
"resource": "resource:foo:*",
"conditions": [
{"ForAnyValue:StringEquals": {"groups": "admin"}}
]
}
]
}
which basically says test whether any of the values in the request context key groups
is present in the set("groups")
With such a policy we could then make the request:
{
"subject": "johndoe",
"action": "view",
"resource": "resource:foo:bar",
"context": {
"groups": ["admin", "group2", "group3"]
}
}
where "groups" is populated with all of the groups a user belongs to.