# Access Profiles Original

Access Profiles (ACPs) are a way of expressing the set of actions which accounts are permitted to
perform on database records (`object`) in the system.

As a result, there are specific requirements to what these can control and how they are expressed.

Access profiles define an action of `allow` or `deny`: `deny` has priority over `allow` and will
override even if applicable. They should only be created by system access profiles because certain
changes must be denied.

Access profiles are stored as entries and are dynamically loaded into a structure that is more
efficient for use at runtime. `Schema` and its transactions are a similar implementation.

## Search Requirements

A search access profile must be able to limit:

1. the content of a search request and its scope.
2. the set of data returned from the objects visible.

An example:

> Alice should only be able to search for objects where the class is `person` and the object is a
> memberOf the group called "visible".
>
> Alice should only be able to see those the attribute `displayName` for those users (not their
> `legalName`), and their public `email`.

Worded a bit differently. You need permission over the scope of entries, you need to be able to read
the attribute to filter on it, and you need to be able to read the attribute to receive it in the
result entry.

If Alice searches for `(&(name=william)(secretdata=x))`, we should not allow this to proceed because
Alice doesn't have the rights to read secret data, so they should not be allowed to filter on it.
How does this work with two overlapping ACPs? For example: one that allows read of name and
description to class = group, and one that allows name to user. We don't want to say
`(&(name=x)(description=foo))` and it to be allowed, because we don't know the target class of the
filter. Do we "unmatch" all users because they have no access to the filter components? (Could be
done by inverting and putting in an AndNot of the non-matchable overlaps). Or do we just filter our
description from the users returned (But that implies they DID match, which is a disclosure).

More concrete:

```text
search {
    action: allow
    targetscope: Eq("class", "group")
    targetattr: name
    targetattr: description
}

search {
    action: allow
    targetscope: Eq("class", "user")
    targetattr: name
}

SearchRequest {
    ...
    filter: And: {
        Pres("name"),
        Pres("description"),
    }
}
```

A potential defense is:

```text
acp class group: Pres(name) and Pres(desc) both in target attr, allow
acp class user: Pres(name) allow, Pres(desc) deny. Invert and Append
```

So the filter now is:

```text
And: {
    AndNot: {
        Eq("class", "user")
    },
    And: {
        Pres("name"),
        Pres("description"),
    },
}
```

This would now only allow access to the `name` and `description` of the class `group`.

If we extend this to a third, this would work. A more complex example:

```text
search {
    action: allow
    targetscope: Eq("class", "group")
    targetattr: name
    targetattr: description
}

search {
    action: allow
    targetscope: Eq("class", "user")
    targetattr: name
}

search {
    action: allow
    targetscope: And(Eq("class", "user"), Eq("name", "william"))
    targetattr: description
}
```

Now we have a single user where we can read `description`. So the compiled filter above as:

```text
And: {
    AndNot: {
        Eq("class", "user")
    },
    And: {
        Pres("name"),
        Pres("description"),
    },
}
```

This would now be invalid, first, because we would see that `class=user` and `william` has no name
so that would be excluded also. We also may not even have "class=user" in the second ACP, so we
can't use subset filter matching to merge the two.

As a result, I think the only possible valid solution is to perform the initial filter, then
determine on the candidates if we _could_ have have valid access to filter on all required
attributes. IE this means even with an index look up, we still are required to perform some filter
application on the candidates.

I think this will mean on a possible candidate, we have to apply all ACP, then create a union of the
resulting targetattrs, and then compared that set into the set of attributes in the filter.

This will be slow on large candidate sets (potentially), but could be sped up with parallelism,
caching or other methods. However, in the same step, we can also apply the step of extracting only
the allowed read target attrs, so this is a valuable exercise.

## Delete Requirements

A `delete` profile must contain the `content` and `scope` of a delete.

An example:

> Alice should only be able to delete objects where the `memberOf` is `purgeable`, and where they
> are not marked as `protected`.

