Skip to content
This repository has been archived by the owner on Jan 30, 2020. It is now read-only.

Commit

Permalink
Merging master to develop in preparation for 2.7.0 release.
Browse files Browse the repository at this point in the history
  • Loading branch information
weierophinney committed May 1, 2018
2 parents e73fbfd + e10a01c commit c9568f4
Show file tree
Hide file tree
Showing 28 changed files with 1,715 additions and 225 deletions.
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@

All notable changes to this project will be documented in this file, in reverse chronological order by release.

## 2.7.0 - 2018-05-01

### Added

- [#23](https://github.com/zendframework/zend-permissions-acl/pull/23) adds a new assertion, `ExpressionAssertion`, to allow programatically or
automatically (from configuration) building standard comparison assertions
using a variety of operators, including `=` (`==`), `!=`, `<`, `<=`, `>`,
`>=`, `===`, `!==`, `in` (`in_array`), `!in` (`! in_array`), `regex`
(`preg_match`), and `!regex` (`! preg_match`). See https://docs.zendframework.com/zend-permissions-acl/expression/
for details on usage.

- [#3](https://github.com/zendframework/zend-permissions-acl/pull/3) adds two new interfaces designed to allow creation of ownership-based assertions
easier:

- `Zend\Permissions\Acl\ProprietaryInterface` is applicable to both roles and
resources, and provides the method `getOwnerId()` for retrieving the owner
role of an object.

- `Zend\Permissions\Acl\Assertion\OwnershipAssertion` ensures that the owner
of a proprietary resource matches that of the role.

See https://docs.zendframework.com/zend-permissions-acl/ownership/ for details
on usage.

### Changed

- Nothing.

### Deprecated

- Nothing.

### Removed

- Nothing.

### Fixed

- Nothing.

## 2.6.1 - 2018-05-01

### Added
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
},
"extra": {
"branch-alias": {
"dev-master": "2.6.x-dev",
"dev-develop": "2.7.x-dev"
"dev-master": "2.7.x-dev",
"dev-develop": "2.8.x-dev"
}
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

195 changes: 195 additions & 0 deletions docs/book/expression.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Expression Assertions

- Since 2.7.0

Many custom assertions are doing basic comparisons:

- Equality of a role property to a value or property of the resource.
- Other comparisons (`>`, `<`, `in_array`, etc.) of a role property to a value
or values (potentially a property of the resource).
- Regular expressions.

While these can be easily accommodated by the `CallbackAssertion`, such
assertions have one notable problem: they cannot be easily serialized.

To facilitate such assertions, we now provide
`Zend\Permissions\Acl\Assertion\ExpressionAssertion`. This class provides two
static factory methods for creating an instance, each expecting the following:

- The left operand
- An operator
- The right operand

When the assertion is executed, it uses the operator to determine how to compare
the two operands, and thus answer the assertion.

## Operands

The operands can be any PHP value.

Additionally, they can be an associative array containing the key
`ExpressionAssertion::OPERAND_CONTEXT_PROPERTY` (`__context`), with a string
value.

That value can be one of the following:

- A string matching the values "acl", "privilege", "role", or "resource", with
the latter two being most common. When one of these is provided, the
corresponding argument to the `assert()` method will be used.

- A dot-separated string with the first segment being one of the above values,
and the second being a property or field of that object. The
`ExpressionAssertion` will test for:

- a method matching `get<field>()`
- a method matching `is<field>()`
- a public property named `<field>`

in that specific order. In the first two cases, `<field>` will be normalized
to WordCase when creating the method name to test.

## Operators

`ExpressionAssertion` supports the following operators:

```php
const OPERATOR_EQ = '=';
const OPERATOR_NEQ = '!=';
const OPERATOR_LT = '<';
const OPERATOR_LTE = '<=';
const OPERATOR_GT = '>';
const OPERATOR_GTE = '>=';
const OPERATOR_IN = 'in';
const OPERATOR_NIN = '!in';
const OPERATOR_REGEX = 'regex';
const OPERATOR_NREGEX = '!regex';
const OPERATOR_SAME = '===';
const OPERATOR_NSAME = '!==';
```

In most cases, these will operate using the operators as listed above, with the
following exceptions:

- `OPERATOR_EQ` will use `==` as the comparison operator; `OPERATOR_NEQ` will
likewise use `!=`.
- `OPERATOR_IN` and `OPERATOR_NIN` use `in_array()` (with the latter negating
the result), both doing strict comparisons. The right hand operand is expected
to be the array in which to look for results, and the left hand operand is
expected to be the needle to look for.
- `OPERATOR_REGEX` and `OPERATOR_NREGEX` will perform a `preg_match()`
operation, using the right hand operand as the regular expression, and the
left hand operand as the value to compare.

## Constructors

The constructor of `ExpressionAssertion` is private. Instead, you will use one
of two static methods in order to create instances:

- `fromProperties($left, $operator, $right)`
- `fromArray(array $expression)` (expects keys for "left", "operator", and "right")

When creating expressions manually, the first is generally the best choice. When
storing expressions in configuration or a database, the latter is useful, as you
can pass a row of data at a time to the method to get expression instances.

## Examples

First, we'll define both a role and a resource:

```php
namespace Blog\Entity;

use Zend\Permissions\Acl\Resource\ResourceInterface;
use Zend\Permissions\Acl\Role\RoleInterface;

class BlogPost implements ResourceInterface
{
public $title;

public $shortDescription;

public $content;

public $author;

public function __construct(array $data = [])
{
foreach ($data as $property => $value) {
$this->$property = $value;
}
}

public function getResourceId()
{
return 'blogPost';
}

public function getShortDescription()
{
return $this->shortDescription;
}

public function getAuthorName()
{
return $this->author ? $this->author->username : '';
}
}

class User implements RoleInterface
{
public $username;

public $role = 'guest';

public $age;

public function __construct(array $data = [])
{
foreach ($data as $property => $value) {
$this->$property = $value;
}
}

public function getRoleId()
{
return $this->role;
}

public function isAdult()
{
return $this->age >= 18;
}
}
```

Next, let's define some assertions.

```php
use Zend\Permissions\Acl\Assertion\ExpressionAssertion;

// Username of role must be "test":
// Will access $username property on the role instance.
$isTestUser = ExpressionAssertion::fromProperties(
[ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.username'],
'===',
'test'
);


// Role must be at least 18 years old:
// Will execute `isAdult()` on the role instance.
$isOfLegalAge = ExpressionAssertion::fromProperties(
[ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'role.adult'],
'===',
true
);

// Must have edited text:
// Will do a regex comparison on the shortDescription of the blog post
// to ensure we do not have filler text.
$isEditedDescription = ExpressionAssertion::fromArray([
'left' => [ExpressionAssertion::OPERAND_CONTEXT_PROPERTY => 'resource.shortDescription'],
'operator' => '!regex',
'right' => '/lorem ipsum/i',
]);
```
126 changes: 126 additions & 0 deletions docs/book/ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Ownership Assertions

- Since 2.7.0

When setting up permissions for an application, site owners common will want to
allow roles to manipulate resources owned by the user with that role. For
example, a blog author should have permission to _write_ new posts, and also to
_modify_ his or her **own** posts, but **not** posts of other authors.

To accomodate this use case, we provide two interfaces:

- **`Zend\Acl\ProprietaryInterface`** is applicable to _resources_ and _roles_.
It provides information about the _owner_ of an object. Objects implementing
this interface are used in conjunction with the `OwnershipAssertion`.

- **`Zend\Acl\Assertion\OwnershipAssertion`** ensures that a resource is owned
by a specific role by comparing it to owners provided by
`ProprietaryInterface` implementations.

### Example

Consider the following entities:

```php
namespace MyApp\Entity;

use Zend\Permissions\Acl\ProprietaryInterface;
use Zend\Permissions\Acl\Resource\ResourceInterface;
use Zend\Permissions\Acl\Role\RoleInterface;

class User implements RoleInterface, ProprietaryInterface
{
protected $id;

protected $role = 'guest';

public function __construct($id, $role)
{
$this->id = $id;
$this->role = $role;
}

public function getRoleId()
{
return $this->role;
}

public function getOwnerId()
{
return $this->id;
}
}

class BlogPost implements ResourceInterface, ProprietaryInterface
{
public $author = null;

public function getResourceId()
{
return 'blogPost';
}

public function getOwnerId()
{
if ($this->author === null) {
return null;
}

return $this->author->getOwnerId();
}
}
```

The `User` marks itself as an _owner_ by implementing `ProprietaryInterface`;
its `getOwnerId()` method will return the user identifier provided during
instantiation.

A `BlogPost` marks itself as a resource and an _owner_ by also implementing
`ProprietaryInterface`; in its case, it returns the author identifier, if
present, but `null` otherwise.

Now let's wire these up into an ACL:

```php
namespace MyApp;

use MyApp\Entity;
use Zend\Permissions\Acl\Acl;
use Zend\Permissions\Acl\Assertion\OwnershipAssertion;

$acl = new Acl();
$acl->addRole('guest');
$acl->addRole('member', 'guest');
$acl->addRole('author', 'member');
$acl->addRole('admin');

$acl->addResource('blogPost');
$acl->addResource('comment');

$acl->allow('guest', 'blogPost', 'view');
$acl->allow('guest', 'comment', array('view', 'submit'));
$acl->allow('author', 'blogPost', 'write');
$acl->allow('author', 'blogPost', 'edit', new OwnershipAssertion());
$acl->allow('admin');

$author1 = new User(1, 'author');
$author2 = new User(2, 'author');

$blogPost = new BlogPost();
$blogPost->author = $author1;
```

The takeaways from the above should be:

- An `author` can _write_ blog posts, and _edit_ posts it owns.
- `$author1` and `$author2` are both authors.
- `$author1` is the author of `$blogPost`.

Knowing these facts, we can expect the following assertion results:

```php
$acl->isAllowed($author1, 'blogPost', 'write'); // true
$acl->isAllowed($author1, $blogPost, 'edit'); // true
$acl->isAllowed($author2, 'blogPost', 'write'); // true
$acl->isAllowed($author2, $blogPost, 'edit'); // false
```
Loading

0 comments on commit c9568f4

Please sign in to comment.