diff --git a/.env.example b/.env.example index 6e00765ba..4fc4727bf 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ DB_PASSWORD=secret CACHE_DRIVER=file SESSION_DRIVER=file -QUEUE_DRIVER=sync +QUEUE_DRIVER=database REDIS_HOST=localhost REDIS_PASSWORD=null diff --git a/app/Http/Controllers/Server/SubuserController.php b/app/Http/Controllers/Server/SubuserController.php index c3757658a..01faa001f 100644 --- a/app/Http/Controllers/Server/SubuserController.php +++ b/app/Http/Controllers/Server/SubuserController.php @@ -3,8 +3,15 @@ namespace Pterodactyl\Http\Controllers\Server; use DB; +use Auth; use Alert; +use Log; + use Pterodactyl\Models; +use Pterodactyl\Repositories\SubuserRepository; + +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\DisplayValidationException; use Illuminate\Http\Request; use Pterodactyl\Http\Controllers\Controller; @@ -71,7 +78,106 @@ class SubuserController extends Controller public function postView(Request $request, $uuid, $id) { - // + + $server = Models\Server::getByUUID($uuid); + $this->authorize('edit-subuser', $server); + + $subuser = Models\Subuser::where(DB::raw('md5(id)'), $id)->where('server_id', $server->id)->first(); + + try { + + if (!$subuser) { + throw new DisplayException('Unable to locate a subuser by that ID.'); + } else if ($subuser->user_id === Auth::user()->id) { + throw new DisplayException('You are not authorized to edit you own account.'); + } + + $repo = new SubuserRepository; + $repo->update($subuser->id, [ + 'permissions' => $request->input('permissions'), + 'server' => $server->id, + 'user' => $subuser->user_id + ]); + + Alert::success('Subuser permissions have successfully been updated.')->flash(); + } catch (DisplayValidationException $ex) { + return redirect()->route('server.subusers.view', [ + 'uuid' => $uuid, + 'id' => $id + ])->withErrors(json_decode($ex->getMessage())); + } catch (DisplayException $ex) { + Alert::danger($ex->getMessage())->flash(); + } catch (\Exception $ex) { + Log::error($ex); + Alert::danger('An unknown error occured while attempting to update this subuser.')->flash(); + } + return redirect()->route('server.subusers.view', [ + 'uuid' => $uuid, + 'id' => $id + ]); + } + + public function getNew(Request $request, $uuid) + { + $server = Models\Server::getByUUID($uuid); + $this->authorize('create-subuser', $server); + + return view('server.users.new', [ + 'server' => $server, + 'node' => Models\Node::find($server->node) + ]); + } + + public function postNew(Request $request, $uuid) + { + $server = Models\Server::getByUUID($uuid); + $this->authorize('create-subuser', $server); + + try { + $repo = new SubuserRepository; + $id = $repo->create($server->id, $request->except([ + '_token' + ])); + Alert::success('Successfully created new subuser.')->flash(); + return redirect()->route('server.subusers.view', [ + 'uuid' => $uuid, + 'id' => md5($id) + ]); + } catch (DisplayValidationException $ex) { + return redirect()->route('server.subusers.new', $uuid)->withErrors(json_decode($ex->getMessage()))->withInput(); + } catch (DisplayException $ex) { + Alert::danger($ex->getMessage())->flash(); + } catch (\Exception $ex) { + Log::error($ex); + Alert::danger('An unknown error occured while attempting to add a new subuser.')->flash(); + } + return redirect()->route('server.subusers.new', $uuid)->withInput(); + } + + public function deleteSubuser(Request $request, $uuid, $id) + { + $server = Models\Server::getByUUID($uuid); + $this->authorize('delete-subuser', $server); + + try { + $subuser = Models\Subuser::select('id')->where(DB::raw('md5(id)'), $id)->where('server_id', $server->id)->first(); + if (!$subuser) { + throw new DisplayException('No subuser by that ID was found on the system.'); + } + + $repo = new SubuserRepository; + $repo->delete($subuser->id); + return response('', 204); + } catch (DisplayException $ex) { + response()->json([ + 'error' => $ex->getMessage() + ], 422); + } catch (\Exception $ex) { + Log::error($ex); + response()->json([ + 'error' => 'An unknown error occured while attempting to delete this subuser.' + ], 503); + } } } diff --git a/app/Http/Routes/AuthRoutes.php b/app/Http/Routes/AuthRoutes.php index 0d50f2c8d..7b7f1d441 100644 --- a/app/Http/Routes/AuthRoutes.php +++ b/app/Http/Routes/AuthRoutes.php @@ -35,6 +35,7 @@ class AuthRoutes { // Show Password Reset Form $router->get('password', [ + 'as' => 'auth.password', 'uses' => 'Auth\PasswordController@getEmail' ]); @@ -45,6 +46,7 @@ class AuthRoutes { // Show Verification Checkpoint $router->get('password/reset/{token}', [ + 'as' => 'auth.reset', 'uses' => 'Auth\PasswordController@getReset' ]); diff --git a/app/Http/Routes/ServerRoutes.php b/app/Http/Routes/ServerRoutes.php index f29264afa..6d4145bb4 100644 --- a/app/Http/Routes/ServerRoutes.php +++ b/app/Http/Routes/ServerRoutes.php @@ -58,6 +58,15 @@ class ServerRoutes { 'uses' => 'Server\SubuserController@getIndex' ]); + $router->get('users/new', [ + 'as' => 'server.subusers.new', + 'uses' => 'Server\SubuserController@getNew' + ]); + + $router->post('users/new', [ + 'uses' => 'Server\SubuserController@postNew' + ]); + $router->get('users/view/{id}', [ 'as' => 'server.subusers.view', 'uses' => 'Server\SubuserController@getView' @@ -67,6 +76,10 @@ class ServerRoutes { 'uses' => 'Server\SubuserController@postView' ]); + $router->delete('users/delete/{id}', [ + 'uses' => 'Server\SubuserController@deleteSubuser' + ]); + // Assorted AJAX Routes $router->group(['prefix' => 'ajax'], function ($server) use ($router) { // Returns Server Status diff --git a/app/Models/Permission.php b/app/Models/Permission.php index f35195393..2028ddcfc 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -14,6 +14,13 @@ class Permission extends Model */ protected $table = 'permissions'; + /** + * Fields that are not mass assignable. + * + * @var array + */ + protected $guarded = ['id', 'created_at', 'updated_at']; + public function scopePermission($query, $permission) { return $query->where('permission', $permission); diff --git a/app/Models/Subuser.php b/app/Models/Subuser.php index f671ff299..fb89b6bed 100644 --- a/app/Models/Subuser.php +++ b/app/Models/Subuser.php @@ -15,6 +15,20 @@ class Subuser extends Model */ protected $table = 'subusers'; + /** + * The attributes excluded from the model's JSON form. + * + * @var array + */ + protected $hidden = ['daemonSecret']; + + /** + * Fields that are not mass assignable. + * + * @var array + */ + protected $guarded = ['id', 'created_at', 'updated_at']; + /** * @var mixed */ diff --git a/app/Repositories/SubuserRepository.php b/app/Repositories/SubuserRepository.php index e5a0162aa..4044a3f1d 100644 --- a/app/Repositories/SubuserRepository.php +++ b/app/Repositories/SubuserRepository.php @@ -4,16 +4,29 @@ namespace Pterodactyl\Repositories; use DB; use Validator; +use Mail; use Pterodactyl\Models; +use Pterodactyl\Repositories\UserRepository; use Pterodactyl\Services\UuidService; use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Exceptions\DisplayException; -class UserRepository +class SubuserRepository { + /** + * Core permissions required for every subuser on the daemon. + * Without this we cannot connect the websocket or get basic + * information about the server. + * @var array + */ + protected $coreDaemonPermissions = [ + 's:get', + 's:console' + ]; + /** * Allowed permissions and their related daemon permission. * @var array @@ -30,12 +43,12 @@ class UserRepository // File Manager 'list-files' => 's:files:get', - 'edit-file' => 's:files:read', - 'save-file' => 's:files:post', - 'create-file' => 's:files:post', - 'download-file' => null, - 'upload-file' => 's:files:upload', - 'delete-file' => 's:files:delete', + 'edit-files' => 's:files:read', + 'save-files' => 's:files:post', + 'create-files' => 's:files:post', + 'download-files' => null, + 'upload-files' => 's:files:upload', + 'delete-files' => 's:files:delete', // Subusers 'list-subusers' => null, @@ -46,6 +59,8 @@ class UserRepository // Management 'set-connection' => null, + 'view-startup' => null, + 'edit-startup' => null, 'view-sftp' => null, 'reset-sftp' => 's:set-password' ]; @@ -55,6 +70,154 @@ class UserRepository // } + /** + * Creates a new subuser on the server. + * @param integer $id The ID of the server to add this subuser to. + * @param array $data + * @throws DisplayValidationException + * @throws DisplayException + * @return integer Returns the ID of the newly created subuser. + */ + public function create($sid, array $data) + { + $server = Models\Server::findOrFail($sid); + $validator = Validator::make($data, [ + 'permissions' => 'required|array', + 'email' => 'required|email' + ]); + + if ($validator->fails()) { + throw new DisplayValidationException(json_encode($validator->all())); + } + + DB::beginTransaction(); + + // Determine if this user exists or if we need to make them an account. + $user = Models\User::where('email', $data['email'])->first(); + if (!$user) { + $password = str_random(16); + try { + $repo = new UserRepository; + $uid = $repo->create($data['email'], $password); + $user = Models\User::findOrFail($uid); + } catch (\Exception $ex) { + throw $ex; + } + } + + $uuid = new UuidService; + + $subuser = new Models\Subuser; + $subuser->fill([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'daemonSecret' => (string) $uuid->generate('servers', 'uuid') + ]); + $subuser->save(); + + $daemonPermissions = $this->coreDaemonPermissions; + foreach($data['permissions'] as $permission) { + if (array_key_exists($permission, $this->permissions)) { + // Build the daemon permissions array for sending. + if (!is_null($this->permissions[$permission])) { + array_push($daemonPermissions, $this->permissions[$permission]); + } + $model = new Models\Permission; + $model->fill([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'permission' => $permission + ]); + $model->save(); + } + } + + // Contact Daemon + // We contact even if they don't have any daemon permissions to overwrite + // if they did have them previously. + try { + + $node = Models\Node::getByID($server->node); + $client = Models\Node::guzzleRequest($server->node); + + $res = $client->request('PATCH', '/server', [ + 'headers' => [ + 'X-Access-Server' => $server->uuid, + 'X-Access-Token' => $node->daemonSecret + ], + 'json' => [ + 'keys' => [ + $subuser->daemonSecret => $daemonPermissions + ] + ] + ]); + + $email = $data['email']; + Mail::queue('emails.added-subuser', [ + 'serverName' => $server->name, + 'url' => route('server.index', $server->uuidShort), + ], function ($message) use ($email) { + $message->to($email); + $message->subject('Pterodactyl - Added to Server'); + }); + DB::commit(); + return $subuser->id; + } catch (\GuzzleHttp\Exception\TransferException $ex) { + DB::rollBack(); + throw new DisplayException('There was an error attempting to connect to the daemon to add this user.'); + } catch (\Exception $ex) { + DB::rollBack(); + throw $ex; + } + return false; + } + + /** + * Revokes a users permissions on a server. + * @param integer $id The ID of the subuser row in MySQL. + * @param array $data + * @throws DisplayValidationException + * @throws DisplayException + * @return void + */ + public function delete($id) + { + $subuser = Models\Subuser::findOrFail($id); + $server = Models\Server::findOrFail($subuser->server_id); + + DB::beginTransaction(); + + Models\Permission::where('user_id', $subuser->user_id)->where('server_id', $subuser->server_id)->delete(); + + try { + $node = Models\Node::getByID($server->node); + $client = Models\Node::guzzleRequest($server->node); + + $res = $client->request('PATCH', '/server', [ + 'headers' => [ + 'X-Access-Server' => $server->uuid, + 'X-Access-Token' => $node->daemonSecret + ], + 'json' => [ + 'keys' => [ + $subuser->daemonSecret => [] + ] + ] + ]); + + $subuser->delete(); + DB::commit(); + return true; + } catch (\GuzzleHttp\Exception\TransferException $ex) { + DB::rollBack(); + throw new DisplayException('There was an error attempting to connect to the daemon to delete this subuser.'); + } catch (\Exception $ex) { + DB::rollBack(); + throw $ex; + } + return false; + } + /** * Updates permissions for a given subuser. * @param integer $id The ID of the subuser row in MySQL. (Not the user ID) @@ -66,13 +229,67 @@ class UserRepository public function update($id, array $data) { $validator = Validator::make($data, [ - 'permissions' => 'required|array' + 'permissions' => 'required|array', + 'user' => 'required|exists:users,id', + 'server' => 'required|exists:servers,id', ]); if ($validator->fails()) { throw new DisplayValidationException(json_encode($validator->all())); } - // @TODO the thing. + $subuser = Models\Subuser::findOrFail($id); + $server = Models\Server::findOrFail($data['server']); + DB::beginTransaction(); + Models\Permission::where('user_id', $subuser->user_id)->where('server_id', $subuser->server_id)->delete(); + + $daemonPermissions = $this->coreDaemonPermissions; + foreach($data['permissions'] as $permission) { + if (array_key_exists($permission, $this->permissions)) { + // Build the daemon permissions array for sending. + if (!is_null($this->permissions[$permission])) { + array_push($daemonPermissions, $this->permissions[$permission]); + } + $model = new Models\Permission; + $model->fill([ + 'user_id' => $data['user'], + 'server_id' => $data['server'], + 'permission' => $permission + ]); + $model->save(); + } + } + + // Contact Daemon + // We contact even if they don't have any daemon permissions to overwrite + // if they did have them previously. + try { + $node = Models\Node::getByID($server->node); + $client = Models\Node::guzzleRequest($server->node); + + $res = $client->request('PATCH', '/server', [ + 'headers' => [ + 'X-Access-Server' => $server->uuid, + 'X-Access-Token' => $node->daemonSecret + ], + 'json' => [ + 'keys' => [ + $subuser->daemonSecret => $daemonPermissions + ] + ] + ]); + + DB::commit(); + return true; + } catch (\GuzzleHttp\Exception\TransferException $ex) { + DB::rollBack(); + throw new DisplayException('There was an error attempting to connect to the daemon to update permissions.'); + } catch (\Exception $ex) { + DB::rollBack(); + throw $ex; + } + return false; } + +} diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index 7f5b8ade1..b2f344f55 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -5,6 +5,7 @@ namespace Pterodactyl\Repositories; use DB; use Hash; use Validator; +use Mail; use Pterodactyl\Models; use Pterodactyl\Services\UuidService; @@ -48,16 +49,28 @@ class UserRepository $user = new Models\User; $uuid = new UuidService; + DB::beginTransaction(); + $user->uuid = $uuid->generate('users', 'uuid'); $user->email = $email; $user->password = Hash::make($password); $user->language = 'en'; $user->root_admin = ($admin) ? 1 : 0; + $user->save(); try { - $user->save(); + Mail::queue('emails.new-account', [ + 'email' => $user->email, + 'forgot' => route('auth.password'), + 'login' => route('auth.login') + ], function ($message) use ($email) { + $message->to($email); + $message->subject('Pterodactyl - New Account'); + }); + DB::commit(); return $user->id; } catch (\Exception $ex) { + DB::rollBack(); throw $e; } } diff --git a/config/queue.php b/config/queue.php index cf9b09da0..dd2f96038 100644 --- a/config/queue.php +++ b/config/queue.php @@ -16,7 +16,7 @@ return [ | */ - 'default' => env('QUEUE_DRIVER', 'sync'), + 'default' => env('QUEUE_DRIVER', 'database'), /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2016_01_18_235655_create_jobs_table.php b/database/migrations/2016_01_18_235655_create_jobs_table.php new file mode 100644 index 000000000..81b2d29f2 --- /dev/null +++ b/database/migrations/2016_01_18_235655_create_jobs_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->string('queue'); + $table->longText('payload'); + $table->tinyInteger('attempts')->unsigned(); + $table->tinyInteger('reserved')->unsigned(); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + $table->index(['queue', 'reserved', 'reserved_at']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('jobs'); + } +} diff --git a/database/migrations/2016_01_18_235846_create_failed_jobs_table.php b/database/migrations/2016_01_18_235846_create_failed_jobs_table.php new file mode 100644 index 000000000..c1ba41b48 --- /dev/null +++ b/database/migrations/2016_01_18_235846_create_failed_jobs_table.php @@ -0,0 +1,33 @@ +increments('id'); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->timestamp('failed_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('failed_jobs'); + } +} diff --git a/resources/views/emails/added-subuser.blade.php b/resources/views/emails/added-subuser.blade.php new file mode 100644 index 000000000..5bc3fd87c --- /dev/null +++ b/resources/views/emails/added-subuser.blade.php @@ -0,0 +1,9 @@ + +
+You are recieving this email because you have been added as a subuser for {{ $serverName }} on Pterodactyl Panel.
+ + diff --git a/resources/views/emails/new-account.blade.php b/resources/views/emails/new-account.blade.php new file mode 100644 index 000000000..10d31d5d2 --- /dev/null +++ b/resources/views/emails/new-account.blade.php @@ -0,0 +1,12 @@ + + +You are recieving this email because an account has been created for you on Pterodactyl Panel.
+Login: {{ $login }}
+Email: {{ $email }}
+Forgot your password? {{ $forgot }}
+ + diff --git a/resources/views/server/users/index.blade.php b/resources/views/server/users/index.blade.php index f17dfc124..4b9787198 100644 --- a/resources/views/server/users/index.blade.php +++ b/resources/views/server/users/index.blade.php @@ -23,16 +23,68 @@{{ $user->a_userEmail }}
Allows user to set the default connection used for a server as well as view avaliable ports.
+