Add Max Concurrent Connections for database users

Closes #1849

Allows database users to be limited to a number of concurrent connections to prevent one user from connecting hundreds of time and bottlenecking the MySQL server.
This commit is contained in:
AreYouScared 2020-04-22 06:00:04 -04:00
parent 2c3a9228ec
commit f0e4764a11
10 changed files with 56 additions and 6 deletions

View File

@ -68,9 +68,10 @@ interface DatabaseRepositoryInterface extends RepositoryInterface
* @param string $username * @param string $username
* @param string $remote * @param string $remote
* @param string $password * @param string $password
* @param string $max_connections
* @return bool * @return bool
*/ */
public function createUser(string $username, string $remote, string $password): bool; public function createUser(string $username, string $remote, string $password, string $max_connections): bool;
/** /**
* Give a specific user access to a given database. * Give a specific user access to a given database.

View File

@ -30,7 +30,7 @@ class Database extends Model
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'server_id', 'database_host_id', 'database', 'username', 'password', 'remote', 'server_id', 'database_host_id', 'database', 'username', 'password', 'remote', 'max_connections',
]; ];
/** /**
@ -51,6 +51,7 @@ class Database extends Model
'database_host_id' => 'required|exists:database_hosts,id', 'database_host_id' => 'required|exists:database_hosts,id',
'database' => 'required|string|alpha_dash|between:3,100', 'database' => 'required|string|alpha_dash|between:3,100',
'username' => 'string|alpha_dash|between:3,100', 'username' => 'string|alpha_dash|between:3,100',
'max_connections' => 'string',
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
'password' => 'string', 'password' => 'string',
]; ];

View File

@ -135,11 +135,12 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
* @param string $username * @param string $username
* @param string $remote * @param string $remote
* @param string $password * @param string $password
* @param string $max_connections
* @return bool * @return bool
*/ */
public function createUser(string $username, string $remote, string $password): bool public function createUser(string $username, string $remote, string $password, string $max_connections): bool
{ {
return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password)); return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\' WITH MAX_USER_CONNECTIONS %s', $username, $remote, $password, $max_connections));
} }
/** /**

View File

@ -84,7 +84,8 @@ class DatabaseManagementService
$this->repository->createUser( $this->repository->createUser(
$database->username, $database->username,
$database->remote, $database->remote,
$this->encrypter->decrypt($database->password) $this->encrypter->decrypt($database->password),
$database->max_connections
); );
$this->repository->assignUserToDatabase( $this->repository->assignUserToDatabase(
$database->database, $database->database,

View File

@ -71,7 +71,7 @@ class DatabasePasswordService
]); ]);
$this->repository->dropUser($database->username, $database->remote); $this->repository->dropUser($database->username, $database->remote);
$this->repository->createUser($database->username, $database->remote, $password); $this->repository->createUser($database->username, $database->remote, $password, $database->max_connections);
$this->repository->assignUserToDatabase($database->database, $database->username, $database->remote); $this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
$this->repository->flush(); $this->repository->flush();
}); });

View File

@ -56,6 +56,7 @@ class ServerDatabaseTransformer extends BaseTransformer
'database' => $model->database, 'database' => $model->database,
'username' => $model->username, 'username' => $model->username,
'remote' => $model->remote, 'remote' => $model->remote,
'max_connections' => $model->max_connections,
'created_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $model->created_at) 'created_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $model->created_at)
->setTimezone(config('app.timezone')) ->setTimezone(config('app.timezone'))
->toIso8601String(), ->toIso8601String(),

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddMaxConnectionsColumnToDatabasesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('databases', function (Blueprint $table) {
$table->integer('max_connections')->nullable(false)->default(0)->after('password');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('databases', function (Blueprint $table) {
$table->dropColumn('max_connections');
});
}
}

View File

@ -151,6 +151,10 @@ export default ({ database, className }: Props) => {
<p className={'text-sm'}>{database.username}</p> <p className={'text-sm'}>{database.username}</p>
<p className={'mt-1 text-2xs text-neutral-500 uppercase select-none'}>Username</p> <p className={'mt-1 text-2xs text-neutral-500 uppercase select-none'}>Username</p>
</div> </div>
<div className={'ml-8 text-center'}>
<p className={'text-sm'}>{database.max_connections}</p>
<p className={'mt-1 text-2xs text-neutral-500 uppercase select-none'}>Max Connections</p>
</div>
<div className={'ml-8'}> <div className={'ml-8'}>
<button className={'btn btn-sm btn-secondary mr-2'} onClick={() => setConnectionVisible(true)}> <button className={'btn btn-sm btn-secondary mr-2'} onClick={() => setConnectionVisible(true)}>
<FontAwesomeIcon icon={faEye} fixedWidth={true}/> <FontAwesomeIcon icon={faEye} fixedWidth={true}/>

View File

@ -99,6 +99,7 @@
<th>Database Name</th> <th>Database Name</th>
<th>Username</th> <th>Username</th>
<th>Connections From</th> <th>Connections From</th>
<th>Max Connections</th>
<th></th> <th></th>
</tr> </tr>
@foreach($databases as $database) @foreach($databases as $database)
@ -107,6 +108,7 @@
<td class="middle">{{ $database->database }}</td> <td class="middle">{{ $database->database }}</td>
<td class="middle">{{ $database->username }}</td> <td class="middle">{{ $database->username }}</td>
<td class="middle">{{ $database->remote }}</td> <td class="middle">{{ $database->remote }}</td>
<td class="middle">{{ $database->max_connections }}</td>
<td class="text-center"> <td class="text-center">
<a href="{{ route('admin.servers.view.database', $database->getRelation('server')->id) }}"> <a href="{{ route('admin.servers.view.database', $database->getRelation('server')->id) }}">
<button class="btn btn-xs btn-primary">Manage</button> <button class="btn btn-xs btn-primary">Manage</button>

View File

@ -37,6 +37,7 @@
<th>Username</th> <th>Username</th>
<th>Connections From</th> <th>Connections From</th>
<th>Host</th> <th>Host</th>
<th>Max Conenctions</th>
<th></th> <th></th>
</tr> </tr>
@foreach($server->databases as $database) @foreach($server->databases as $database)
@ -45,6 +46,7 @@
<td>{{ $database->username }}</td> <td>{{ $database->username }}</td>
<td>{{ $database->remote }}</td> <td>{{ $database->remote }}</td>
<td><code>{{ $database->host->host }}:{{ $database->host->port }}</code></td> <td><code>{{ $database->host->host }}:{{ $database->host->port }}</code></td>
<td>{{ $database->max_connections }}</td>
<td class="text-center"> <td class="text-center">
<button data-action="reset-password" data-id="{{ $database->id }}" class="btn btn-xs btn-primary"><i class="fa fa-refresh"></i></button> <button data-action="reset-password" data-id="{{ $database->id }}" class="btn btn-xs btn-primary"><i class="fa fa-refresh"></i></button>
<button data-action="remove" data-id="{{ $database->id }}" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i></button> <button data-action="remove" data-id="{{ $database->id }}" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i></button>
@ -83,6 +85,11 @@
<input id="pRemote" type="text" name="remote" class="form-control" value="%" /> <input id="pRemote" type="text" name="remote" class="form-control" value="%" />
<p class="text-muted small">This should reflect the IP address that connections are allowed from. Uses standard MySQL notation. If unsure leave as <code>%</code>.</p> <p class="text-muted small">This should reflect the IP address that connections are allowed from. Uses standard MySQL notation. If unsure leave as <code>%</code>.</p>
</div> </div>
<div class="form-group">
<label for="pmax_connections" class="control-label">Max Concurrent Connections</label>
<input id="pmax_connections" type="text" name="max_connections" class="form-control" value="150" />
<p class="text-muted small">This should reflect the max number of concurrent connections from this user to the database. Use <code>0</code> for unlimited</p>
</div>
</div> </div>
<div class="box-footer"> <div class="box-footer">
{!! csrf_field() !!} {!! csrf_field() !!}