Skip to content

Commit

Permalink
Merge pull request #8 from givebutter/keyable-scope
Browse files Browse the repository at this point in the history
Keyable scope
  • Loading branch information
liran-co authored Jan 11, 2022
2 parents 2308ae9 + d622b75 commit 5a25ec1
Show file tree
Hide file tree
Showing 12 changed files with 437 additions and 13 deletions.
17 changes: 11 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"description": "Add API keys to your Laravel models",
"license": "MIT",
"keywords": [
"laravel",
"php",
"laravel",
"php",
"api",
"rest",
"json",
Expand All @@ -21,20 +21,25 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": "^7.0|^8.0"
"php": "^7.0|^8.0",
"orchestra/testbench": "^6.23",
"phpunit/phpunit": "^9.5"
},
"require-dev": {
},
"autoload": {
"psr-4": {
"Givebutter\\LaravelKeyable\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Givebutter\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Givebutter\\LaravelKeyable\\KeyableServiceProvider"
]
}
}
}
}
60 changes: 60 additions & 0 deletions src/Http/Middleware/EnforceKeyableScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Givebutter\LaravelKeyable\Http\Middleware;

use Closure;
use Illuminate\Support\Arr;
use Illuminate\Support\Reflector;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class EnforceKeyableScope
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param Closure $next
* @param string|null $guard
*
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
$route = $request->route();

if (empty($route->parameterNames())) {
return $next($request);
}

$parameterName = $route->parameterNames()[0];
$parameterValue = $route->originalParameters()[$parameterName];
$parameter = Arr::first($route->signatureParameters(UrlRoutable::class));
$instance = app(Reflector::getParameterClassName($parameter));

$childRouteBindingMethod = $route->allowsTrashedBindings()
? 'resolveSoftDeletableChildRouteBinding'
: 'resolveChildRouteBinding';

if (! $request->keyable->{$childRouteBindingMethod}(
$parameterName,
$parameterValue,
$route->bindingFieldFor($parameterName)
)) {
throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
}

return $next($request);
}

protected static function getParameterName($name, $parameters)
{
if (array_key_exists($name, $parameters)) {
return $name;
}

if (array_key_exists($snakedName = Str::snake($name), $parameters)) {
return $snakedName;
}
}
}
36 changes: 29 additions & 7 deletions src/KeyableServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace Givebutter\LaravelKeyable;

use Illuminate\Routing\Route;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Illuminate\Routing\PendingResourceRegistration;
use Givebutter\LaravelKeyable\Console\Commands\DeleteApiKey;
use Givebutter\LaravelKeyable\Console\Commands\GenerateApiKey;
use Givebutter\LaravelKeyable\Http\Middleware\AuthenticateApiKey;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Givebutter\LaravelKeyable\Http\Middleware\EnforceKeyableScope;

class KeyableServiceProvider extends ServiceProvider
{
Expand All @@ -20,12 +23,14 @@ public function boot(Router $router)
$this->publishes([
__DIR__ . '/../config/keyable.php' => config_path('keyable.php'),
]);

$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');

$this->registerMiddleware($router);

$this->registerCommands();

$this->registerMacros();
}

/**
Expand All @@ -37,7 +42,7 @@ public function register()
{
//
}

protected function registerCommands()
{
if ($this->app->runningInConsole()) {
Expand All @@ -47,7 +52,7 @@ protected function registerCommands()
]);
}
}

/**
* Register middleware.
*
Expand All @@ -60,8 +65,25 @@ protected function registerMiddleware(Router $router)
$versionComparison = version_compare(app()->version(), '5.4.0');
if ($versionComparison >= 0) {
$router->aliasMiddleware('auth.apikey', AuthenticateApiKey::class);
$router->aliasMiddleware('keyableScoped', EnforceKeyableScope::class);
} else {
$router->middleware('auth.apikey', AuthenticateApiKey::class);
$router->middleware('keyableScoped', EnforceKeyableScope::class);
}
}

protected function registerMacros()
{
PendingResourceRegistration::macro('keyableScoped', function () {
$this->middleware('keyableScoped');

return $this;
});

Route::macro('keyableScoped', function () {
$this->middleware('keyableScoped');

return $this;
});
}
}
34 changes: 34 additions & 0 deletions tests/Feature/AuthenticateApiKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Givebutter\Tests\Feature;

use Givebutter\Tests\TestCase;
use Givebutter\Tests\Support\Account;
use Illuminate\Support\Facades\Route;

class AuthenticateApiKey extends TestCase
{
/** @test */
public function request_with_api_key_responds_ok()
{
Route::get("/api/posts", function () {
return response('All good', 200);
})->middleware(['api', 'auth.apikey']);

$account = Account::create();

$this->withHeaders([
'Authorization' => 'Bearer ' . $account->createApiKey()->key,
])->get("/api/posts")->assertOk();
}

/** @test */
public function request_without_api_key_responds_unauthorized()
{
Route::get("/api/posts", function () {
return response('All good', 200);
})->middleware(['api', 'auth.apikey']);

$this->get("/api/posts")->assertUnauthorized();
}
}
116 changes: 116 additions & 0 deletions tests/Feature/EnforceKeyableScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace Givebutter\Tests\Feature;

use Illuminate\Http\Request;
use Givebutter\Tests\TestCase;
use Givebutter\Tests\Support\Post;
use Givebutter\Tests\Support\Account;
use Illuminate\Support\Facades\Route;
use Givebutter\Tests\Support\PostsController;
use Givebutter\Tests\Support\CommentsController;

class EnforceKeyableScope extends TestCase
{
/** @test */
public function request_with_parameter_must_be_owned_by_keyable()
{
Route::get("/api/posts/{post}", function (Request $request, Post $post) {
return response('All good', 200);
})->middleware(['api', 'auth.apikey'])->keyableScoped();

$account = Account::create();
$post = $account->posts()->create();

$this->withHeaders([
'Authorization' => 'Bearer ' . $account->createApiKey()->key,
])->get("/api/posts/{$post->id}")->assertOk();
}

/** @test */
public function request_with_model_not_owned_by_keyable_throws_model_not_found()
{
Route::get("/api/posts/{post}", function (Request $request, Post $post) {
return response('All good', 200);
})->middleware([ 'api', 'auth.apikey'])->keyableScoped();

$account = Account::create();
$account2 = Account::create();
$post = $account2->posts()->create();

$this->withHeaders([
'Authorization' => 'Bearer ' . $account->createApiKey()->key,
])->get("/api/posts/{$post->id}")->assertNotFound();
}

/** @test */
public function works_with_resource_routes()
{
Route::prefix('api')->middleware(['api', 'auth.apikey'])->group(function () {
Route::apiResource('posts', PostsController::class)
->only('show')
->keyableScoped();
});

/*
| --------------------------------
| PASSING
| --------------------------------
*/
$account = Account::create();
$post = $account->posts()->create();

$this->withHeaders([
'Authorization' => 'Bearer ' . $account->createApiKey()->key,
])->get("/api/posts/{$post->id}")->assertOk();

/*
| --------------------------------
| FAILING
| --------------------------------
*/
$account2 = Account::create();
$post = $account2->posts()->create();

$this->withHeaders([
'Authorization' => 'Bearer ' . $account->createApiKey()->key,
])->get("/api/posts/{$post->id}")->assertNotFound();
}

/** @test */
public function can_use_scoped_with_keyableScoped()
{
Route::middleware(['api', 'auth.apikey'])->group(function () {
Route::apiResource('posts.comments', CommentsController::class)
->only('show')
->scoped()
->keyableScoped();
});

/*
| --------------------------------
| PASSING
| --------------------------------
*/
$account = Account::create();
$post = $account->posts()->create();
$comment = $post->comments()->create();

$this->withHeaders([
'Authorization' => 'Bearer ' . $account->createApiKey()->key,
])->get("posts/{$post->id}/comments/{$comment->id}")->assertOk();

/*
| --------------------------------
| FAILING
| --------------------------------
*/
$account2 = Account::create();
$post2 = $account2->posts()->create();
$comment2 = $post2->comments()->create();

$this->withHeaders([
'Authorization' => 'Bearer ' . $account->createApiKey()->key,
])->get("posts/{$post->id}/comments/{$comment2->id}")->assertNotFound();
}
}
16 changes: 16 additions & 0 deletions tests/Support/Account.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Givebutter\Tests\Support;

use Givebutter\LaravelKeyable\Keyable;
use Illuminate\Database\Eloquent\Model;

class Account extends Model
{
use Keyable;

public function posts()
{
return $this->hasMany(Post::class);
}
}
14 changes: 14 additions & 0 deletions tests/Support/Comment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Givebutter\Tests\Support;

use Givebutter\Tests\Support\Post;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
public function post()
{
return $this->belongsTo(Post::class);
}
}
14 changes: 14 additions & 0 deletions tests/Support/CommentsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Givebutter\Tests\Support;

use Illuminate\Http\Request;
use Givebutter\Tests\Support\Post;

class CommentsController
{
public function show(Request $request, Post $post, Comment $comment)
{
return response('All good', 200);
}
}
Loading

0 comments on commit 5a25ec1

Please sign in to comment.