mirror of
https://github.com/anikeen-com/id.git
synced 2026-11-05 07:09:33 +00:00
Merge pull request #18 from anikeen-com/feature/reseller-scopes
Add product and reseller scopes (3.1.0)
This commit is contained in:
5 files changed
+125
No files matched your search
@@ -0,0 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 3.1.0 (2026-11-05)
|
||||
|
||||
- Add the scope `products:read` (`Scope::PRODUCTS_READ`) for read-only access to the product APIs
|
||||
of the Anikeen Console.
|
||||
- Add the reseller scopes `reseller-customers:read`, `reseller-customers`, `reseller-grants` and
|
||||
`reseller-orders` (`Scope::RESELLER_CUSTOMERS_READ`, `Scope::RESELLER_CUSTOMERS`,
|
||||
`Scope::RESELLER_GRANTS`, `Scope::RESELLER_ORDERS`) for the reseller API of the Anikeen Console.
|
||||
- Document the scope schema and the new scopes in the README.
|
||||
|
||||
Older releases: see the [GitHub releases](https://github.com/anikeen-com/id/releases) and tags.
|
||||
@@ -160,6 +160,27 @@ if ($result->success()) {
|
||||
}
|
||||
```
|
||||
|
||||
### Scopes
|
||||
|
||||
Scopes follow one schema: `resource` grants full access, `resource:read` read-only access and
|
||||
`resource:client` access to what the app or token created itself. All of them are constants on
|
||||
`Anikeen\Id\Enums\Scope`, e.g. `Scope::ORDERS_READ`.
|
||||
|
||||
The Anikeen Console accepts personal access tokens and OAuth tokens (no client credentials) with
|
||||
these scopes for its product and reseller APIs:
|
||||
|
||||
| Scope | Constant | Grants |
|
||||
|-------|----------|--------|
|
||||
| `products:read` | `Scope::PRODUCTS_READ` | view the products of your teams |
|
||||
| `reseller-customers:read` | `Scope::RESELLER_CUSTOMERS_READ` | view a reseller team's customers and invitations |
|
||||
| `reseller-customers` | `Scope::RESELLER_CUSTOMERS` | manage customers: invite, resend, remove logins (includes reading) |
|
||||
| `reseller-grants` | `Scope::RESELLER_GRANTS` | grant customers access to products and revoke it |
|
||||
| `reseller-orders` | `Scope::RESELLER_ORDERS` | order products for customers, charged right away |
|
||||
|
||||
A full reseller scope includes reading. `user` and `*` do not open the reseller endpoints: the
|
||||
token has to carry the reseller scope itself. For the product APIs there is only `products:read`;
|
||||
changing products needs `user`.
|
||||
|
||||
### Change the default access token / refresh token field name
|
||||
|
||||
If your access / refresh token fields differ from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method:
|
||||
|
||||
+21
@@ -160,6 +160,27 @@ if ($result->success()) {
|
||||
}
|
||||
```
|
||||
|
||||
### Scopes
|
||||
|
||||
Scopes follow one schema: `resource` grants full access, `resource:read` read-only access and
|
||||
`resource:client` access to what the app or token created itself. All of them are constants on
|
||||
`Anikeen\Id\Enums\Scope`, e.g. `Scope::ORDERS_READ`.
|
||||
|
||||
The Anikeen Console accepts personal access tokens and OAuth tokens (no client credentials) with
|
||||
these scopes for its product and reseller APIs:
|
||||
|
||||
| Scope | Constant | Grants |
|
||||
|-------|----------|--------|
|
||||
| `products:read` | `Scope::PRODUCTS_READ` | view the products of your teams |
|
||||
| `reseller-customers:read` | `Scope::RESELLER_CUSTOMERS_READ` | view a reseller team's customers and invitations |
|
||||
| `reseller-customers` | `Scope::RESELLER_CUSTOMERS` | manage customers: invite, resend, remove logins (includes reading) |
|
||||
| `reseller-grants` | `Scope::RESELLER_GRANTS` | grant customers access to products and revoke it |
|
||||
| `reseller-orders` | `Scope::RESELLER_ORDERS` | order products for customers, charged right away |
|
||||
|
||||
A full reseller scope includes reading. `user` and `*` do not open the reseller endpoints: the
|
||||
token has to carry the reseller scope itself. For the product APIs there is only `products:read`;
|
||||
changing products needs `user`.
|
||||
|
||||
### Change the default access token / refresh token field name
|
||||
|
||||
If your access / refresh token fields differ from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method:
|
||||
|
||||
@@ -36,5 +36,16 @@ class Scope
|
||||
const SSH_KEYS = 'ssh-keys';
|
||||
const SSH_KEYS_READ = 'ssh-keys:read';
|
||||
|
||||
// Products of the Anikeen Console (servers, floating IPs, hosting, domains, zones, object storage),
|
||||
// read-only. Changing products needs `user`; there is no full `products` scope.
|
||||
const PRODUCTS_READ = 'products:read';
|
||||
|
||||
// Reseller API of the Anikeen Console. A full scope includes reading; `user` and `*` do not
|
||||
// open these endpoints, a token must carry the reseller scope itself.
|
||||
const RESELLER_CUSTOMERS = 'reseller-customers';
|
||||
const RESELLER_CUSTOMERS_READ = 'reseller-customers:read';
|
||||
const RESELLER_GRANTS = 'reseller-grants';
|
||||
const RESELLER_ORDERS = 'reseller-orders';
|
||||
|
||||
const ADMIN = 'admin';
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Tests;
|
||||
|
||||
use Anikeen\Id\Enums\Scope;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use ReflectionClass;
|
||||
|
||||
class ScopeTest extends TestCase
|
||||
{
|
||||
public static function newScopes(): array
|
||||
{
|
||||
return [
|
||||
'products:read' => [Scope::PRODUCTS_READ, 'products:read'],
|
||||
'reseller-customers' => [Scope::RESELLER_CUSTOMERS, 'reseller-customers'],
|
||||
'reseller-customers:read' => [Scope::RESELLER_CUSTOMERS_READ, 'reseller-customers:read'],
|
||||
'reseller-grants' => [Scope::RESELLER_GRANTS, 'reseller-grants'],
|
||||
'reseller-orders' => [Scope::RESELLER_ORDERS, 'reseller-orders'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('newScopes')]
|
||||
public function test_the_product_and_reseller_scopes_have_their_names(string $constant, string $expected): void
|
||||
{
|
||||
$this->assertSame($expected, $constant);
|
||||
}
|
||||
|
||||
public function test_there_is_no_full_products_scope_the_console_would_not_accept(): void
|
||||
{
|
||||
// the Console's product APIs accept `user`, `user:read` and `products:read` only
|
||||
$this->assertNotContains('products', (new ReflectionClass(Scope::class))->getConstants());
|
||||
}
|
||||
|
||||
public function test_every_scope_follows_the_resource_and_level_schema(): void
|
||||
{
|
||||
$scopes = array_values((new ReflectionClass(Scope::class))->getConstants());
|
||||
|
||||
$this->assertSame($scopes, array_values(array_unique($scopes)), 'scope values must be unique');
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
// "resource" (full access), "resource:read" or "resource:client"; Anikeen ID groups by resource
|
||||
$this->assertMatchesRegularExpression('/^[a-z]+(-[a-z]+)*(:(read|client))?$/', $scope);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_each_reseller_resource_is_its_own_group(): void
|
||||
{
|
||||
$groups = collect((new ReflectionClass(Scope::class))->getConstants())
|
||||
->filter(fn (string $scope) => str_starts_with($scope, 'reseller-'))
|
||||
->groupBy(fn (string $scope) => explode(':', $scope)[0])
|
||||
->map(fn ($scopes) => $scopes->sort()->values()->all())
|
||||
->all();
|
||||
|
||||
$this->assertSame([
|
||||
'reseller-customers' => ['reseller-customers', 'reseller-customers:read'],
|
||||
'reseller-grants' => ['reseller-grants'],
|
||||
'reseller-orders' => ['reseller-orders'],
|
||||
], $groups);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user