## Create Requirements

A `create` profile defines the following limits to what objects can be created, through the
combination of filters and attributes.

An example:

> Alice should only be able to create objects where the `class` is `group`, and can only name the
> group, but they cannot add members to the group.

An example of a content requirement could be something like "the value of an attribute must pass a
regular expression filter". This could limit a user to creating a group of any name, except where
the group's name contains "admin". This a contrived example which is also possible with filtering,
but more complex requirements are possible.

For example, we want to be able to limit the classes that someone _could_ create on an object
because classes often are used in security rules.

## Modify Requirements

A `modify` profile defines the following limits:

- a filter for which objects can be modified,
- a set of attributes which can be modified.

A `modify` profile defines a limit on the `modlist` actions.

For example: you may only be allowed to ensure `presence` of a value. (Modify allowing purge,
not-present, and presence).

Content requirements (see [Create Requirements](#create-requirements)) are out of scope at the
moment.

An example:

Alice should only be able to modify a user's password if that user is a member of the students
group.

> [!NOTE]
>
> `modify` does not imply `read` of the attribute. Care should be taken that we don't disclose the
> current value in any error messages if the operation fails.

## Targeting Requirements

The `target` of an access profile should be a filter defining the objects that this applies to.

The filter limit for the profiles of what they are acting on requires a single special operation
which is the concept of "targeting self".

For example: we could define a rule that says "members of group X are allowed self-write to the
`mobilePhoneNumber` attribute".

An extension to the filter code could allow an extra filter enum of `self`, that would allow this to
operate correctly, and would consume the entry in the event as the target of "Self". This would be
best implemented as a compilation of `self -> eq(uuid, self.uuid)`.

## Implementation Details

CHANGE: Receiver should be a group, and should be single value/multivalue? Can _only_ be a group.

Example profiles:

```text
search {
    action: allow
    receiver: Eq("memberof", "admins")
    targetscope: Pres("class")
    targetattr: legalName
    targetattr: displayName
    description: Allow admins to read all users names
}

search {
    action: allow
    receiver: Self
    targetscope: Self
    targetattr: homeAddress
    description: Allow everyone to read only their own homeAddress
}

delete {
    action: allow
    receiver: Or(Eq("memberof", "admins), Eq("memberof", "servicedesk"))
    targetscope: Eq("memberof", "tempaccount")
    description: Allow admins or servicedesk to delete any member of "temp accounts".
}

// This difference in targetscope behaviour could be justification to change the keyword here
// to prevent confusion.
create {
    action: allow
    receiver: Eq("name", "alice")
    targetscope: And(Eq("class", "person"), Eq("location", "AU"))
    createattr: location
    createattr: legalName
    createattr: mail
    createclass: person
    createclass: object
    description: Allow alice to make new persons, only with class person+object, and only set
        the attributes mail, location and legalName. The created object must conform to targetscope
}

modify {
    action: allow
    receiver: Eq("name", "claire")
    targetscope: And(Eq("class", "group"), Eq("name", "admins"))
    presentattr: member
    description: Allow claire to promote people as members of the admins group.
}

modify {
    action: allow
    receiver: Eq("name", "claire")
    targetscope: And(Eq("class", "person"), Eq("memberof", "students"))
    presentattr: sshkeys
    presentattr: class
    targetclass: unixuser
    description: Allow claire to modify persons in the students group, and to grant them the
        class of unixuser (only this class can be granted!). Subsequently, she may then give
        the sshkeys values as a modification.
}

modify {
    action: allow
    receiver: Eq("name", "alice")
    targetscope: Eq("memberof", "students")
    removedattr: sshkeys
    description: Allow allice to purge or remove sshkeys from members of the students group,
        but not add new ones
}

modify {
    action: allow
    receiver: Eq("name", "alice")
    targetscope: Eq("memberof", "students")
    removedattr: sshkeys
    presentattr: sshkeys
    description: Allow alice full control over the ssh keys attribute on members of students.
}

// This may not be valid: Perhaps if <*>attr: is on modify/create, then targetclass, must
// must be set, else class is considered empty.
//
// This profile could in fact be an invalid example, because presentattr: class, but not
// targetclass, so nothing could be granted.
modify {
    action: allow
    receiver: Eq("name", "alice")
    targetscope: Eq("memberof", "students")
    presentattr: class
    description: Allow alice to grant any class to members of students.
}
```

## Formalised Schema

A complete schema would be:

### Attributes

| Name                   | Single/Multi | Type              | Description                                       |
| ---------------------- | ------------ | ----------------- | ------------------------------------------------- |
| acp_allow              | single value | bool              |                                                   |
| acp_enable             | single value | bool              | This ACP is enabled                               |
| acp_receiver           | single value | filter            | ???                                               |
| acp_targetscope        | single value | filter            | ???                                               |
| acp_search_attr        | multi value  | utf8 case insense | A list of attributes that can be searched.        |
| acp_create_class       | multi value  | utf8 case insense | Object classes in which an object can be created. |
| acp_create_attr        | multi value  | utf8 case insense | Attribute Entries that can be created.            |
| acp_modify_removedattr | multi value  | utf8 case insense | Modify if removed?                                |
| acp_modify_presentattr | multi value  | utf8 case insense | ???                                               |
| acp_modify_class       | multi value  | utf8 case insense | ???                                               |

### Classes

| Name                   | Must Have                         | May Have                                                             |
| ---------------------- | --------------------------------- | -------------------------------------------------------------------- |
| access_control_profile | `[acp_receiver, acp_targetscope]` | `[description, acp_allow]`                                           |
| access_control_search  | `[acp_search_attr]`               |                                                                      |
| access_control_delete  |                                   |                                                                      |
| access_control_modify  |                                   | `[acp_modify_removedattr, acp_modify_presentattr, acp_modify_class]` |
| access_control_create  |                                   | `[acp_create_class, acp_create_attr]`                                |

> [!NOTE]
>
> empty sets really mean empty sets!

The ACP code will assert that both `access_control_profile` _and_ one of the
`search/delete/modify/create` classes exists on an ACP. An important factor of this design is now
the ability to _compose_ multiple ACP's into a single entry allowing a `create/delete/modify` to
exist! However, each one must still list their respective actions to allow proper granularity.

## "Search" Application

The set of access controls is checked, and the set where receiver matches the current identified
user is collected. These then are added to the users requested search as:

```text
And(<User Search Request>, Or(<Set of Search Profile Filters))
```

In this manner, the search security is easily applied, as if the targets to conform to one of the
required search profile filters, the outer `And` condition is nullified and no results returned.

Once complete, in the translation of the entry -> proto_entry, each access control and its allowed
set of attrs has to be checked to determine what of that entry can be displayed. Consider there are
three entries, A, B, C. An ACI that allows read of "name" on A, B exists, and a read of "mail" on B,
C. The correct behaviour is then:

```text
A: name
B: name, mail
C: mail
```

So this means that the `entry -> proto entry` part is likely the most expensive part of the access
control operation, but also one of the most important. It may be possible to compile to some kind of
faster method, but initially a simple version is needed.

## "Delete" Application

Delete is similar to search, however there is the risk that the user may say something like:

```text
Pres("class").
```

Were we to approach this like search, this would then have "every thing the identified user is
allowed to delete, is deleted". A consideration here is that `Pres("class")` would delete "all"
objects in the directory, but with the access control present, it would limit the deletion to the
set of allowed deletes.

In a sense this is a correct behaviour - they were allowed to delete everything they asked to
delete. However, in another it's not valid: the request was broad and they were not allowed access
to delete everything they requested.

The possible abuse vector here is that an attacker could then use delete requests to enumerate the
existence of entries in the database that they do not have access to. This requires someone to have
the delete privilege which in itself is very high level of access, so this risk may be minimal.

So the choices are:

1. Treat it like search and allow the user to delete what they are allowed to delete, but ignore
   other objects
2. Deny the request because their delete was too broad, and they must specify a valid deletion
   request.

Option #2 seems more correct because the `delete` request is an explicit request, not a request
where you want partial results. Imagine someone wants to delete users A and B at the same time, but
only has access to A. They want this request to fail so they KNOW B was not deleted, rather than it
succeed and have B still exist with a partial delete status.

However, a possible issue is that Option #2 means that a delete request of
`And(Eq(attr, allowed_attribute), Eq(attr, denied))`, which is rejected may indicate presence of the
`denied` attribute. So option #1 may help in preventing a security risk of information disclosure.

<!-- TODO
@yaleman: not always, it could indicate that the attribute doesn't exist so it's an invalid filter, but
    that would depend if the response was "invalid" in both cases, or "invalid" / "refused"
-->

This is also a concern for modification, where the modification attempt may or may not fail
depending on the entries and if you can/can't see them.

> [!NOTE]
>
> You can only `delete`/`modify` within the read scope you have. If you can't read it (based on the
> read rules of `search`), you can't `delete` it. This is in addition to the filter rules of the
> `delete` applying as well. So performing a `delete` of `Pres(class)`, will only delete in your
> `read` scope and will never disclose if you are denied access.

<!-- TODO
@yaleman: This goes back to the commentary on Option #2 and feels icky like SQL's `DELETE FROM <table>` just deleting everything. It's more complex from the client - you have to search for a set of things to delete - then delete them.
Explicitly listing the objects you want to delete feels.... way less bad. This applies to modifies too.  😁
-->

## "Create" Application

Create seems like the easiest to apply. Ensure that only the attributes in `createattr` are in the
`createevent`, ensure the classes only contain the set in `createclass`, then finally apply
`filter_no_index` to the entry to entry. If all of this passes, the create is allowed.

A key point is that there is no union of `create` ACI's - the WHOLE ACI must pass, not parts of
multiple. This means if a control say "allows creating group with member" and "allows creating user
with name", creating a group with `name` is not allowed - despite your ability to create an entry
with `name`, its classes don't match. This way, the administrator of the service can define create
controls with specific intent for how they will be used without the risk of two controls causing
unintended effects (`users` that are also `groups`, or allowing invalid values.

An important consideration is how to handle overlapping ACI. If two ACI _could_ match the create
should we enforce both conditions are upheld? Or only a single upheld ACI allows the create?

In some cases it may not be possible to satisfy both, and that would block creates. The intent of
the access profile is that "something like this CAN" be created, so I believe that provided only a
single control passes, the create should be allowed.

## "Modify" Application

Modify is similar to Create, however we specifically filter on the `modlist` action of `present`,
`removed` or `purged` with the action. The rules of create still apply; provided all requirements of
the modify are permitted, then it is allowed once at least one profile allows the change.

A key difference is that if the modify ACP lists multiple `presentattr` types, the modify request is
valid if it is only modifying one attribute. IE we say `presentattr: name, email`, but we only
attempt to modify `email`.

## Considerations

- When should access controls be applied? During an operation, we only validate schema after pre*
  Plugin application, so likely it has to be "at that point", to ensure schema-based validity of the
  entries that are allowed to be changed.
- Self filter keyword should compile to `eq("uuid",  "....")`. When do we do this and how?
- `memberof` could take `name` or `uuid`, we need to be able to resolve this correctly, but this is
  likely an issue in `memberof` which needs to be addressed, ie `memberof uuid` vs `memberof attr`.
- Content controls in `create` and `modify` will be important to get right to avoid the security
  issues of LDAP access controls. Given that `class` has special importance, it's only right to give
  it extra consideration in these controls.
- In the future when `recyclebin` is added, a `re-animation` access profile should be created
  allowing revival of entries given certain conditions of the entry we are attempting to revive. A
  service-desk user should not be able to revive a deleted high-privilege user.
