vmailadmin/app/Http/Controllers/VMailController.php
2020-05-23 15:20:01 +02:00

728 lines
27 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Request;
use App\Account;
use App\Alias;
use App\Domain;
use App\DomainUser;
use App\TlsPolicy;
use App\User;
use App\DovecotPw;
use App\Entropy;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Exceptions\PermissionException;
use App\Exceptions\ErrorException;
use Illuminate\Validation\Rule;
class VMailController extends Controller
{
const MinimumEntropy = 50;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
if(Auth::user()->hasRole('admin'))
{
$domains = Domain::all();
return view('layouts/vmail',['domains' => $domains]);
}
elseif(Auth::user()->hasRole('user'))
{
$domains = Auth::user()->cards;
return view('layouts/vmail',['domains' => $domains]);
}
else
{
return view('layouts/unverified',);
}
return view('layouts/vmail',['domains' => $domains]);
}
public function domainsAjax(Request $request)
{
$action = $request->input('action');
try
{
// block unverified users
if(!(Auth::user()->hasRole('admin') || Auth::user()->hasRole('user'))){
throw PermissionException('Not Authorized');
}
if($action == 'create')
{
if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}
$validatedData = $request->validate([
'domain' => ['required', 'string', 'unique:App\Domain,domain'],
]);
$domain = new Domain();
$domain->domain = $validatedData['domain'];
$domain->save();
$domain->users()->attach(Auth::user()->id,['role' => 'own']);
return [$domain];
}
elseif($action == 'rename')
{
if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}
$validatedData = $request->validate([
'domain' => ['required', 'string', 'exists:App\Domain,domain'],
'newname' => ['required', 'string', 'unique:App\Domain,domain'],
]);
$domain = Domain::where('domain',$validatedData['domain'])->first();
$accounts = $domain->accounts;
$aliases = $domain->aliases;
$users = $domain->users;
foreach($users as $u)
{
$domain->users()->detach($u->id);
}
$domain->domain = $validatedData['newname'];
$domain->save();
foreach($accounts as $a)
{
$a->domain()->associate($domain);
$a->save();
}
foreach($aliases as $a)
{
$a->source_domain()->associate($domain);
$a->save();
}
foreach($users as $u)
{
$domain->users()->attach($u->id);
}
$domain->save();
return [$domain];
}
elseif($action == 'drop')
{
if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}
$validatedData = $request->validate([
'domain' => ['required', 'string', 'exists:App\Domain,domain'],
]);
$domain = Domain::where('domain',$validatedData['domain'])->first();
// remove all aliases and accounts related to this domain
$domain->delete();
foreach(Account::where('domain',$validatedData['domain'])->get() as $a){
$a->delete();
}
foreach(Alias::where('source_domain',$validatedData['domain'])->get() as $a){
$a->delete();
}
return true;
}
elseif($action == 'listadmins')
{
$validatedData = $request->validate([
'domain' => ['required', 'string', 'exists:App\Domain,domain'],
]);
$domain = Domain::where('domain',$validatedData['domain'])->first();
if(!Auth::user()->hasRole('admin')){
// if not an admin, check if edit roles are enabled for this domain
$domain->needRole('edit'); // throws exception if curren user does not have this role
}
return $domain->users;
}
elseif($action == 'setadmin')
{
$validatedData = $request->validate([
'domain' => ['required', 'string', 'exists:App\Domain,domain'],
'userid' => ['required', 'int', 'exists:App\User,id'],
'role' => ['required', 'string','in:own,edit,view'],
]);
$domain = Domain::where('domain',$validatedData['domain'])->first();
if(!Auth::user()->hasRole('admin')){
// if not an admin, check if edit roles are enabled for this domain
$domain->needRole('own'); // throws exception if curren user does not have this role
if($validatedData['userid'] == Auth::user()->id)
{
throw ValidationException('Cannot edit own ownership');
}
}
$domain->users()->attach($validatedData['userid'],['role' => $validatedData['userid']]);
$domain->save();
return true;
}
elseif($action == 'deladmin')
{
$validatedData = $request->validate([
'domain' => ['required', 'string', 'exists:App\Domain,domain'],
'userid' => ['required', 'int', 'exists:App\User,id'],
]);
$domain = Domain::where('domain',$validatedData['domain'])->first();
if(!Auth::user()->hasRole('admin')){
// if not an admin, check if edit roles are enabled for this domain
$domain->needRole('own'); // throws exception if curren user does not have this role
if($validatedData['userid'] == Auth::user()->id)
{
throw ValidationException('Cannot remove own ownership');
}
}
$domain->users()->detach($validatedData['userid']);
$domain->save();
return true;
}
elseif($action == 'listusers')
{
$users = User::all();
return $users;
}
elseif($action == 'list')
{
if(Auth::user()->hasRole('admin'))
{
$domains = Domain::all();
}
else
{
$domains = Auth::user()->cards;
}
return $domains;
}
else
{
return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
}
}
catch(ValidationException $v)
{
return response(['fail' => 'validation', 'errors' => $v->errors()],400);
}
catch(PermissionException $x)
{
return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
}
catch(ErrorException $v)
{
return response(['fail' => 'errors', 'errors' => $v->errors()],400);
}
}
public function tlsAjax(Request $request)
{
$action = $request->input('action');
try
{
// allow only admin
if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}
if($action == 'list')
{
$policies = TlsPolicy::all();
return $policies;
}
elseif($action == 'getpolicy')
{
$validatedData = $request->validate([
'domain' => ['required', 'string',],
]);
$domain = Domain::where('domain',$validatedData['domain'])->first();
$tlspolicy = $domain->tlspolicy;
if(empty($tlspolicy)) {
return ['policy' => 'null', 'params' => ''];
} else {
return $tlspolicy;
}
return response(['fail' => 'action', 'errors' => ["Unauthorized for domain '{$validatedData['domain']}'"]],403);
}
elseif($action == 'setpolicy')
{
$validatedData = $request->validate([
'domain' => ['required', 'string', ],
'policy' => ['required', 'string','in:null,none,may,encrypt,dane,dane-only,fingerprint,verify,secure'],
'params' => ['nullable','string'],
]);
$tlspolicy = TlsPolicy::where('domain',$validatedData['domain'])->first();
if($tlspolicy == null){
$tlspolicy = new TlsPolicy(['domain' => $validatedData['domain'],
'policy' => $validatedData['policy'],
'params' => $validatedData['params'] ]);
$tlspolicy->save();
}
else {
$tlspolicy->policy = $validatedData['policy'];
$tlspolicy->params = $validatedData['params'];
$tlspolicy->save();
}
return $tlspolicy;
}
elseif($action == 'delpolicy')
{
$validatedData = $request->validate([
'domain' => ['required', 'string', 'exists:App\TlsPolicy,domain' ],
]);
$tlspolicy = TlsPolicy::where('domain',$validatedData['domain'])->first();
$tlspolicy->delete();
return true;
}
else
{
return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
}
}
catch(ValidationException $v)
{
return response(['fail' => 'validation', 'errors' => $v->errors()],400);
}
catch(PermissionException $x)
{
return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
}
catch(ErrorException $v)
{
return response(['fail' => 'errors', 'errors' => $v->errors()],400);
}
}
public function accountsAjax(Request $request)
{
$action = $request->input('action');
try
{
// block unverified users
if(!(Auth::user()->hasRole('admin') || Auth::user()->hasRole('user'))){
throw PermissionException('Not Authorized');
}
$firstValidatedData = $request->validate([
'domain' => ['required', 'string', 'exists:App\Domain,domain'],
]);
$domain = Domain::where('domain',$firstValidatedData['domain'])->first();
if(!Auth::user()->hasRole('admin')){
// if not an admin, check if edit roles are enabled for this domain
$domain->needRole('edit'); // throws exception if curren user does not have this role
}
if($action == 'create')
{
$validatedData = $request->validate([
'username' => ['required', 'string',],
'enabled' => ['required', 'boolean',],
'sendonly' => ['required', 'boolean',],
'quota' => ['required', 'integer',],
'password' => ['required', 'string',],
]);
if(Account::where('username',$validatedData['username'])->where('domain', $firstValidatedData['domain'])->count() > 0){
throw new ValidationException('Account already exists');
}
// encode password
if(Entropy::Calculate($validatedData['password']) < static::MinimumEntropy) {
throw new ValidationException('Password is not complex enough');
}
$hash = DovecotPw::Encrypt($validatedData['password']);
$account = Account::Create([
'username' => $validatedData['username'],
'domain' => $firstValidatedData['domain'],
'enabled' => $validatedData['enabled'],
'sendonly' => $validatedData['sendonly'],
'quota' => $validatedData['quota'],
'password' => $hash,
]);
$account->save();
return [$account];
}
elseif($action == 'checkusername')
{
$validatedData = $request->validate([
'username' => ['required', 'string', ''],
'id' => ['nullable','integer'],
]);
if(!empty($validatedData['id']))
{
$a = Account::Find($validatedData['id']);
if($validatedData['username'] == $a->username) {
return true;
}
}
return (Account::where('domain',$domain->domain)->where('username',$validatedData['username'])->count() == 0);
}
elseif($action == 'list')
{
$accounts = $domain->accounts;
return $accounts;
}
elseif($action == 'update')
{
$validatedData = $request->validate([
'id' => ['required','integer'],
'username' => ['nullable', 'string',],
'enabled' => ['required', 'boolean',],
'sendonly' => ['required', 'boolean',],
'quota' => ['required', 'integer',],
'password' => ['nullable', 'string',],
]);
$account = Account::Find($validatedData['id']);
if(!empty($validatedData['username']) && $validatedData['username'] != $account->username)
{
if((Account::where('domain',$domain)->where('username',$validatedData['username'])->count()) == 0)
{
$account->username = $validatedData['username'];
}
}
$account->enabled = $validatedData['enabled'];
$account->sendonly = $validatedData['sendonly'];
$account->quota = $validatedData['quota'];
if(!empty($validatedData['password']))
{
if(Entropy::Calculate($validatedData['password']) < static::MinimumEntropy) {
throw new ValidationException('Password is not complex enough');
}
// encode password
$hash = DovecotPw::Encrypt($validatedData['password']);
$account->password = $hash;
}
$account->save();
return [$account,$hash];
}
elseif($action == 'delete')
{
$validatedData = $request->validate([
'id' => ['required','integer'],
]);
$account = Account::Find($validatedData['id']);
$account->delete();
return [];
}
else
{
return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
}
}
catch(ValidationException $v)
{
return response(['fail' => 'validation', 'errors' => $v->errors()],400);
}
catch(PermissionException $x)
{
return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
}
catch(ErrorException $v)
{
return response(['fail' => 'errors', 'errors' => $v->errors()],400);
}
}
public function siteAccountsAjax(Request $request)
{
$action = $request->input('action');
try
{
// Allow only admin
if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}
if($action == 'create')
{
$validatedData = $request->validate([
'username' => ['required', 'string',],
'enabled' => ['required', 'boolean',],
'sendonly' => ['required', 'boolean',],
'quota' => ['required', 'integer',],
'password' => ['required', 'string',],
]);
if(Account::where('username',$validatedData['username'])->where('domain', '')->count() > 0){
throw new ValidationException('Account already exists');
}
// encode password
if(Entropy::Calculate($validatedData['password']) < static::MinimumEntropy) {
throw new ValidationException('Password is not complex enough');
}
$hash = DovecotPw::Encrypt($validatedData['password']);
$account = Account::Create([
'username' => $validatedData['username'],
'domain' => "localhost",
'enabled' => $validatedData['enabled'],
'sendonly' => $validatedData['sendonly'],
'quota' => $validatedData['quota'],
'password' => $hash,
]);
$account->save();
return [$account];
}
elseif($action == 'checkusername')
{
$validatedData = $request->validate([
'username' => ['required', 'string', ''],
'id' => ['nullable','integer'],
]);
if(!empty($validatedData['id']))
{
$a = Account::Find($validatedData['id']);
if($validatedData['username'] == $a->username) {
return true;
}
}
return (Account::where('domain',"localhost")->where('username',$validatedData['username'])->count() == 0);
}
elseif($action == 'list')
{
$accounts = Account::where('domain',"localhost")->get();
return $accounts;
}
elseif($action == 'update')
{
$validatedData = $request->validate([
'id' => ['required','integer'],
'username' => ['nullable', 'string',],
'enabled' => ['required', 'boolean',],
'sendonly' => ['required', 'boolean',],
'quota' => ['required', 'integer',],
'password' => ['nullable', 'string',],
]);
$account = Account::Find($validatedData['id']);
if(!empty($validatedData['username']) && $validatedData['username'] != $account->username)
{
if((Account::where('domain',"localhost")->where('username',$validatedData['username'])->count()) == 0)
{
$account->username = $validatedData['username'];
}
}
$account->enabled = $validatedData['enabled'];
$account->sendonly = $validatedData['sendonly'];
$account->quota = $validatedData['quota'];
if(!empty($validatedData['password']))
{
if(Entropy::Calculate($validatedData['password']) < static::MinimumEntropy) {
throw new ValidationException('Password is not complex enough');
}
// encode password
$hash = DovecotPw::Encrypt($validatedData['password']);
$account->password = $hash;
}
$account->save();
return [$account,$hash];
}
elseif($action == 'delete')
{
$validatedData = $request->validate([
'id' => ['required','integer'],
]);
$account = Account::Find($validatedData['id']);
$account->delete();
return [];
}
else
{
return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
}
}
catch(ValidationException $v)
{
return response(['fail' => 'validation', 'errors' => $v->errors()],400);
}
catch(PermissionException $x)
{
return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
}
catch(ErrorException $v)
{
return response(['fail' => 'errors', 'errors' => $v->errors()],400);
}
}
private function tidyAliases($aliases)
{
// figure out how to merge
$a_list = [];
foreach($aliases as $a)
{
if(empty($a_list[$a->source_username]))
{
$a_list[$a->source_username] = new \stdClass;
$a_list[$a->source_username]->source = $a->source_username;
$a_list[$a->source_username]->dest = [];
}
$a->destination = empty($a->destination_domain)?($a->destination_username):($a->destination_username.'@'.$a->destination_domain);
$a_list[$a->source_username]->dest[] = $a;
}
return array_values($a_list);
}
public function aliasesAjax(Request $request)
{
$action = $request->input('action');
try
{
// block unverified users
if(!(Auth::user()->hasRole('admin') || Auth::user()->hasRole('user'))){
throw PermissionException('Not Authorized');
}
$firstValidatedData = $request->validate([
'domain' => ['required', 'string', 'exists:App\Domain,domain'],
]);
$domain = Domain::where('domain',$firstValidatedData['domain'])->first();
if(!Auth::user()->hasRole('admin')){
// if not an admin, check if edit roles are enabled for this domain
$domain->needRole('edit'); // throws exception if curren user does not have this role
}
if($action == 'checksource')
{
$validatedData = $request->validate([
'source' => ['required', 'string', ''],
'id' => ['nullable','integer'],
]);
if(!empty($validatedData['id']))
{
$a = Alias::Find($validatedData['id']);
if($validatedData['source_username'] == $a->source) {
return true;
}
}
return (Alias::where('source_domain',$domain->domain)->where('source_username',$validatedData['source'])->count() == 0);
}
elseif($action == 'list')
{
return $this->tidyAliases($domain->aliases);
}
elseif($action == 'add_dest')
{
$validatedData = $request->validate([
'source' => ['required','string'],
'destination' => ['nullable', 'string',],
'enabled' => ['required', 'boolean',],
'comment' => ['nullable', 'string',],
]);
$dparts = explode('@',$validatedData['destination'],2);
$a = Alias::Create([
'source_username' => $validatedData['source'],
'source_domain' => $domain->domain,
'destination_username' => $dparts[0],
'destination_domain' => isset($dparts[1])?$dparts[1]:null,
'enabled' => $validatedData['enabled'],
'comment' => $validatedData['comment'],
]);
$a->save();
return $this->tidyAliases([$a]);
}
elseif($action == 'update_dest')
{
$validatedData = $request->validate([
'id' => ['required','integer'],
'destination' => ['nullable', 'string',],
'enabled' => ['required', 'boolean',],
'comment' => ['nullable', 'string',],
]);
$a = Alias::Find($validatedData['id']);
$dparts = explode('@',$validatedData['destination'],2);
$a->destination_username = $dparts[0];
$a->destination_domain = isset($dparts[1])?$dparts[1]:null;
$a->enabled = $validatedData['enabled'];
$a->comment = $validatedData['comment'];
$a->save();
return $this->tidyAliases([$a]);
}
elseif($action == 'delete_dest')
{
$validatedData = $request->validate([
'id' => ['required','integer'],
]);
$a = Alias::Find($validatedData['id']);
$a->delete();
return [];
}
elseif($action == 'delete_src')
{
$validatedData = $request->validate([
'source' => ['required','string'],
]);
$aliases = Alias::where('source_domain',$domain->domain)->where('source_username',$validatedData['source']);
foreach($aliases as $a)
{
$a->delete();
}
return [];
}
else
{
return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
}
}
catch(ValidationException $v)
{
return response(['fail' => 'validation', 'errors' => $v->errors()],400);
}
catch(PermissionException $x)
{
return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
}
catch(ErrorException $v)
{
return response(['fail' => 'errors', 'errors' => $v->errors()],400);
}
}
}