diff --git a/docker/redis/Dockerfile b/docker/redis/Dockerfile --- a/docker/redis/Dockerfile +++ b/docker/redis/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:30 +FROM fedora:34 ENV container docker ENV SYSTEMD_PAGER='' diff --git a/src/.env.example b/src/.env.example --- a/src/.env.example +++ b/src/.env.example @@ -3,10 +3,11 @@ APP_KEY= APP_DEBUG=true APP_URL=http://127.0.0.1:8000 +#APP_PASSPHRASE= APP_PUBLIC_URL= APP_DOMAIN=kolabnow.com APP_THEME=default -APP_TENANT_ID=1 +APP_TENANT_ID=5 APP_LOCALE=en APP_LOCALES=en,de @@ -34,6 +35,8 @@ SESSION_DRIVER=file SESSION_LIFETIME=120 +OPENEXCHANGERATES_API_KEY="from openexchangerates.org" + MFA_DSN=mysql://roundcube:Welcome2KolabSystems@127.0.0.1/roundcube MFA_TOTP_DIGITS=6 MFA_TOTP_INTERVAL=30 diff --git a/src/app/Auth/SecondFactor.php b/src/app/Auth/SecondFactor.php --- a/src/app/Auth/SecondFactor.php +++ b/src/app/Auth/SecondFactor.php @@ -2,8 +2,6 @@ namespace App\Auth; -use App\Sku; -use App\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Kolab2FA\Storage\Base; @@ -93,17 +91,11 @@ public function factors(): array { // First check if the user has the 2FA SKU - $sku_2fa = Sku::where('title', '2fa')->first(); + if ($this->user->hasSku('2fa')) { + $factors = (array) $this->enumerate(); + $factors = array_unique($factors); - if ($sku_2fa) { - $has_2fa = $this->user->entitlements()->where('sku_id', $sku_2fa->id)->first(); - - if ($has_2fa) { - $factors = (array) $this->enumerate(); - $factors = array_unique($factors); - - return $factors; - } + return $factors; } return []; @@ -186,7 +178,7 @@ */ public static function code(string $email): string { - $sf = new self(User::where('email', $email)->first()); + $sf = new self(\App\User::where('email', $email)->first()); $driver = $sf->getDriver('totp:8132a46b1f741f88de25f47e'); return (string) $driver->get_code(); diff --git a/src/app/Backends/OpenExchangeRates.php b/src/app/Backends/OpenExchangeRates.php --- a/src/app/Backends/OpenExchangeRates.php +++ b/src/app/Backends/OpenExchangeRates.php @@ -7,7 +7,7 @@ /** * Import exchange rates from openexchangerates.org * - * @param string Base currency + * @param string $baseCurrency Base currency * * @return array exchange rates */ diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php --- a/src/app/Console/Command.php +++ b/src/app/Console/Command.php @@ -7,6 +7,13 @@ abstract class Command extends \Illuminate\Console\Command { /** + * This needs to be here to be used. + * + * @var null + */ + protected $commandPrefix = null; + + /** * Annotate this command as being dangerous for any potential unintended consequences. * * Commands are considered dangerous if; @@ -77,6 +84,10 @@ $model = new $objectClass(); } + if ($this->commandPrefix == 'scalpel') { + return $model; + } + $modelsWithTenant = [ \App\Discount::class, \App\Domain::class, @@ -91,22 +102,20 @@ \App\Wallet::class, ]; - $tenant_id = \config('app.tenant_id'); + $tenantId = \config('app.tenant_id'); // Add tenant filter if (in_array($objectClass, $modelsWithTenant)) { - $model = $model->withEnvTenant(); + $model = $model->withEnvTenantContext(); } elseif (in_array($objectClass, $modelsWithOwner)) { - $model = $model->whereExists(function ($query) use ($tenant_id) { + $model = $model->whereExists(function ($query) use ($tenantId) { $query->select(DB::raw(1)) ->from('users') ->whereRaw('wallets.user_id = users.id') - ->whereRaw('users.tenant_id ' . ($tenant_id ? "= $tenant_id" : 'is null')); + ->whereRaw('users.tenant_id ' . ($tenantId ? "= $tenantId" : 'is null')); }); } - // TODO: tenant check for Entitlement, Transaction, etc. - return $model; } diff --git a/src/app/Console/Commands/Domain/CreateCommand.php b/src/app/Console/Commands/Domain/CreateCommand.php --- a/src/app/Console/Commands/Domain/CreateCommand.php +++ b/src/app/Console/Commands/Domain/CreateCommand.php @@ -19,7 +19,7 @@ * * @var string */ - protected $description = "Create a domain."; + protected $description = "Create a domain"; /** * Execute the console command. diff --git a/src/app/Console/Commands/Domain/DeleteCommand.php b/src/app/Console/Commands/Domain/DeleteCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Domain/DeleteCommand.php @@ -0,0 +1,34 @@ +argument('domain'); + + $domain = $this->getDomain($argument); + + if (!$domain) { + $this->error("No such domain {$argument}"); + return 1; + } + + if ($domain->isPublic()) { + $this->error("This domain is a public registration domain."); + return 1; + } + + parent::handle(); + } +} diff --git a/src/app/Console/Commands/Domain/UsersCommand.php b/src/app/Console/Commands/Domain/UsersCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Domain/UsersCommand.php @@ -0,0 +1,13 @@ +getDomain($this->argument('domain')); - - if (!$domain) { - return 1; - } - - $domain->delete(); - } -} diff --git a/src/app/Console/Commands/DomainList.php b/src/app/Console/Commands/DomainList.php --- a/src/app/Console/Commands/DomainList.php +++ b/src/app/Console/Commands/DomainList.php @@ -34,7 +34,7 @@ $domains = Domain::orderBy('namespace'); } - $domains->withEnvTenant()->each( + $domains->withEnvTenantContext()->each( function ($domain) { $msg = $domain->namespace; diff --git a/src/app/Console/Commands/DomainListUsers.php b/src/app/Console/Commands/DomainListUsers.php deleted file mode 100644 --- a/src/app/Console/Commands/DomainListUsers.php +++ /dev/null @@ -1,83 +0,0 @@ -getDomain($this->argument('domain')); - - if (!$domain) { - return 1; - } - - if ($domain->isPublic()) { - $this->error("This domain is a public registration domain."); - return 1; - } - - // TODO: actually implement listing users - $wallet = $domain->wallet(); - - if (!$wallet) { - $this->error("This domain isn't billed to a wallet."); - return 1; - } - - $mailboxSKU = \App\Sku::where('title', 'mailbox')->first(); - - if (!$mailboxSKU) { - $this->error("No mailbox SKU available."); - } - - $entitlements = $wallet->entitlements() - ->where('entitleable_type', \App\User::class) - ->where('sku_id', $mailboxSKU->id)->get(); - - $users = []; - - foreach ($entitlements as $entitlement) { - $users[] = $entitlement->entitleable; - } - - usort($users, function ($a, $b) { - return $a->email > $b->email; - }); - - foreach ($users as $user) { - $this->info($user->email); - } - } -} diff --git a/src/app/Console/Commands/PackageSkus.php b/src/app/Console/Commands/PackageSkus.php --- a/src/app/Console/Commands/PackageSkus.php +++ b/src/app/Console/Commands/PackageSkus.php @@ -28,7 +28,7 @@ */ public function handle() { - $packages = Package::withEnvTenant()->get(); + $packages = Package::withEnvTenantContext()->get(); foreach ($packages as $package) { $this->info(sprintf("Package: %s", $package->title)); diff --git a/src/app/Console/Commands/PlanPackages.php b/src/app/Console/Commands/PlanPackages.php --- a/src/app/Console/Commands/PlanPackages.php +++ b/src/app/Console/Commands/PlanPackages.php @@ -38,7 +38,7 @@ */ public function handle() { - $plans = Plan::withEnvTenant()->get(); + $plans = Plan::withEnvTenantContext()->get(); foreach ($plans as $plan) { $this->info(sprintf("Plan: %s", $plan->title)); diff --git a/src/app/Console/Commands/User/DeleteCommand.php b/src/app/Console/Commands/User/DeleteCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/User/DeleteCommand.php @@ -0,0 +1,15 @@ +withEnvTenantContext()->where('email', $this->argument('user'))->first(); + + if (!$user) { + $user = \App\User::withTrashed()->withEnvTenantContext()->where('id', $this->argument('user'))->first(); + } + + if (!$user) { + $this->error("No such user '" . $this->argument('user') . "' within this tenant context."); + $this->info("Try ./artisan scalpel:user:read --attr=email --attr=tenant_id " . $this->argument('user')); + return 1; + } + + $statuses = [ + 'active' => \App\User::STATUS_ACTIVE, + 'suspended' => \App\User::STATUS_SUSPENDED, + 'deleted' => \App\User::STATUS_DELETED, + 'ldapReady' => \App\User::STATUS_LDAP_READY, + 'imapReady' => \App\User::STATUS_IMAP_READY, + ]; + + $user_state = []; + + foreach (\array_keys($statuses) as $state) { + $func = 'is' . \ucfirst($state); + if ($user->$func()) { + $user_state[] = $state; + } + } + + $this->info("Status: " . \implode(',', $user_state)); + } +} diff --git a/src/app/Console/Commands/UserStatus.php b/src/app/Console/Commands/UserStatus.php deleted file mode 100644 --- a/src/app/Console/Commands/UserStatus.php +++ /dev/null @@ -1,53 +0,0 @@ -getUser($this->argument('user')); - - if (!$user) { - return 1; - } - - $statuses = [ - 'active' => User::STATUS_ACTIVE, - 'suspended' => User::STATUS_SUSPENDED, - 'deleted' => User::STATUS_DELETED, - 'ldapReady' => User::STATUS_LDAP_READY, - 'imapReady' => User::STATUS_IMAP_READY, - ]; - - foreach ($statuses as $text => $bit) { - $func = 'is' . \ucfirst($text); - - $this->info(sprintf("%d %s: %s", $bit, $text, $user->$func())); - } - - $this->info("In total: {$user->status}"); - } -} diff --git a/src/app/Console/Commands/WalletBalances.php b/src/app/Console/Commands/WalletBalances.php --- a/src/app/Console/Commands/WalletBalances.php +++ b/src/app/Console/Commands/WalletBalances.php @@ -29,7 +29,7 @@ { $wallets = \App\Wallet::select('wallets.*') ->join('users', 'users.id', '=', 'wallets.user_id') - ->withEnvTenant('users') + ->withEnvTenantContext('users') ->all(); $wallets->each( diff --git a/src/app/Console/Commands/WalletCharge.php b/src/app/Console/Commands/WalletCharge.php --- a/src/app/Console/Commands/WalletCharge.php +++ b/src/app/Console/Commands/WalletCharge.php @@ -41,7 +41,7 @@ // Get all wallets, excluding deleted accounts $wallets = Wallet::select('wallets.*') ->join('users', 'users.id', '=', 'wallets.user_id') - ->withEnvTenant('users') + ->withEnvTenantContext('users') ->whereNull('users.deleted_at') ->cursor(); } diff --git a/src/app/Console/Commands/WalletExpected.php b/src/app/Console/Commands/WalletExpected.php --- a/src/app/Console/Commands/WalletExpected.php +++ b/src/app/Console/Commands/WalletExpected.php @@ -38,7 +38,7 @@ } else { $wallets = \App\Wallet::select('wallets.*') ->join('users', 'users.id', '=', 'wallets.user_id') - ->withEnvTenant('users') + ->withEnvTenantContext('users') ->all(); } diff --git a/src/app/Console/Development/UserStatus.php b/src/app/Console/Development/UserStatus.php deleted file mode 100644 --- a/src/app/Console/Development/UserStatus.php +++ /dev/null @@ -1,86 +0,0 @@ -argument('userid'))->firstOrFail(); - - $this->info("Found user: {$user->id}"); - - $statuses = [ - 'active' => User::STATUS_ACTIVE, - 'suspended' => User::STATUS_SUSPENDED, - 'deleted' => User::STATUS_DELETED, - 'ldapReady' => User::STATUS_LDAP_READY, - 'imapReady' => User::STATUS_IMAP_READY, - ]; - - // I'd prefer "-state" and "+state" syntax, but it's not possible - $delete = false; - if ($update = $this->option('del')) { - $delete = true; - } elseif ($update = $this->option('add')) { - // do nothing - } - - if (!empty($update)) { - $map = \array_change_key_case($statuses); - $update = \strtolower($update); - - if (isset($map[$update])) { - if ($delete && $user->status & $map[$update]) { - $user->status ^= $map[$update]; - $user->save(); - } elseif (!$delete && !($user->status & $map[$update])) { - $user->status |= $map[$update]; - $user->save(); - } - } - } - - $user_state = []; - foreach (\array_keys($statuses) as $state) { - $func = 'is' . \ucfirst($state); - if ($user->$func()) { - $user_state[] = $state; - } - } - - $this->info("Status: " . \implode(',', $user_state)); - } -} diff --git a/src/app/Console/ObjectDeleteCommand.php b/src/app/Console/ObjectDeleteCommand.php --- a/src/app/Console/ObjectDeleteCommand.php +++ b/src/app/Console/ObjectDeleteCommand.php @@ -36,6 +36,10 @@ $classes = class_uses_recursive($this->objectClass); + if (in_array(SoftDeletes::class, $classes)) { + $this->signature .= " {--with-deleted : Consider deleted {$this->objectName}s}"; + } + parent::__construct(); } @@ -87,9 +91,19 @@ if ($this->commandPrefix == 'scalpel') { $this->objectClass::withoutEvents( function () use ($object) { - $object->delete(); + if ($object->deleted_at) { + $object->forceDelete(); + } else { + $object->delete(); + } } ); + } else { + if ($object->deleted_at) { + $object->forceDelete(); + } else { + $object->delete(); + } } } } diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -115,7 +115,7 @@ */ public static function getPublicDomains(): array { - return self::withEnvTenant() + return self::withEnvTenantContext() ->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC)) ->get(['namespace'])->pluck('namespace')->toArray(); } @@ -420,6 +420,43 @@ } /** + * List the users of a domain, so long as the domain is not a public registration domain. + * + * @return array + */ + public function users(): array + { + if ($this->isPublic()) { + return []; + } + + $wallet = $this->wallet(); + + if (!$wallet) { + return []; + } + + $mailboxSKU = \App\Sku::withObjectTenantContext($this)->where('title', 'mailbox')->first(); + + if (!$mailboxSKU) { + \Log::error("No mailbox SKU available."); + return []; + } + + $entitlements = $wallet->entitlements() + ->where('entitleable_type', \App\User::class) + ->where('sku_id', $mailboxSKU->id)->get(); + + $users = []; + + foreach ($entitlements as $entitlement) { + $users[] = $entitlement->entitleable; + } + + return $users; + } + + /** * Verify if a domain exists in DNS * * @return bool True if registered, False otherwise diff --git a/src/app/Group.php b/src/app/Group.php --- a/src/app/Group.php +++ b/src/app/Group.php @@ -58,7 +58,7 @@ throw new \Exception("Group already assigned to a wallet"); } - $sku = \App\Sku::where('title', 'group')->first(); + $sku = \App\Sku::withObjectTenantContext($this)->where('title', 'group')->first(); $exists = $wallet->entitlements()->where('sku_id', $sku->id)->count(); \App\Entitlement::create([ diff --git a/src/app/Handlers/Beta/Base.php b/src/app/Handlers/Beta/Base.php --- a/src/app/Handlers/Beta/Base.php +++ b/src/app/Handlers/Beta/Base.php @@ -19,14 +19,7 @@ // 2) active and a 'beta' entitlement must exist. if ($sku->active) { - $beta = \App\Sku::where('title', 'beta')->first(); - if (!$beta) { - return false; - } - - if ($user->entitlements()->where('sku_id', $beta->id)->first()) { - return true; - } + return $user->hasSku('beta'); } else { if ($user->entitlements()->where('sku_id', $sku->id)->first()) { return true; diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -43,14 +43,15 @@ $plans = []; // Use reverse order just to have individual on left, group on right ;) - Plan::select()->orderByDesc('title')->get()->map(function ($plan) use (&$plans) { - $plans[] = [ - 'title' => $plan->title, - 'name' => $plan->name, - 'button' => __('app.planbutton', ['plan' => $plan->name]), - 'description' => $plan->description, - ]; - }); + Plan::withEnvTenantContext()->orderByDesc('title')->get() + ->map(function ($plan) use (&$plans) { + $plans[] = [ + 'title' => $plan->title, + 'name' => $plan->name, + 'button' => __('app.planbutton', ['plan' => $plan->name]), + 'description' => $plan->description, + ]; + }); return response()->json(['status' => 'success', 'plans' => $plans]); } @@ -121,7 +122,7 @@ */ public function invitation($id) { - $invitation = SignupInvitation::withEnvTenant()->find($id); + $invitation = SignupInvitation::withEnvTenantContext()->find($id); if (empty($invitation) || $invitation->isCompleted()) { return $this->errorResponse(404); @@ -218,7 +219,7 @@ // Signup via invitation if ($request->invitation) { - $invitation = SignupInvitation::withEnvTenant()->find($request->invitation); + $invitation = SignupInvitation::withEnvTenantContext()->find($request->invitation); if (empty($invitation) || $invitation->isCompleted()) { return $this->errorResponse(404); @@ -345,13 +346,13 @@ if (!$this->plan) { // Get the plan if specified and exists... if ($this->code && $this->code->plan) { - $plan = Plan::where('title', $this->code->plan)->first(); + $plan = Plan::withEnvTenantContext()->where('title', $this->code->plan)->first(); } // ...otherwise use the default plan if (empty($plan)) { // TODO: Get default plan title from config - $plan = Plan::where('title', 'individual')->first(); + $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); } $this->plan = $plan; diff --git a/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php b/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php --- a/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php @@ -8,26 +8,32 @@ class DiscountsController extends Controller { /** - * Returns (active) discounts defined in the system. + * Returns (active) discounts defined in the system for the user context. + * + * @param int $id User identifier * * @return \Illuminate\Http\JsonResponse JSON response */ - public function index() + public function userDiscounts($id) { - $discounts = []; + $user = \App\User::find($id); + + if (!$this->checkTenant($user)) { + return $this->errorResponse(404); + } - Discount::withEnvTenant() + $discounts = Discount::withObjectTenantContext($user) ->where('active', true) ->orderBy('discount') ->get() - ->map(function ($discount) use (&$discounts) { + ->map(function ($discount) { $label = $discount->discount . '% - ' . $discount->description; if ($discount->code) { $label .= " [{$discount->code}]"; } - $discounts[] = [ + return [ 'id' => $discount->id, 'discount' => $discount->discount, 'code' => $discount->code, diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php --- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php @@ -20,7 +20,7 @@ $result = collect([]); if ($owner) { - if ($owner = User::withEnvTenant()->find($owner)) { + if ($owner = User::find($owner)) { foreach ($owner->wallets as $wallet) { $entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get(); @@ -33,7 +33,7 @@ $result = $result->sortBy('namespace')->values(); } } elseif (!empty($search)) { - if ($domain = Domain::withEnvTenant()->where('namespace', $search)->first()) { + if ($domain = Domain::where('namespace', $search)->first()) { $result->push($domain); } } @@ -64,9 +64,9 @@ */ public function suspend(Request $request, $id) { - $domain = Domain::withEnvTenant()->find($id); + $domain = Domain::find($id); - if (empty($domain) || $domain->isPublic()) { + if (!$this->checkTenant($domain) || $domain->isPublic()) { return $this->errorResponse(404); } @@ -88,9 +88,9 @@ */ public function unsuspend(Request $request, $id) { - $domain = Domain::withEnvTenant()->find($id); + $domain = Domain::find($id); - if (empty($domain) || $domain->isPublic()) { + if (!$this->checkTenant($domain) || $domain->isPublic()) { return $this->errorResponse(404); } diff --git a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php --- a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php @@ -20,7 +20,7 @@ $result = collect([]); if ($owner) { - if ($owner = User::withEnvTenant()->find($owner)) { + if ($owner = User::find($owner)) { foreach ($owner->wallets as $wallet) { $wallet->entitlements()->where('entitleable_type', Group::class)->get() ->each(function ($entitlement) use ($result) { @@ -31,7 +31,7 @@ $result = $result->sortBy('namespace')->values(); } } elseif (!empty($search)) { - if ($group = Group::withEnvTenant()->where('email', $search)->first()) { + if ($group = Group::where('email', $search)->first()) { $result->push($group); } } @@ -78,9 +78,9 @@ */ public function suspend(Request $request, $id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::find($id); - if (empty($group)) { + if (!$this->checkTenant($group)) { return $this->errorResponse(404); } @@ -102,9 +102,9 @@ */ public function unsuspend(Request $request, $id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::find($id); - if (empty($group)) { + if (!$this->checkTenant($group)) { return $this->errorResponse(404); } diff --git a/src/app/Http/Controllers/API/V4/Admin/PackagesController.php b/src/app/Http/Controllers/API/V4/Admin/PackagesController.php deleted file mode 100644 --- a/src/app/Http/Controllers/API/V4/Admin/PackagesController.php +++ /dev/null @@ -1,7 +0,0 @@ -withEnvTenant(); + $query = $query->withEnvTenantContext(); } } - // TODO: Tenant selector for admins - return $query; } } diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php --- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php @@ -8,6 +8,7 @@ use App\User; use App\UserAlias; use App\UserSetting; +use App\Wallet; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; @@ -37,19 +38,14 @@ $result = collect([]); if ($owner) { - $owner = User::where('id', $owner) - ->withEnvTenant() - ->whereNull('role') - ->first(); + $owner = User::find($owner); if ($owner) { - $result = $owner->users(false)->whereNull('role')->orderBy('email')->get(); + $result = $owner->users(false)->orderBy('email')->get(); } } elseif (strpos($search, '@')) { // Search by email $result = User::withTrashed()->where('email', $search) - ->withEnvTenant() - ->whereNull('role') ->orderBy('email') ->get(); @@ -72,8 +68,6 @@ if (!$user_ids->isEmpty()) { $result = User::withTrashed()->whereIn('id', $user_ids) - ->withEnvTenant() - ->whereNull('role') ->orderBy('email') ->get(); } @@ -81,36 +75,39 @@ } elseif (is_numeric($search)) { // Search by user ID $user = User::withTrashed()->where('id', $search) - ->withEnvTenant() - ->whereNull('role') ->first(); if ($user) { $result->push($user); } - } elseif (!empty($search)) { + } elseif (strpos($search, '.') !== false) { // Search by domain $domain = Domain::withTrashed()->where('namespace', $search) - ->withEnvTenant() ->first(); if ($domain) { - if ( - ($wallet = $domain->wallet()) - && ($owner = $wallet->owner()->withTrashed()->withEnvTenant()->first()) - && empty($owner->role) - ) { + if (($wallet = $domain->wallet()) && ($owner = $wallet->owner()->withTrashed()->first())) { + $result->push($owner); + } + } + } elseif (!empty($search)) { + $wallet = Wallet::find($search); + + if ($wallet) { + if ($owner = $wallet->owner()->withTrashed()->first()) { $result->push($owner); } } } // Process the result - $result = $result->map(function ($user) { - $data = $user->toArray(); - $data = array_merge($data, self::userStatuses($user)); - return $data; - }); + $result = $result->map( + function ($user) { + $data = $user->toArray(); + $data = array_merge($data, self::userStatuses($user)); + return $data; + } + ); $result = [ 'list' => $result, @@ -131,13 +128,17 @@ */ public function reset2FA(Request $request, $id) { - $user = User::withEnvTenant()->find($id); + $user = User::find($id); - if (empty($user) || !$this->guard()->user()->canUpdate($user)) { + if (!$this->checkTenant($user)) { return $this->errorResponse(404); } - $sku = Sku::where('title', '2fa')->first(); + if (!$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(403); + } + + $sku = Sku::withObjectTenantContext($user)->where('title', '2fa')->first(); // Note: we do select first, so the observer can delete // 2FA preferences from Roundcube database, so don't @@ -152,6 +153,42 @@ } /** + * Display information on the user account specified by $id. + * + * @param int $id The account to show information for. + * + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + $user = User::find($id); + + if (!$this->checkTenant($user)) { + return $this->errorResponse(404); + } + + if (!$this->guard()->user()->canRead($user)) { + return $this->errorResponse(403); + } + + $response = $this->userResponse($user); + + // Simplified Entitlement/SKU information, + // TODO: I agree this format may need to be extended in future + $response['skus'] = []; + foreach ($user->entitlements as $ent) { + $sku = $ent->sku; + if (!isset($response['skus'][$sku->id])) { + $response['skus'][$sku->id] = ['costs' => [], 'count' => 0]; + } + $response['skus'][$sku->id]['count']++; + $response['skus'][$sku->id]['costs'][] = $ent->cost; + } + + return response()->json($response); + } + + /** * Create a new user record. * * @param \Illuminate\Http\Request $request The API request. @@ -173,12 +210,16 @@ */ public function suspend(Request $request, $id) { - $user = User::withEnvTenant()->find($id); + $user = User::find($id); - if (empty($user) || !$this->guard()->user()->canUpdate($user)) { + if (!$this->checkTenant($user)) { return $this->errorResponse(404); } + if (!$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(403); + } + $user->suspend(); return response()->json([ @@ -197,12 +238,16 @@ */ public function unsuspend(Request $request, $id) { - $user = User::withEnvTenant()->find($id); + $user = User::find($id); - if (empty($user) || !$this->guard()->user()->canUpdate($user)) { + if (!$this->checkTenant($user)) { return $this->errorResponse(404); } + if (!$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(403); + } + $user->unsuspend(); return response()->json([ @@ -221,12 +266,16 @@ */ public function update(Request $request, $id) { - $user = User::withEnvTenant()->find($id); + $user = User::find($id); - if (empty($user) || !$this->guard()->user()->canUpdate($user)) { + if (!$this->checkTenant($user)) { return $this->errorResponse(404); } + if (!$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(403); + } + // For now admins can change only user external email address $rules = []; diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php --- a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php @@ -8,7 +8,6 @@ use App\Transaction; use App\Wallet; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; @@ -25,7 +24,7 @@ { $wallet = Wallet::find($id); - if (empty($wallet) || !Auth::guard()->user()->canRead($wallet)) { + if (empty($wallet) || !$this->checkTenant($wallet->owner)) { return $this->errorResponse(404); } @@ -61,9 +60,9 @@ public function oneOff(Request $request, $id) { $wallet = Wallet::find($id); - $user = Auth::guard()->user(); + $user = $this->guard()->user(); - if (empty($wallet) || !$user->canRead($wallet)) { + if (empty($wallet) || !$this->checkTenant($wallet->owner)) { return $this->errorResponse(404); } @@ -130,7 +129,7 @@ { $wallet = Wallet::find($id); - if (empty($wallet) || !Auth::guard()->user()->canRead($wallet)) { + if (empty($wallet) || !$this->checkTenant($wallet->owner)) { return $this->errorResponse(404); } @@ -138,7 +137,7 @@ if (empty($request->discount)) { $wallet->discount()->dissociate(); $wallet->save(); - } elseif ($discount = Discount::withEnvTenant()->find($request->discount)) { + } elseif ($discount = Discount::withObjectTenantContext($wallet->owner)->find($request->discount)) { $wallet->discount()->associate($discount); $wallet->save(); } diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php --- a/src/app/Http/Controllers/API/V4/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/DomainsController.php @@ -7,7 +7,6 @@ use App\Backends\LDAP; use Carbon\Carbon; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; class DomainsController extends Controller { @@ -18,7 +17,7 @@ */ public function index() { - $user = Auth::guard()->user(); + $user = $this->guard()->user(); $list = []; foreach ($user->domains() as $domain) { @@ -51,10 +50,13 @@ */ public function confirm($id) { - $domain = Domain::findOrFail($id); + $domain = Domain::find($id); - // Only owner (or admin) has access to the domain - if (!Auth::guard()->user()->canRead($domain)) { + if (!$this->checkTenant($domain)) { + return $this->errorResponse(404); + } + + if (!$this->guard()->user()->canRead($domain)) { return $this->errorResponse(403); } @@ -117,10 +119,13 @@ */ public function show($id) { - $domain = Domain::withEnvTenant()->findOrFail($id); + $domain = Domain::find($id); + + if (!$this->checkTenant($domain)) { + return $this->errorResponse(404); + } - // Only owner (or admin) has access to the domain - if (!Auth::guard()->user()->canRead($domain)) { + if (!$this->guard()->user()->canRead($domain)) { return $this->errorResponse(403); } @@ -152,10 +157,13 @@ */ public function status($id) { - $domain = Domain::withEnvTenant()->findOrFail($id); + $domain = Domain::find($id); + + if (!$this->checkTenant($domain)) { + return $this->errorResponse(404); + } - // Only owner (or admin) has access to the domain - if (!Auth::guard()->user()->canRead($domain)) { + if (!$this->guard()->user()->canRead($domain)) { return $this->errorResponse(403); } diff --git a/src/app/Http/Controllers/API/V4/GroupsController.php b/src/app/Http/Controllers/API/V4/GroupsController.php --- a/src/app/Http/Controllers/API/V4/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/GroupsController.php @@ -32,9 +32,9 @@ */ public function destroy($id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::find($id); - if (empty($group)) { + if (!$this->checkTenant($group)) { return $this->errorResponse(404); } @@ -96,9 +96,9 @@ */ public function show($id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::find($id); - if (empty($group)) { + if (!$this->checkTenant($group)) { return $this->errorResponse(404); } @@ -123,9 +123,9 @@ */ public function status($id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::find($id); - if (empty($group)) { + if (!$this->checkTenant($group)) { return $this->errorResponse(404); } @@ -308,9 +308,9 @@ */ public function update(Request $request, $id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::find($id); - if (empty($group)) { + if (!$this->checkTenant($group)) { return $this->errorResponse(404); } diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php --- a/src/app/Http/Controllers/API/V4/OpenViduController.php +++ b/src/app/Http/Controllers/API/V4/OpenViduController.php @@ -218,8 +218,7 @@ } // Check if there's still a valid meet entitlement for the room owner - $sku = \App\Sku::where('title', 'meet')->first(); - if ($sku && !$room->owner->entitlements()->where('sku_id', $sku->id)->first()) { + if (!$room->owner->hasSku('meet')) { return $this->errorResponse(404, \trans('meet.room-not-found')); } diff --git a/src/app/Http/Controllers/API/V4/PackagesController.php b/src/app/Http/Controllers/API/V4/PackagesController.php --- a/src/app/Http/Controllers/API/V4/PackagesController.php +++ b/src/app/Http/Controllers/API/V4/PackagesController.php @@ -54,7 +54,7 @@ { // TODO: Packages should have an 'active' flag too, I guess $response = []; - $packages = Package::select()->orderBy('title')->get(); + $packages = Package::withSubjectTenantContext()->select()->orderBy('title')->get(); foreach ($packages as $package) { $response[] = [ diff --git a/src/app/Http/Controllers/API/V4/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php --- a/src/app/Http/Controllers/API/V4/PaymentsController.php +++ b/src/app/Http/Controllers/API/V4/PaymentsController.php @@ -7,7 +7,6 @@ use App\Wallet; use App\Payment; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; class PaymentsController extends Controller @@ -19,7 +18,7 @@ */ public function mandate() { - $user = Auth::guard()->user(); + $user = $this->guard()->user(); // TODO: Wallet selection $wallet = $user->wallets()->first(); @@ -38,10 +37,10 @@ */ public function mandateCreate(Request $request) { - $current_user = Auth::guard()->user(); + $user = $this->guard()->user(); // TODO: Wallet selection - $wallet = $current_user->wallets()->first(); + $wallet = $user->wallets()->first(); // Input validation if ($errors = self::mandateValidate($request, $wallet)) { @@ -81,7 +80,7 @@ */ public function mandateDelete() { - $user = Auth::guard()->user(); + $user = $this->guard()->user(); // TODO: Wallet selection $wallet = $user->wallets()->first(); @@ -107,10 +106,10 @@ */ public function mandateUpdate(Request $request) { - $current_user = Auth::guard()->user(); + $user = $this->guard()->user(); // TODO: Wallet selection - $wallet = $current_user->wallets()->first(); + $wallet = $user->wallets()->first(); // Input validation if ($errors = self::mandateValidate($request, $wallet)) { @@ -190,10 +189,10 @@ */ public function store(Request $request) { - $current_user = Auth::guard()->user(); + $user = $this->guard()->user(); // TODO: Wallet selection - $wallet = $current_user->wallets()->first(); + $wallet = $user->wallets()->first(); $rules = [ 'amount' => 'required|numeric', @@ -244,10 +243,10 @@ // TODO currently unused // public function cancel(Request $request) // { - // $current_user = Auth::guard()->user(); + // $user = $this->guard()->user(); // // TODO: Wallet selection - // $wallet = $current_user->wallets()->first(); + // $wallet = $user->wallets()->first(); // $paymentId = $request->payment; @@ -372,9 +371,9 @@ * * @return \Illuminate\Http\JsonResponse The response */ - public static function paymentMethods(Request $request) + public function paymentMethods(Request $request) { - $user = Auth::guard()->user(); + $user = $this->guard()->user(); // TODO: Wallet selection $wallet = $user->wallets()->first(); @@ -393,9 +392,9 @@ * * @return \Illuminate\Http\JsonResponse The response */ - public static function hasPayments(Request $request) + public function hasPayments(Request $request) { - $user = Auth::guard()->user(); + $user = $this->guard()->user(); // TODO: Wallet selection $wallet = $user->wallets()->first(); @@ -421,9 +420,9 @@ * * @return \Illuminate\Http\JsonResponse The response */ - public static function payments(Request $request) + public function payments(Request $request) { - $user = Auth::guard()->user(); + $user = $this->guard()->user(); // TODO: Wallet selection $wallet = $user->wallets()->first(); diff --git a/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php b/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php @@ -2,6 +2,54 @@ namespace App\Http\Controllers\API\V4\Reseller; +use App\Domain; +use App\User; + class DomainsController extends \App\Http\Controllers\API\V4\Admin\DomainsController { + /** + * Search for domains + * + * @return \Illuminate\Http\JsonResponse + */ + public function index() + { + $search = trim(request()->input('search')); + $owner = trim(request()->input('owner')); + $result = collect([]); + + if ($owner) { + if ($owner = User::withSubjectTenantContext()->find($owner)) { + foreach ($owner->wallets as $wallet) { + $entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get(); + + foreach ($entitlements as $entitlement) { + $domain = $entitlement->entitleable; + $result->push($domain); + } + } + + $result = $result->sortBy('namespace')->values(); + } + } elseif (!empty($search)) { + if ($domain = Domain::withSubjectTenantContext()->where('namespace', $search)->first()) { + $result->push($domain); + } + } + + // Process the result + $result = $result->map(function ($domain) { + $data = $domain->toArray(); + $data = array_merge($data, self::domainStatuses($domain)); + return $data; + }); + + $result = [ + 'list' => $result, + 'count' => count($result), + 'message' => \trans('app.search-foundxdomains', ['x' => count($result)]), + ]; + + return response()->json($result); + } } diff --git a/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php b/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php @@ -2,6 +2,56 @@ namespace App\Http\Controllers\API\V4\Reseller; +use App\Group; +use App\User; + class GroupsController extends \App\Http\Controllers\API\V4\Admin\GroupsController { + /** + * Search for groups + * + * @return \Illuminate\Http\JsonResponse + */ + public function index() + { + $search = trim(request()->input('search')); + $owner = trim(request()->input('owner')); + $result = collect([]); + + if ($owner) { + if ($owner = User::withSubjectTenantContext()->find($owner)) { + foreach ($owner->wallets as $wallet) { + $wallet->entitlements()->where('entitleable_type', Group::class)->get() + ->each(function ($entitlement) use ($result) { + $result->push($entitlement->entitleable); + }); + } + + $result = $result->sortBy('namespace')->values(); + } + } elseif (!empty($search)) { + if ($group = Group::withSubjectTenantContext()->where('email', $search)->first()) { + $result->push($group); + } + } + + // Process the result + $result = $result->map(function ($group) { + $data = [ + 'id' => $group->id, + 'email' => $group->email, + ]; + + $data = array_merge($data, self::groupStatuses($group)); + return $data; + }); + + $result = [ + 'list' => $result, + 'count' => count($result), + 'message' => \trans('app.search-foundxdistlists', ['x' => count($result)]), + ]; + + return response()->json($result); + } } diff --git a/src/app/Http/Controllers/API/V4/Reseller/InvitationsController.php b/src/app/Http/Controllers/API/V4/Reseller/InvitationsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/InvitationsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/InvitationsController.php @@ -28,7 +28,7 @@ */ public function destroy($id) { - $invitation = SignupInvitation::withUserTenant()->find($id); + $invitation = SignupInvitation::withSubjectTenantContext()->find($id); if (empty($invitation)) { return $this->errorResponse(404); @@ -66,7 +66,7 @@ $page = intval(request()->input('page')) ?: 1; $hasMore = false; - $result = SignupInvitation::withUserTenant() + $result = SignupInvitation::withSubjectTenantContext() ->latest() ->limit($pageSize + 1) ->offset($pageSize * ($page - 1)); @@ -108,7 +108,7 @@ */ public function resend($id) { - $invitation = SignupInvitation::withUserTenant()->find($id); + $invitation = SignupInvitation::withSubjectTenantContext()->find($id); if (empty($invitation)) { return $this->errorResponse(404); @@ -139,6 +139,9 @@ $errors = []; $invitations = []; + $envTenantId = \config('app.tenant_id'); + $subjectTenantId = auth()->user()->tenant_id; + if (!empty($request->file) && is_object($request->file)) { // Expected a text/csv file with multiple email addresses if (!$request->file->isValid()) { @@ -194,8 +197,14 @@ $count = 0; foreach ($invitations as $idx => $invitation) { - SignupInvitation::create($invitation); + $inv = SignupInvitation::create($invitation); $count++; + + // Set the invitation tenant to the reseller tenant + if ($envTenantId != $subjectTenantId) { + $inv->tenant_id = $subjectTenantId; + $inv->save(); + } } return response()->json([ diff --git a/src/app/Http/Controllers/API/V4/Reseller/PackagesController.php b/src/app/Http/Controllers/API/V4/Reseller/PackagesController.php deleted file mode 100644 --- a/src/app/Http/Controllers/API/V4/Reseller/PackagesController.php +++ /dev/null @@ -1,7 +0,0 @@ -user(); + + if ($addQuery) { + $query = $addQuery($query, $user->tenant_id); + } else { + $query = $query->withSubjectTenantContext(); + } + + return $query; + } } diff --git a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php --- a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php @@ -2,6 +2,107 @@ namespace App\Http\Controllers\API\V4\Reseller; +use App\Domain; +use App\Group; +use App\User; +use App\UserAlias; +use App\UserSetting; + class UsersController extends \App\Http\Controllers\API\V4\Admin\UsersController { + /** + * Searching of user accounts. + * + * @return \Illuminate\Http\JsonResponse + */ + public function index() + { + $search = trim(request()->input('search')); + $owner = trim(request()->input('owner')); + $result = collect([]); + + if ($owner) { + $owner = User::where('id', $owner) + ->withSubjectTenantContext() + ->whereNull('role') + ->first(); + + if ($owner) { + $result = $owner->users(false)->whereNull('role')->orderBy('email')->get(); + } + } elseif (strpos($search, '@')) { + // Search by email + $result = User::withTrashed()->where('email', $search) + ->withSubjectTenantContext() + ->whereNull('role') + ->orderBy('email') + ->get(); + + if ($result->isEmpty()) { + // Search by an alias + $user_ids = UserAlias::where('alias', $search)->get()->pluck('user_id'); + + // Search by an external email + $ext_user_ids = UserSetting::where('key', 'external_email') + ->where('value', $search) + ->get() + ->pluck('user_id'); + + $user_ids = $user_ids->merge($ext_user_ids)->unique(); + + // Search by a distribution list email + if ($group = Group::withTrashed()->where('email', $search)->first()) { + $user_ids = $user_ids->merge([$group->wallet()->user_id])->unique(); + } + + if (!$user_ids->isEmpty()) { + $result = User::withTrashed()->whereIn('id', $user_ids) + ->withSubjectTenantContext() + ->whereNull('role') + ->orderBy('email') + ->get(); + } + } + } elseif (is_numeric($search)) { + // Search by user ID + $user = User::withTrashed()->where('id', $search) + ->withSubjectTenantContext() + ->whereNull('role') + ->first(); + + if ($user) { + $result->push($user); + } + } elseif (!empty($search)) { + // Search by domain + $domain = Domain::withTrashed()->where('namespace', $search) + ->withSubjectTenantContext() + ->first(); + + if ($domain) { + if ( + ($wallet = $domain->wallet()) + && ($owner = $wallet->owner()->withTrashed()->withSubjectTenantContext()->first()) + && empty($owner->role) + ) { + $result->push($owner); + } + } + } + + // Process the result + $result = $result->map(function ($user) { + $data = $user->toArray(); + $data = array_merge($data, self::userStatuses($user)); + return $data; + }); + + $result = [ + 'list' => $result, + 'count' => count($result), + 'message' => \trans('app.search-foundxusers', ['x' => count($result)]), + ]; + + return response()->json($result); + } } diff --git a/src/app/Http/Controllers/API/V4/Reseller/WalletsController.php b/src/app/Http/Controllers/API/V4/Reseller/WalletsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/WalletsController.php @@ -2,10 +2,6 @@ namespace App\Http\Controllers\API\V4\Reseller; -use App\Discount; -use App\Wallet; -use Illuminate\Http\Request; - class WalletsController extends \App\Http\Controllers\API\V4\Admin\WalletsController { } diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php --- a/src/app/Http/Controllers/API/V4/SkusController.php +++ b/src/app/Http/Controllers/API/V4/SkusController.php @@ -5,7 +5,6 @@ use App\Http\Controllers\Controller; use App\Sku; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; class SkusController extends Controller { @@ -54,7 +53,7 @@ public function index() { // Note: Order by title for consistent ordering in tests - $skus = Sku::withEnvTenant()->where('active', true)->orderBy('title')->get(); + $skus = Sku::withSubjectTenantContext()->where('active', true)->orderBy('title')->get(); $response = []; @@ -120,13 +119,13 @@ */ public function userSkus($id) { - $user = \App\User::withEnvTenant()->find($id); + $user = \App\User::find($id); - if (empty($user)) { + if (!$this->checkTenant($user)) { return $this->errorResponse(404); } - if (!Auth::guard()->user()->canRead($user)) { + if (!$this->guard()->user()->canRead($user)) { return $this->errorResponse(403); } @@ -134,7 +133,7 @@ $response = []; // Note: Order by title for consistent ordering in tests - $skus = Sku::withEnvTenant()->orderBy('title')->get(); + $skus = Sku::withObjectTenantContext($user)->orderBy('title')->get(); foreach ($skus as $sku) { if (!class_exists($sku->handler_class)) { diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -47,7 +47,7 @@ */ public function destroy($id) { - $user = User::find($id); + $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -95,7 +95,7 @@ */ public function show($id) { - $user = User::find($id); + $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -131,7 +131,7 @@ */ public function status($id) { - $user = User::find($id); + $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -289,7 +289,7 @@ return $error_response; } - if (empty($request->package) || !($package = \App\Package::find($request->package))) { + if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) { $errors = ['package' => \trans('validation.packagerequired')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } @@ -340,7 +340,7 @@ */ public function update(Request $request, $id) { - $user = User::find($id); + $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -411,7 +411,7 @@ } // list of skus, [id=>obj] - $skus = Sku::all()->mapWithKeys( + $skus = Sku::withEnvTenantContext()->get()->mapWithKeys( function ($sku) { return [$sku->id => $sku]; } @@ -689,7 +689,7 @@ } // Check if domain exists - $domain = Domain::where('namespace', $domain)->first(); + $domain = Domain::withEnvTenantContext()->where('namespace', $domain)->first(); if (empty($domain)) { return \trans('validation.domaininvalid'); @@ -763,7 +763,7 @@ } // Check if domain exists - $domain = Domain::where('namespace', $domain)->first(); + $domain = Domain::withEnvTenantContext()->where('namespace', $domain)->first(); if (empty($domain)) { return \trans('validation.domaininvalid'); diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php --- a/src/app/Http/Controllers/API/V4/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/WalletsController.php @@ -8,7 +8,6 @@ use App\Providers\PaymentProvider; use Carbon\Carbon; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; /** * API\WalletsController @@ -58,12 +57,12 @@ { $wallet = Wallet::find($id); - if (empty($wallet)) { + if (empty($wallet) || !$this->checkTenant($wallet->owner)) { return $this->errorResponse(404); } // Only owner (or admin) has access to the wallet - if (!Auth::guard()->user()->canRead($wallet)) { + if (!$this->guard()->user()->canRead($wallet)) { return $this->errorResponse(403); } @@ -126,8 +125,12 @@ { $wallet = Wallet::find($id); + if (empty($wallet) || !$this->checkTenant($wallet->owner)) { + return $this->errorResponse(404); + } + // Only owner (or admin) has access to the wallet - if (!Auth::guard()->user()->canRead($wallet)) { + if (!$this->guard()->user()->canRead($wallet)) { abort(403); } @@ -171,8 +174,12 @@ { $wallet = Wallet::find($id); + if (empty($wallet) || !$this->checkTenant($wallet->owner)) { + return $this->errorResponse(404); + } + // Only owner (or admin) has access to the wallet - if (!Auth::guard()->user()->canRead($wallet)) { + if (!$this->guard()->user()->canRead($wallet)) { return $this->errorResponse(403); } @@ -205,8 +212,12 @@ { $wallet = Wallet::find($id); + if (empty($wallet) || !$this->checkTenant($wallet->owner)) { + return $this->errorResponse(404); + } + // Only owner (or admin) has access to the wallet - if (!Auth::guard()->user()->canRead($wallet)) { + if (!$this->guard()->user()->canRead($wallet)) { return $this->errorResponse(403); } diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php --- a/src/app/Http/Controllers/Controller.php +++ b/src/app/Http/Controllers/Controller.php @@ -50,6 +50,29 @@ } /** + * Check if current user has access to the specified object + * by being an admin or existing in the same tenant context. + * + * @param ?object $object Model object + * + * @return bool + */ + protected function checkTenant(object $object = null): bool + { + if (empty($object)) { + return false; + } + + $user = $this->guard()->user(); + + if ($user->role == 'admin') { + return true; + } + + return $object->tenant_id == $user->tenant_id; + } + + /** * Get the guard to be used during authentication. * * @return \Illuminate\Contracts\Auth\Guard diff --git a/src/app/Http/Middleware/AuthenticateReseller.php b/src/app/Http/Middleware/AuthenticateReseller.php --- a/src/app/Http/Middleware/AuthenticateReseller.php +++ b/src/app/Http/Middleware/AuthenticateReseller.php @@ -25,10 +25,6 @@ abort(403, "Unauthorized"); } - if ($user->tenant_id != \config('app.tenant_id')) { - abort(403, "Unauthorized"); - } - return $next($request); } } diff --git a/src/app/Observers/SkuObserver.php b/src/app/Observers/SkuObserver.php --- a/src/app/Observers/SkuObserver.php +++ b/src/app/Observers/SkuObserver.php @@ -24,7 +24,5 @@ } $sku->tenant_id = \config('app.tenant_id'); - - // TODO: We should make sure that tenant_id + title is unique } } diff --git a/src/app/Observers/UserAliasObserver.php b/src/app/Observers/UserAliasObserver.php --- a/src/app/Observers/UserAliasObserver.php +++ b/src/app/Observers/UserAliasObserver.php @@ -30,6 +30,13 @@ return false; } + if ($alias->user) { + if ($alias->user->tenant_id != $domain->tenant_id) { + \Log::error("Reseller for user '{$alias->user->email}' and domain '{$domain->namespace}' differ."); + return false; + } + } + return true; } diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -63,54 +63,96 @@ } // Register some template helpers - Blade::directive('theme_asset', function ($path) { - $path = trim($path, '/\'"'); - return ""; - }); + Blade::directive( + 'theme_asset', + function ($path) { + $path = trim($path, '/\'"'); + return ""; + } + ); + + Builder::macro( + 'withEnvTenantContext', + function (string $table = null) { + $tenantId = \config('app.tenant_id'); - // Query builder 'withEnvTenant' macro - Builder::macro('withEnvTenant', function (string $table = null) { - $tenant_id = \config('app.tenant_id'); + if ($tenantId) { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); + } - if ($tenant_id) { /** @var Builder $this */ - return $this->where(($table ? "$table." : '') . 'tenant_id', $tenant_id); + return $this->whereNull(($table ? "$table." : "") . "tenant_id"); } + ); + + Builder::macro( + 'withObjectTenantContext', + function (object $object, string $table = null) { + // backend artisan cli + if (app()->runningInConsole()) { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $object->tenant_id); + } + + $subject = auth()->user(); + + if ($subject->role == "admin") { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $object->tenant_id); + } - /** @var Builder $this */ - return $this->whereNull(($table ? "$table." : '') . 'tenant_id'); - }); + $tenantId = $subject->tenant_id; - // Query builder 'withUserTenant' macro - Builder::macro('withUserTenant', function (string $table = null) { - $tenant_id = auth()->user()->tenant_id; + if ($tenantId) { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); + } - if ($tenant_id) { /** @var Builder $this */ - return $this->where(($table ? "$table." : '') . 'tenant_id', $tenant_id); + return $this->whereNull(($table ? "$table." : "") . "tenant_id"); } + ); + + Builder::macro( + 'withSubjectTenantContext', + function (string $table = null) { + if ($user = auth()->user()) { + $tenantId = $user->tenant_id; + } else { + $tenantId = \config('app.tenant_id'); + } + + if ($tenantId) { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); + } - /** @var Builder $this */ - return $this->whereNull(($table ? "$table." : '') . 'tenant_id'); - }); + /** @var Builder $this */ + return $this->whereNull(($table ? "$table." : "") . "tenant_id"); + } + ); // Query builder 'whereLike' mocro - Builder::macro('whereLike', function (string $column, string $search, int $mode = 0) { - $search = addcslashes($search, '%_'); - - switch ($mode) { - case 2: - $search .= '%'; - break; - case 1: - $search = '%' . $search; - break; - default: - $search = '%' . $search . '%'; - } + Builder::macro( + 'whereLike', + function (string $column, string $search, int $mode = 0) { + $search = addcslashes($search, '%_'); + + switch ($mode) { + case 2: + $search .= '%'; + break; + case 1: + $search = '%' . $search; + break; + default: + $search = '%' . $search . '%'; + } - /** @var Builder $this */ - return $this->where($column, 'like', $search); - }); + /** @var Builder $this */ + return $this->where($column, 'like', $search); + } + ); } } diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php --- a/src/app/Providers/Payment/Mollie.php +++ b/src/app/Providers/Payment/Mollie.php @@ -583,6 +583,7 @@ ); $availableMethods = []; + foreach ($providerMethods as $method) { $availableMethods[$method->id] = [ 'id' => $method->id, diff --git a/src/app/Tenant.php b/src/app/Tenant.php --- a/src/app/Tenant.php +++ b/src/app/Tenant.php @@ -13,6 +13,7 @@ class Tenant extends Model { protected $fillable = [ + 'id', 'title', ]; diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -312,7 +312,7 @@ if ($this->tenant_id) { $domains = Domain::where('tenant_id', $this->tenant_id); } else { - $domains = Domain::withEnvTenant(); + $domains = Domain::withEnvTenantContext(); } $domains = $domains->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC)) @@ -462,9 +462,9 @@ * * @return bool True if specified SKU entitlement exists */ - public function hasSku($title): bool + public function hasSku(string $title): bool { - $sku = Sku::where('title', $title)->first(); + $sku = Sku::withObjectTenantContext($this)->where('title', $title)->first(); if (!$sku) { return false; diff --git a/src/app/Utils.php b/src/app/Utils.php --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -159,6 +159,38 @@ fclose($fp); } + + /** + * Generate a passphrase. Not intended for use in production, so limited to environments that are not production. + * + * @return string + */ + public static function generatePassphrase() + { + if (\config('app.env') == 'production') { + throw new \Exception("Thou shall not pass!"); + } + + if (\config('app.passphrase')) { + return \config('app.passphrase'); + } + + $alphaLow = 'abcdefghijklmnopqrstuvwxyz'; + $alphaUp = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $num = '0123456789'; + $stdSpecial = '~`!@#$%^&*()-_+=[{]}\\|\'";:/?.>,<'; + + $source = $alphaLow . $alphaUp . $num . $stdSpecial; + + $result = ''; + + for ($x = 0; $x < 16; $x++) { + $result .= substr($source, rand(0, (strlen($source) - 1)), 1); + } + + return $result; + } + /** * Calculate the broadcast address provided a net number and a prefix. * diff --git a/src/config/app.php b/src/config/app.php --- a/src/config/app.php +++ b/src/config/app.php @@ -53,6 +53,8 @@ 'url' => env('APP_URL', 'http://localhost'), + 'passphrase' => env('APP_PASSPHRASE', null), + 'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')), 'asset_url' => env('ASSET_URL', null), diff --git a/src/database/migrations/2020_05_05_095212_create_tenants_table.php b/src/database/migrations/2020_05_05_095212_create_tenants_table.php --- a/src/database/migrations/2020_05_05_095212_create_tenants_table.php +++ b/src/database/migrations/2020_05_05_095212_create_tenants_table.php @@ -23,19 +23,21 @@ } ); - \App\Tenant::create(['title' => 'Kolab Now']); + $tenantId = \config('app.tenant_id'); - foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $table_name) { + $tenant = \App\Tenant::create(['id' => $tenantId, 'title' => 'Kolab Now']); + + foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $tableName) { Schema::table( - $table_name, + $tableName, function (Blueprint $table) { $table->bigInteger('tenant_id')->unsigned()->nullable(); $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null'); } ); - if ($tenant_id = \config('app.tenant_id')) { - DB::statement("UPDATE `{$table_name}` SET `tenant_id` = {$tenant_id}"); + if ($tenantId) { + DB::statement("UPDATE `{$tableName}` SET `tenant_id` = {$tenantId}"); } } @@ -48,9 +50,6 @@ } ); } - - // FIXME: Should we also have package_skus.fee ? - // We have package_skus.cost, but I think it is not used anywhere. } /** @@ -60,9 +59,9 @@ */ public function down() { - foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $table_name) { + foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $tableName) { Schema::table( - $table_name, + $tableName, function (Blueprint $table) { $table->dropForeign(['tenant_id']); $table->dropColumn('tenant_id'); diff --git a/src/database/seeds/local/DiscountSeeder.php b/src/database/seeds/local/DiscountSeeder.php --- a/src/database/seeds/local/DiscountSeeder.php +++ b/src/database/seeds/local/DiscountSeeder.php @@ -38,5 +38,21 @@ 'code' => 'TEST', ] ); + + // We're running in reseller mode, add a sample discount + $tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get(); + + foreach ($tenants as $tenant) { + $discount = Discount::create( + [ + 'description' => "Sample Discount by Reseller '{$tenant->title}'", + 'discount' => 10, + 'active' => true, + ] + ); + + $discount->tenant_id = $tenant->id; + $discount->save(); + } } } diff --git a/src/database/seeds/local/DomainSeeder.php b/src/database/seeds/local/DomainSeeder.php --- a/src/database/seeds/local/DomainSeeder.php +++ b/src/database/seeds/local/DomainSeeder.php @@ -33,7 +33,7 @@ [ 'namespace' => $domain, 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_PUBLIC + 'type' => Domain::TYPE_PUBLIC, ] ); } @@ -43,7 +43,7 @@ [ 'namespace' => \config('app.domain'), 'status' => DOMAIN::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_PUBLIC + 'type' => Domain::TYPE_PUBLIC, ] ); } @@ -59,23 +59,27 @@ [ 'namespace' => $domain, 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_EXTERNAL + 'type' => Domain::TYPE_EXTERNAL, ] ); } - // example tenant domain, note that 'tenant_id' is not a fillable. - $domain = Domain::create( - [ - 'namespace' => 'example-tenant.dev-local', - 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_PUBLIC - ] - ); + // We're running in reseller mode, add a sample discount + $tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get(); - $tenant = \App\Tenant::where('title', 'Sample Tenant')->first(); + foreach ($tenants as $tenant) { + $domainNamespace = strtolower(str_replace(' ', '-', $tenant->title)) . '.dev-local'; - $domain->tenant_id = $tenant->id; - $domain->save(); + $domain = Domain::create( + [ + 'namespace' => $domainNamespace, + 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, + 'type' => Domain::TYPE_PUBLIC, + ] + ); + + $domain->tenant_id = $tenant->id; + $domain->save(); + } } } diff --git a/src/database/seeds/local/PackageSeeder.php b/src/database/seeds/local/PackageSeeder.php --- a/src/database/seeds/local/PackageSeeder.php +++ b/src/database/seeds/local/PackageSeeder.php @@ -15,16 +15,17 @@ */ public function run() { - $skuGroupware = Sku::firstOrCreate(['title' => 'groupware']); - $skuMailbox = Sku::firstOrCreate(['title' => 'mailbox']); - $skuStorage = Sku::firstOrCreate(['title' => 'storage']); + $skuDomain = Sku::where(['title' => 'domain-hosting', 'tenant_id' => \config('app.tenant_id')])->first(); + $skuGroupware = Sku::where(['title' => 'groupware', 'tenant_id' => \config('app.tenant_id')])->first(); + $skuMailbox = Sku::where(['title' => 'mailbox', 'tenant_id' => \config('app.tenant_id')])->first(); + $skuStorage = Sku::where(['title' => 'storage', 'tenant_id' => \config('app.tenant_id')])->first(); $package = Package::create( [ 'title' => 'kolab', 'name' => 'Groupware Account', 'description' => 'A fully functional groupware account.', - 'discount_rate' => 0 + 'discount_rate' => 0, ] ); @@ -40,7 +41,7 @@ // be the number of SKU free units. $package->skus()->updateExistingPivot( $skuStorage, - ['qty' => 2], + ['qty' => 5], false ); @@ -49,7 +50,7 @@ 'title' => 'lite', 'name' => 'Lite Account', 'description' => 'Just mail and no more.', - 'discount_rate' => 0 + 'discount_rate' => 0, ] ); @@ -61,8 +62,8 @@ $package->skus()->saveMany($skus); $package->skus()->updateExistingPivot( - Sku::firstOrCreate(['title' => 'storage']), - ['qty' => 2], + $skuStorage, + ['qty' => 5], false ); @@ -71,14 +72,95 @@ 'title' => 'domain-hosting', 'name' => 'Domain Hosting', 'description' => 'Use your own, existing domain.', - 'discount_rate' => 0 + 'discount_rate' => 0, ] ); $skus = [ - Sku::firstOrCreate(['title' => 'domain-hosting']) + $skuDomain ]; $package->skus()->saveMany($skus); + + // We're running in reseller mode, add a sample discount + $tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get(); + + foreach ($tenants as $tenant) { + $skuDomain = Sku::where(['title' => 'domain-hosting', 'tenant_id' => $tenant->id])->first(); + $skuGroupware = Sku::where(['title' => 'groupware', 'tenant_id' => $tenant->id])->first(); + $skuMailbox = Sku::where(['title' => 'mailbox', 'tenant_id' => $tenant->id])->first(); + $skuStorage = Sku::where(['title' => 'storage', 'tenant_id' => $tenant->id])->first(); + + $package = Package::create( + [ + 'title' => 'kolab', + 'name' => 'Groupware Account', + 'description' => 'A fully functional groupware account.', + 'discount_rate' => 0 + ] + ); + + $package->tenant_id = $tenant->id; + $package->save(); + + $skus = [ + $skuMailbox, + $skuGroupware, + $skuStorage + ]; + + $package->skus()->saveMany($skus); + + // This package contains 2 units of the storage SKU, which just so happens to also + // be the number of SKU free units. + $package->skus()->updateExistingPivot( + $skuStorage, + ['qty' => 5], + false + ); + + $package = Package::create( + [ + 'title' => 'lite', + 'name' => 'Lite Account', + 'description' => 'Just mail and no more.', + 'discount_rate' => 0 + ] + ); + + $package->tenant_id = $tenant->id; + $package->save(); + + $skus = [ + $skuMailbox, + $skuStorage + ]; + + $package->skus()->saveMany($skus); + + $package->skus()->updateExistingPivot( + $skuStorage, + ['qty' => 5], + false + ); + + $package = Package::create( + [ + 'title' => 'domain-hosting', + 'name' => 'Domain Hosting', + 'description' => 'Use your own, existing domain.', + 'discount_rate' => 0 + ] + ); + + $package->tenant_id = $tenant->id; + $package->save(); + + $skus = [ + $skuDomain + ]; + + $package->skus()->saveMany($skus); + } } } diff --git a/src/database/seeds/local/PlanSeeder.php b/src/database/seeds/local/PlanSeeder.php --- a/src/database/seeds/local/PlanSeeder.php +++ b/src/database/seeds/local/PlanSeeder.php @@ -15,101 +15,6 @@ */ public function run() { - /* - $plan = Plan::create( - [ - 'title' => 'family', - 'description' => 'A group of accounts for 2 or more users.', - 'discount_qty' => 0, - 'discount_rate' => 0 - ] - ); - - $packages = [ - Package::firstOrCreate(['title' => 'kolab']), - Package::firstOrCreate(['title' => 'domain-hosting']) - ]; - - $plan->packages()->saveMany($packages); - - $plan->packages()->updateExistingPivot( - Package::firstOrCreate(['title' => 'kolab']), - [ - 'qty_min' => 2, - 'qty_max' => -1, - 'discount_qty' => 2, - 'discount_rate' => 50 - ], - false - ); - - $plan = Plan::create( - [ - 'title' => 'small-business', - 'description' => 'Accounts for small business owners.', - 'discount_qty' => 0, - 'discount_rate' => 10 - ] - ); - - $packages = [ - Package::firstOrCreate(['title' => 'kolab']), - Package::firstOrCreate(['title' => 'domain-hosting']) - ]; - - $plan->packages()->saveMany($packages); - - $plan->packages()->updateExistingPivot( - Package::firstOrCreate(['title' => 'kolab']), - [ - 'qty_min' => 5, - 'qty_max' => 25, - 'discount_qty' => 5, - 'discount_rate' => 30 - ], - false - ); - - $plan = Plan::create( - [ - 'title' => 'large-business', - 'description' => 'Accounts for large businesses.', - 'discount_qty' => 0, - 'discount_rate' => 10 - ] - ); - - $packages = [ - Package::firstOrCreate(['title' => 'kolab']), - Package::firstOrCreate(['title' => 'lite']), - Package::firstOrCreate(['title' => 'domain-hosting']) - ]; - - $plan->packages()->saveMany($packages); - - $plan->packages()->updateExistingPivot( - Package::firstOrCreate(['title' => 'kolab']), - [ - 'qty_min' => 20, - 'qty_max' => -1, - 'discount_qty' => 10, - 'discount_rate' => 10 - ], - false - ); - - $plan->packages()->updateExistingPivot( - Package::firstOrCreate(['title' => 'lite']), - [ - 'qty_min' => 0, - 'qty_max' => -1, - 'discount_qty' => 10, - 'discount_rate' => 10 - ], - false - ); - */ - $description = <<<'EOD'

Everything you need to get started or try Kolab Now, including: