Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/ApiServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use Illuminate\Support\Facades\Validator;
use Seat\Api\Http\Middleware\ApiRequest;
use Seat\Api\Http\Middleware\ApiToken;
use Seat\Api\Http\Middleware\CharacterOwnership;
use Seat\Api\Http\Middleware\CorporationOwnership;
use Seat\Services\AbstractSeatPlugin;

/**
Expand Down Expand Up @@ -105,6 +107,12 @@ public function add_middleware($router)
// Ensure incoming request is formed using JSON
$router->aliasMiddleware('api.request', ApiRequest::class);

// Verify that the token owner has access to the requested character
$router->aliasMiddleware('api.character.ownership', CharacterOwnership::class);

// Verify that the token owner has access to the requested corporation
$router->aliasMiddleware('api.corporation.ownership', CorporationOwnership::class);

}

/**
Expand Down
89 changes: 89 additions & 0 deletions src/Http/Middleware/CharacterOwnership.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

/*
* This file is part of SeAT
*
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

namespace Seat\Api\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Seat\Api\Models\ApiToken;

/**
* Class CharacterOwnership.
*
* Ensures that the API token owner has access to the requested character_id.
*
* Tokens with no associated user (user_id is null) are treated as superuser-scoped
* tokens with unrestricted access to all characters. Tokens linked to a specific
* user may only access character_ids belonging to that user, unless that user has
* the superuser role.
*
* @package Seat\Api\Http\Middleware
*/
class CharacterOwnership
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$character_id = $request->route('character_id');

// If there is no character_id in the route, pass through.
if (is_null($character_id)) {
return $next($request);
}

$token = ApiToken::where('token', $request->header('X-Token'))->first();

// Token not found — the api.auth middleware will already have rejected
// this request, but we guard defensively here as well.
if (is_null($token)) {
return response()->json('Unauthorized', 401);
}

// Tokens with no associated user are superuser-scoped (unrestricted).
if (is_null($token->user_id)) {
return $next($request);
}

$user = $token->user;

// Superusers have unrestricted access to all characters.
if ($user->hasSuperUser()) {
return $next($request);
}

$ownedCharacterIds = $user->characters->pluck('character_id');

if (! $ownedCharacterIds->contains((int) $character_id)) {
return response()->json([
'error' => 'Access to this character is not authorized.',
], 403);
}

return $next($request);
}
}
98 changes: 98 additions & 0 deletions src/Http/Middleware/CorporationOwnership.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

/*
* This file is part of SeAT
*
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

namespace Seat\Api\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Seat\Api\Models\ApiToken;

/**
* Class CorporationOwnership.
*
* Ensures that the API token owner has access to the requested corporation_id.
*
* A user is considered to have access to a corporation if at least one of their
* characters belongs to that corporation. Tokens with no associated user
* (user_id is null) are treated as superuser-scoped tokens with unrestricted
* access. Tokens linked to a specific user with the superuser role also retain
* unrestricted access.
*
* @package Seat\Api\Http\Middleware
*/
class CorporationOwnership
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$corporation_id = $request->route('corporation_id');

// If there is no corporation_id in the route, pass through.
if (is_null($corporation_id)) {
return $next($request);
}

$token = ApiToken::where('token', $request->header('X-Token'))->first();

// Token not found — the api.auth middleware will already have rejected
// this request, but we guard defensively here as well.
if (is_null($token)) {
return response()->json('Unauthorized', 401);
}

// Tokens with no associated user are superuser-scoped (unrestricted).
if (is_null($token->user_id)) {
return $next($request);
}

$user = $token->user;

// Superusers have unrestricted access to all corporations.
if ($user->hasSuperUser()) {
return $next($request);
}

// A user has access to a corporation if at least one of their characters
// is a member of that corporation.
$userCorporationIds = $user->characters
->map(function ($character) {
return optional($character->affiliation)->corporation_id;
})
->filter()
->unique()
->values();

if (! $userCorporationIds->contains((int) $corporation_id)) {
return response()->json([
'error' => 'Access to this corporation is not authorized.',
], 403);
}

return $next($request);
}
}
1 change: 1 addition & 0 deletions src/Http/Validation/NewToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function rules()
return [
'comment' => 'max:255',
'allowed_src' => 'required|ip',
'user_id' => 'nullable|integer|exists:users,id',
];
}
}
4 changes: 2 additions & 2 deletions src/Http/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
Route::get('/{killmail_id}')->uses('KillmailsController@getDetail');
});

Route::group(['prefix' => 'character'], function () {
Route::group(['prefix' => 'character', 'middleware' => 'api.character.ownership'], function () {

Route::get('/assets/{character_id}')->uses('CharacterController@getAssets');
Route::get('/contacts/{character_id}')->uses('CharacterController@getContacts');
Expand All @@ -126,7 +126,7 @@
Route::get('/notifications/{character_id}')->uses('CharacterController@getNotifications');
});

Route::group(['prefix' => 'corporation'], function () {
Route::group(['prefix' => 'corporation', 'middleware' => 'api.corporation.ownership'], function () {

Route::get('/assets/{corporation_id}')->uses('CorporationController@getAssets');
Route::get('/contacts/{corporation_id}')->uses('CorporationController@getContacts');
Expand Down
17 changes: 16 additions & 1 deletion src/Models/ApiToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
namespace Seat\Api\Models;

use Illuminate\Database\Eloquent\Model;
use Seat\Web\Models\User;

/**
* Class ApiToken.
Expand All @@ -34,7 +35,7 @@ class ApiToken extends Model
/**
* @var array
*/
protected $fillable = ['token', 'allowed_src', 'comment'];
protected $fillable = ['token', 'allowed_src', 'comment', 'user_id'];

/**
* Make sure we cleanup logs on delete.
Expand Down Expand Up @@ -62,4 +63,18 @@ public function logs()

return $this->hasMany(ApiTokenLog::class);
}

/**
* Return the user that owns this token, if any.
*
* Tokens with no associated user (user_id is null) are considered
* superuser-scoped tokens with unrestricted access.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{

return $this->belongsTo(User::class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of SeAT
*
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddUserIdToApiTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('api_tokens', function (Blueprint $table) {
// Nullable so that existing tokens (which have no associated user)
// are treated as superuser-scoped (unrestricted) tokens.
$table->unsignedBigInteger('user_id')->nullable()->after('comment');
$table->index('user_id');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('api_tokens', function (Blueprint $table) {
$table->dropIndex(['user_id']);
$table->dropColumn('user_id');
});
}
}