Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2528679
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
65 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/Domain.php b/src/app/Domain.php
index 33013a6d..eafd7c40 100644
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -1,502 +1,485 @@
<?php
namespace App;
use App\Wallet;
use App\Traits\BelongsToTenantTrait;
use App\Traits\DomainConfigTrait;
use App\Traits\EntitleableTrait;
use App\Traits\SettingsTrait;
use App\Traits\UuidIntKeyTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* The eloquent definition of a Domain.
*
* @property string $namespace
* @property int $status
* @property int $tenant_id
* @property int $type
*/
class Domain extends Model
{
use BelongsToTenantTrait;
use DomainConfigTrait;
use EntitleableTrait;
use SettingsTrait;
use SoftDeletes;
use UuidIntKeyTrait;
// we've simply never heard of this domain
public const STATUS_NEW = 1 << 0;
// it's been activated
public const STATUS_ACTIVE = 1 << 1;
// domain has been suspended.
public const STATUS_SUSPENDED = 1 << 2;
// domain has been deleted
public const STATUS_DELETED = 1 << 3;
// ownership of the domain has been confirmed
public const STATUS_CONFIRMED = 1 << 4;
// domain has been verified that it exists in DNS
public const STATUS_VERIFIED = 1 << 5;
// domain has been created in LDAP
public const STATUS_LDAP_READY = 1 << 6;
// open for public registration
public const TYPE_PUBLIC = 1 << 0;
// zone hosted with us
public const TYPE_HOSTED = 1 << 1;
// zone registered externally
public const TYPE_EXTERNAL = 1 << 2;
public const HASH_CODE = 1;
public const HASH_TEXT = 2;
public const HASH_CNAME = 3;
protected $fillable = [
'namespace',
'status',
'type'
];
/**
* Assign a package to a domain. The domain should not belong to any existing entitlements.
*
* @param \App\Package $package The package to assign.
* @param \App\User $user The wallet owner.
*
* @return \App\Domain Self
*/
public function assignPackage($package, $user)
{
// If this domain is public it can not be assigned to a user.
if ($this->isPublic()) {
return $this;
}
// See if this domain is already owned by another user.
$wallet = $this->wallet();
if ($wallet) {
\Log::error(
"Domain {$this->namespace} is already assigned to {$wallet->owner->email}"
);
return $this;
}
- $wallet_id = $user->wallets()->first()->id;
-
- foreach ($package->skus as $sku) {
- for ($i = $sku->pivot->qty; $i > 0; $i--) {
- \App\Entitlement::create(
- [
- 'wallet_id' => $wallet_id,
- 'sku_id' => $sku->id,
- 'cost' => $sku->pivot->cost(),
- 'fee' => $sku->pivot->fee(),
- 'entitleable_id' => $this->id,
- 'entitleable_type' => Domain::class
- ]
- );
- }
- }
-
- return $this;
+ return $this->assignPackageAndWallet($package, $user->wallets()->first());
}
/**
* Return list of public+active domain names (for current tenant)
*/
public static function getPublicDomains(): array
{
return self::withEnvTenantContext()
->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC))
->get(['namespace'])->pluck('namespace')->toArray();
}
/**
* Returns whether this domain is active.
*
* @return bool
*/
public function isActive(): bool
{
return ($this->status & self::STATUS_ACTIVE) > 0;
}
/**
* Returns whether this domain is confirmed the ownership of.
*
* @return bool
*/
public function isConfirmed(): bool
{
return ($this->status & self::STATUS_CONFIRMED) > 0;
}
/**
* Returns whether this domain is deleted.
*
* @return bool
*/
public function isDeleted(): bool
{
return ($this->status & self::STATUS_DELETED) > 0;
}
/**
* Returns whether this domain is registered with us.
*
* @return bool
*/
public function isExternal(): bool
{
return ($this->type & self::TYPE_EXTERNAL) > 0;
}
/**
* Returns whether this domain is hosted with us.
*
* @return bool
*/
public function isHosted(): bool
{
return ($this->type & self::TYPE_HOSTED) > 0;
}
/**
* Returns whether this domain is new.
*
* @return bool
*/
public function isNew(): bool
{
return ($this->status & self::STATUS_NEW) > 0;
}
/**
* Returns whether this domain is public.
*
* @return bool
*/
public function isPublic(): bool
{
return ($this->type & self::TYPE_PUBLIC) > 0;
}
/**
* Returns whether this domain is registered in LDAP.
*
* @return bool
*/
public function isLdapReady(): bool
{
return ($this->status & self::STATUS_LDAP_READY) > 0;
}
/**
* Returns whether this domain is suspended.
*
* @return bool
*/
public function isSuspended(): bool
{
return ($this->status & self::STATUS_SUSPENDED) > 0;
}
/**
* Returns whether this (external) domain has been verified
* to exist in DNS.
*
* @return bool
*/
public function isVerified(): bool
{
return ($this->status & self::STATUS_VERIFIED) > 0;
}
/**
* Ensure the namespace is appropriately cased.
*/
public function setNamespaceAttribute($namespace)
{
$this->attributes['namespace'] = strtolower($namespace);
}
/**
* Domain status mutator
*
* @throws \Exception
*/
public function setStatusAttribute($status)
{
$new_status = 0;
$allowed_values = [
self::STATUS_NEW,
self::STATUS_ACTIVE,
self::STATUS_SUSPENDED,
self::STATUS_DELETED,
self::STATUS_CONFIRMED,
self::STATUS_VERIFIED,
self::STATUS_LDAP_READY,
];
foreach ($allowed_values as $value) {
if ($status & $value) {
$new_status |= $value;
$status ^= $value;
}
}
if ($status > 0) {
throw new \Exception("Invalid domain status: {$status}");
}
if ($this->isPublic()) {
$this->attributes['status'] = $new_status;
return;
}
if ($new_status & self::STATUS_CONFIRMED) {
// if we have confirmed ownership of or management access to the domain, then we have
// also confirmed the domain exists in DNS.
$new_status |= self::STATUS_VERIFIED;
$new_status |= self::STATUS_ACTIVE;
}
if ($new_status & self::STATUS_DELETED && $new_status & self::STATUS_ACTIVE) {
$new_status ^= self::STATUS_ACTIVE;
}
if ($new_status & self::STATUS_SUSPENDED && $new_status & self::STATUS_ACTIVE) {
$new_status ^= self::STATUS_ACTIVE;
}
// if the domain is now active, it is not new anymore.
if ($new_status & self::STATUS_ACTIVE && $new_status & self::STATUS_NEW) {
$new_status ^= self::STATUS_NEW;
}
$this->attributes['status'] = $new_status;
}
/**
* Ownership verification by checking for a TXT (or CNAME) record
* in the domain's DNS (that matches the verification hash).
*
* @return bool True if verification was successful, false otherwise
* @throws \Exception Throws exception on DNS or DB errors
*/
public function confirm(): bool
{
if ($this->isConfirmed()) {
return true;
}
$hash = $this->hash(self::HASH_TEXT);
$confirmed = false;
// Get DNS records and find a matching TXT entry
$records = \dns_get_record($this->namespace, DNS_TXT);
if ($records === false) {
throw new \Exception("Failed to get DNS record for {$this->namespace}");
}
foreach ($records as $record) {
if ($record['txt'] === $hash) {
$confirmed = true;
break;
}
}
// Get DNS records and find a matching CNAME entry
// Note: some servers resolve every non-existing name
// so we need to define left and right side of the CNAME record
// i.e.: kolab-verify IN CNAME <hash>.domain.tld.
if (!$confirmed) {
$cname = $this->hash(self::HASH_CODE) . '.' . $this->namespace;
$records = \dns_get_record('kolab-verify.' . $this->namespace, DNS_CNAME);
if ($records === false) {
throw new \Exception("Failed to get DNS record for {$this->namespace}");
}
foreach ($records as $records) {
if ($records['target'] === $cname) {
$confirmed = true;
break;
}
}
}
if ($confirmed) {
$this->status |= Domain::STATUS_CONFIRMED;
$this->save();
}
return $confirmed;
}
/**
* Generate a verification hash for this domain
*
* @param int $mod One of: HASH_CNAME, HASH_CODE (Default), HASH_TEXT
*
* @return string Verification hash
*/
public function hash($mod = null): string
{
$cname = 'kolab-verify';
if ($mod === self::HASH_CNAME) {
return $cname;
}
$hash = \md5('hkccp-verify-' . $this->namespace);
return $mod === self::HASH_TEXT ? "$cname=$hash" : $hash;
}
/**
* Checks if there are any objects (users/aliases/groups) in a domain.
* Note: Public domains are always reported not empty.
*
* @return bool True if there are no objects assigned, False otherwise
*/
public function isEmpty(): bool
{
if ($this->isPublic()) {
return false;
}
// FIXME: These queries will not use indexes, so maybe we should consider
// wallet/entitlements to search in objects that belong to this domain account?
$suffix = '@' . $this->namespace;
$suffixLen = strlen($suffix);
return !(
\App\User::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists()
|| \App\UserAlias::whereRaw('substr(alias, ?) = ?', [-$suffixLen, $suffix])->exists()
|| \App\Group::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists()
|| \App\Resource::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists()
|| \App\SharedFolder::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists()
);
}
/**
* Suspend this domain.
*
* @return void
*/
public function suspend(): void
{
if ($this->isSuspended()) {
return;
}
$this->status |= Domain::STATUS_SUSPENDED;
$this->save();
}
/**
* Unsuspend this domain.
*
* The domain is unsuspended through either of the following courses of actions;
*
* * The account balance has been topped up, or
* * a suspected spammer has resolved their issues, or
* * the command-line is triggered.
*
* Therefore, we can also confidently set the domain status to 'active' should the ownership of or management
* access to have been confirmed before.
*
* @return void
*/
public function unsuspend(): void
{
if (!$this->isSuspended()) {
return;
}
$this->status ^= Domain::STATUS_SUSPENDED;
if ($this->isConfirmed() && $this->isVerified()) {
$this->status |= Domain::STATUS_ACTIVE;
}
$this->save();
}
/**
* List the users of a domain, so long as the domain is not a public registration domain.
* Note: It returns only users with a mailbox.
*
* @return \App\User[] A list of users
*/
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
* @throws \Exception Throws exception on DNS or DB errors
*/
public function verify(): bool
{
if ($this->isVerified()) {
return true;
}
$records = \dns_get_record($this->namespace, DNS_ANY);
if ($records === false) {
throw new \Exception("Failed to get DNS record for {$this->namespace}");
}
// It may happen that result contains other domains depending on the host DNS setup
// that's why in_array() and not just !empty()
if (in_array($this->namespace, array_column($records, 'host'))) {
$this->status |= Domain::STATUS_VERIFIED;
$this->save();
return true;
}
return false;
}
}
diff --git a/src/app/Group.php b/src/app/Group.php
index 6746d79d..e442e4db 100644
--- a/src/app/Group.php
+++ b/src/app/Group.php
@@ -1,267 +1,235 @@
<?php
namespace App;
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
use App\Traits\GroupConfigTrait;
use App\Traits\SettingsTrait;
use App\Traits\UuidIntKeyTrait;
use App\Wallet;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* The eloquent definition of a Group.
*
* @property int $id The group identifier
* @property string $email An email address
* @property string $members A comma-separated list of email addresses
* @property string $name The group name
* @property int $status The group status
* @property int $tenant_id Tenant identifier
*/
class Group extends Model
{
use BelongsToTenantTrait;
use EntitleableTrait;
use GroupConfigTrait;
use SettingsTrait;
use SoftDeletes;
use UuidIntKeyTrait;
// we've simply never heard of this group
public const STATUS_NEW = 1 << 0;
// group has been activated
public const STATUS_ACTIVE = 1 << 1;
// group has been suspended.
public const STATUS_SUSPENDED = 1 << 2;
// group has been deleted
public const STATUS_DELETED = 1 << 3;
// group has been created in LDAP
public const STATUS_LDAP_READY = 1 << 4;
protected $fillable = [
'email',
'members',
'name',
'status',
];
- /**
- * Assign the group to a wallet.
- *
- * @param \App\Wallet $wallet The wallet
- *
- * @return \App\Group Self
- * @throws \Exception
- */
- public function assignToWallet(Wallet $wallet): Group
- {
- if (empty($this->id)) {
- throw new \Exception("Group not yet exists");
- }
-
- if ($this->entitlements()->count()) {
- throw new \Exception("Group already assigned to a wallet");
- }
-
- $sku = \App\Sku::withObjectTenantContext($this)->where('title', 'group')->first();
- $exists = $wallet->entitlements()->where('sku_id', $sku->id)->count();
-
- \App\Entitlement::create([
- 'wallet_id' => $wallet->id,
- 'sku_id' => $sku->id,
- 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
- 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
- 'entitleable_id' => $this->id,
- 'entitleable_type' => Group::class
- ]);
-
- return $this;
- }
/**
* Returns group domain.
*
* @return ?\App\Domain The domain group belongs to, NULL if it does not exist
*/
public function domain(): ?Domain
{
list($local, $domainName) = explode('@', $this->email);
return Domain::where('namespace', $domainName)->first();
}
/**
* Find whether an email address exists as a group (including deleted groups).
*
* @param string $email Email address
* @param bool $return_group Return Group instance instead of boolean
*
* @return \App\Group|bool True or Group model object if found, False otherwise
*/
public static function emailExists(string $email, bool $return_group = false)
{
if (strpos($email, '@') === false) {
return false;
}
$email = \strtolower($email);
$group = self::withTrashed()->where('email', $email)->first();
if ($group) {
return $return_group ? $group : true;
}
return false;
}
/**
* Group members propert accessor. Converts internal comma-separated list into an array
*
* @param string $members Comma-separated list of email addresses
*
* @return array Email addresses of the group members, as an array
*/
public function getMembersAttribute($members): array
{
return $members ? explode(',', $members) : [];
}
/**
* Returns whether this group is active.
*
* @return bool
*/
public function isActive(): bool
{
return ($this->status & self::STATUS_ACTIVE) > 0;
}
/**
* Returns whether this group is deleted.
*
* @return bool
*/
public function isDeleted(): bool
{
return ($this->status & self::STATUS_DELETED) > 0;
}
/**
* Returns whether this group is new.
*
* @return bool
*/
public function isNew(): bool
{
return ($this->status & self::STATUS_NEW) > 0;
}
/**
* Returns whether this group is registered in LDAP.
*
* @return bool
*/
public function isLdapReady(): bool
{
return ($this->status & self::STATUS_LDAP_READY) > 0;
}
/**
* Returns whether this group is suspended.
*
* @return bool
*/
public function isSuspended(): bool
{
return ($this->status & self::STATUS_SUSPENDED) > 0;
}
/**
* Ensure the email is appropriately cased.
*
* @param string $email Group email address
*/
public function setEmailAttribute(string $email)
{
$this->attributes['email'] = strtolower($email);
}
/**
* Ensure the members are appropriately formatted.
*
* @param array $members Email addresses of the group members
*/
public function setMembersAttribute(array $members): void
{
$members = array_unique(array_filter(array_map('strtolower', $members)));
sort($members);
$this->attributes['members'] = implode(',', $members);
}
/**
* Group status mutator
*
* @throws \Exception
*/
public function setStatusAttribute($status)
{
$new_status = 0;
$allowed_values = [
self::STATUS_NEW,
self::STATUS_ACTIVE,
self::STATUS_SUSPENDED,
self::STATUS_DELETED,
self::STATUS_LDAP_READY,
];
foreach ($allowed_values as $value) {
if ($status & $value) {
$new_status |= $value;
$status ^= $value;
}
}
if ($status > 0) {
throw new \Exception("Invalid group status: {$status}");
}
$this->attributes['status'] = $new_status;
}
/**
* Suspend this group.
*
* @return void
*/
public function suspend(): void
{
if ($this->isSuspended()) {
return;
}
$this->status |= Group::STATUS_SUSPENDED;
$this->save();
}
/**
* Unsuspend this group.
*
* @return void
*/
public function unsuspend(): void
{
if (!$this->isSuspended()) {
return;
}
$this->status ^= Group::STATUS_SUSPENDED;
$this->save();
}
}
diff --git a/src/app/Resource.php b/src/app/Resource.php
index 7345b755..e0f4cbff 100644
--- a/src/app/Resource.php
+++ b/src/app/Resource.php
@@ -1,209 +1,176 @@
<?php
namespace App;
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
use App\Traits\ResourceConfigTrait;
use App\Traits\SettingsTrait;
use App\Traits\UuidIntKeyTrait;
use App\Wallet;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* The eloquent definition of a Resource.
*
* @property int $id The resource identifier
* @property string $email An email address
* @property string $name The resource name
* @property int $status The resource status
* @property int $tenant_id Tenant identifier
*/
class Resource extends Model
{
use BelongsToTenantTrait;
use EntitleableTrait;
use ResourceConfigTrait;
use SettingsTrait;
use SoftDeletes;
use UuidIntKeyTrait;
// we've simply never heard of this resource
public const STATUS_NEW = 1 << 0;
// resource has been activated
public const STATUS_ACTIVE = 1 << 1;
// resource has been suspended.
// public const STATUS_SUSPENDED = 1 << 2;
// resource has been deleted
public const STATUS_DELETED = 1 << 3;
// resource has been created in LDAP
public const STATUS_LDAP_READY = 1 << 4;
// resource has been created in IMAP
public const STATUS_IMAP_READY = 1 << 8;
protected $fillable = [
'email',
'name',
'status',
];
/** @var ?string Domain name for a resource to be created */
public $domain;
- /**
- * Assign the resource to a wallet.
- *
- * @param \App\Wallet $wallet The wallet
- *
- * @return \App\Resource Self
- * @throws \Exception
- */
- public function assignToWallet(Wallet $wallet): Resource
- {
- if (empty($this->id)) {
- throw new \Exception("Resource not yet exists");
- }
-
- if ($this->entitlements()->count()) {
- throw new \Exception("Resource already assigned to a wallet");
- }
-
- $sku = \App\Sku::withObjectTenantContext($this)->where('title', 'resource')->first();
- $exists = $wallet->entitlements()->where('sku_id', $sku->id)->count();
-
- \App\Entitlement::create([
- 'wallet_id' => $wallet->id,
- 'sku_id' => $sku->id,
- 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
- 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
- 'entitleable_id' => $this->id,
- 'entitleable_type' => Resource::class
- ]);
-
- return $this;
- }
-
/**
* Returns the resource domain.
*
* @return ?\App\Domain The domain to which the resource belongs to, NULL if it does not exist
*/
public function domain(): ?Domain
{
if (isset($this->domain)) {
$domainName = $this->domain;
} else {
list($local, $domainName) = explode('@', $this->email);
}
return Domain::where('namespace', $domainName)->first();
}
/**
* Find whether an email address exists as a resource (including deleted resources).
*
* @param string $email Email address
* @param bool $return_resource Return Resource instance instead of boolean
*
* @return \App\Resource|bool True or Resource model object if found, False otherwise
*/
public static function emailExists(string $email, bool $return_resource = false)
{
if (strpos($email, '@') === false) {
return false;
}
$email = \strtolower($email);
$resource = self::withTrashed()->where('email', $email)->first();
if ($resource) {
return $return_resource ? $resource : true;
}
return false;
}
/**
* Returns whether this resource is active.
*
* @return bool
*/
public function isActive(): bool
{
return ($this->status & self::STATUS_ACTIVE) > 0;
}
/**
* Returns whether this resource is deleted.
*
* @return bool
*/
public function isDeleted(): bool
{
return ($this->status & self::STATUS_DELETED) > 0;
}
/**
* Returns whether this resource's folder exists in IMAP.
*
* @return bool
*/
public function isImapReady(): bool
{
return ($this->status & self::STATUS_IMAP_READY) > 0;
}
/**
* Returns whether this resource is registered in LDAP.
*
* @return bool
*/
public function isLdapReady(): bool
{
return ($this->status & self::STATUS_LDAP_READY) > 0;
}
/**
* Returns whether this resource is new.
*
* @return bool
*/
public function isNew(): bool
{
return ($this->status & self::STATUS_NEW) > 0;
}
/**
* Resource status mutator
*
* @throws \Exception
*/
public function setStatusAttribute($status)
{
$new_status = 0;
$allowed_values = [
self::STATUS_NEW,
self::STATUS_ACTIVE,
self::STATUS_DELETED,
self::STATUS_IMAP_READY,
self::STATUS_LDAP_READY,
];
foreach ($allowed_values as $value) {
if ($status & $value) {
$new_status |= $value;
$status ^= $value;
}
}
if ($status > 0) {
throw new \Exception("Invalid resource status: {$status}");
}
$this->attributes['status'] = $new_status;
}
}
diff --git a/src/app/SharedFolder.php b/src/app/SharedFolder.php
index e22df5cf..a8d68d88 100644
--- a/src/app/SharedFolder.php
+++ b/src/app/SharedFolder.php
@@ -1,229 +1,196 @@
<?php
namespace App;
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
use App\Traits\SharedFolderConfigTrait;
use App\Traits\SettingsTrait;
use App\Traits\UuidIntKeyTrait;
use App\Wallet;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* The eloquent definition of a SharedFolder.
*
* @property string $email An email address
* @property int $id The folder identifier
* @property string $name The folder name
* @property int $status The folder status
* @property int $tenant_id Tenant identifier
* @property string $type The folder type
*/
class SharedFolder extends Model
{
use BelongsToTenantTrait;
use EntitleableTrait;
use SharedFolderConfigTrait;
use SettingsTrait;
use SoftDeletes;
use UuidIntKeyTrait;
// we've simply never heard of this folder
public const STATUS_NEW = 1 << 0;
// folder has been activated
public const STATUS_ACTIVE = 1 << 1;
// folder has been suspended.
// public const STATUS_SUSPENDED = 1 << 2;
// folder has been deleted
public const STATUS_DELETED = 1 << 3;
// folder has been created in LDAP
public const STATUS_LDAP_READY = 1 << 4;
// folder has been created in IMAP
public const STATUS_IMAP_READY = 1 << 8;
/** @const array Supported folder type labels */
public const SUPPORTED_TYPES = ['mail', 'event', 'contact', 'task', 'note'];
/** @var array Mass-assignable properties */
protected $fillable = [
'email',
'name',
'status',
'type',
];
/** @var ?string Domain name for a shared folder to be created */
public $domain;
- /**
- * Assign the folder to a wallet.
- *
- * @param \App\Wallet $wallet The wallet
- *
- * @return \App\SharedFolder Self
- * @throws \Exception
- */
- public function assignToWallet(Wallet $wallet): SharedFolder
- {
- if (empty($this->id)) {
- throw new \Exception("Shared folder not yet exists");
- }
-
- if ($this->entitlements()->count()) {
- throw new \Exception("Shared folder already assigned to a wallet");
- }
-
- $sku = \App\Sku::withObjectTenantContext($this)->where('title', 'shared-folder')->first();
- $exists = $wallet->entitlements()->where('sku_id', $sku->id)->count();
-
- \App\Entitlement::create([
- 'wallet_id' => $wallet->id,
- 'sku_id' => $sku->id,
- 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
- 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
- 'entitleable_id' => $this->id,
- 'entitleable_type' => SharedFolder::class
- ]);
-
- return $this;
- }
-
/**
* Returns the shared folder domain.
*
* @return ?\App\Domain The domain to which the folder belongs to, NULL if it does not exist
*/
public function domain(): ?Domain
{
if (isset($this->domain)) {
$domainName = $this->domain;
} else {
list($local, $domainName) = explode('@', $this->email);
}
return Domain::where('namespace', $domainName)->first();
}
/**
* Find whether an email address exists as a shared folder (including deleted folders).
*
* @param string $email Email address
* @param bool $return_folder Return SharedFolder instance instead of boolean
*
* @return \App\SharedFolder|bool True or Resource model object if found, False otherwise
*/
public static function emailExists(string $email, bool $return_folder = false)
{
if (strpos($email, '@') === false) {
return false;
}
$email = \strtolower($email);
$folder = self::withTrashed()->where('email', $email)->first();
if ($folder) {
return $return_folder ? $folder : true;
}
return false;
}
/**
* Returns whether this folder is active.
*
* @return bool
*/
public function isActive(): bool
{
return ($this->status & self::STATUS_ACTIVE) > 0;
}
/**
* Returns whether this folder is deleted.
*
* @return bool
*/
public function isDeleted(): bool
{
return ($this->status & self::STATUS_DELETED) > 0;
}
/**
* Returns whether this folder exists in IMAP.
*
* @return bool
*/
public function isImapReady(): bool
{
return ($this->status & self::STATUS_IMAP_READY) > 0;
}
/**
* Returns whether this folder is registered in LDAP.
*
* @return bool
*/
public function isLdapReady(): bool
{
return ($this->status & self::STATUS_LDAP_READY) > 0;
}
/**
* Returns whether this folder is new.
*
* @return bool
*/
public function isNew(): bool
{
return ($this->status & self::STATUS_NEW) > 0;
}
/**
* Folder status mutator
*
* @throws \Exception
*/
public function setStatusAttribute($status)
{
$new_status = 0;
$allowed_values = [
self::STATUS_NEW,
self::STATUS_ACTIVE,
self::STATUS_DELETED,
self::STATUS_IMAP_READY,
self::STATUS_LDAP_READY,
];
foreach ($allowed_values as $value) {
if ($status & $value) {
$new_status |= $value;
$status ^= $value;
}
}
if ($status > 0) {
throw new \Exception("Invalid shared folder status: {$status}");
}
$this->attributes['status'] = $new_status;
}
/**
* Folder type mutator
*
* @throws \Exception
*/
public function setTypeAttribute($type)
{
if (!in_array($type, self::SUPPORTED_TYPES)) {
throw new \Exception("Invalid shared folder type: {$type}");
}
$this->attributes['type'] = $type;
}
}
diff --git a/src/app/Traits/EntitleableTrait.php b/src/app/Traits/EntitleableTrait.php
index 52e0deae..bef0c947 100644
--- a/src/app/Traits/EntitleableTrait.php
+++ b/src/app/Traits/EntitleableTrait.php
@@ -1,39 +1,195 @@
<?php
namespace App\Traits;
+use App\Entitlement;
+use App\Sku;
+use App\Wallet;
+use Illuminate\Support\Str;
+
trait EntitleableTrait
{
+ /**
+ * Assign a package to an entitleable object. It should not have any existing entitlements.
+ *
+ * @param \App\Package $package The package
+ * @param \App\Wallet $wallet The wallet
+ *
+ * @return $this
+ */
+ public function assignPackageAndWallet(\App\Package $package, Wallet $wallet)
+ {
+ // TODO: There should be some sanity checks here. E.g. not package can be
+ // assigned to any entitleable, but we don't really have package types.
+
+ foreach ($package->skus as $sku) {
+ for ($i = $sku->pivot->qty; $i > 0; $i--) {
+ Entitlement::create([
+ 'wallet_id' => $wallet->id,
+ 'sku_id' => $sku->id,
+ 'cost' => $sku->pivot->cost(),
+ 'fee' => $sku->pivot->fee(),
+ 'entitleable_id' => $this->id,
+ 'entitleable_type' => self::class
+ ]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assign a Sku to an entitleable object.
+ *
+ * @param \App\Sku $sku The sku to assign.
+ * @param int $count Count of entitlements to add
+ *
+ * @return $this
+ * @throws \Exception
+ */
+ public function assignSku(Sku $sku, int $count = 1)
+ {
+ // TODO: I guess wallet could be parametrized in future
+ $wallet = $this->wallet();
+ $exists = $this->entitlements()->where('sku_id', $sku->id)->count();
+
+ // TODO: Make sure the SKU can be assigned to the object
+
+ while ($count > 0) {
+ Entitlement::create([
+ 'wallet_id' => $wallet->id,
+ 'sku_id' => $sku->id,
+ 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
+ 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
+ 'entitleable_id' => $this->id,
+ 'entitleable_type' => self::class
+ ]);
+
+ $exists++;
+ $count--;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assign the object to a wallet.
+ *
+ * @param \App\Wallet $wallet The wallet
+ *
+ * @return $this
+ * @throws \Exception
+ */
+ public function assignToWallet(Wallet $wallet)
+ {
+ if (empty($this->id)) {
+ throw new \Exception("Object not yet exists");
+ }
+
+ if ($this->entitlements()->count()) {
+ throw new \Exception("Object already assigned to a wallet");
+ }
+
+ // Find the SKU title, e.g. \App\SharedFolder -> shared-folder
+ // Note: it does not work with User/Domain model (yet)
+ $title = Str::kebab(\class_basename(self::class));
+
+ $sku = Sku::withObjectTenantContext($this)->where('title', $title)->first();
+ $exists = $wallet->entitlements()->where('sku_id', $sku->id)->count();
+
+ Entitlement::create([
+ 'wallet_id' => $wallet->id,
+ 'sku_id' => $sku->id,
+ 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
+ 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
+ 'entitleable_id' => $this->id,
+ 'entitleable_type' => self::class
+ ]);
+
+ return $this;
+ }
+
/**
* Entitlements for this object.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entitlements()
{
- return $this->hasMany(\App\Entitlement::class, 'entitleable_id', 'id')
+ return $this->hasMany(Entitlement::class, 'entitleable_id', 'id')
->where('entitleable_type', self::class);
}
+ /**
+ * Check if an entitlement for the specified SKU exists.
+ *
+ * @param string $title The SKU title
+ *
+ * @return bool True if specified SKU entitlement exists
+ */
+ public function hasSku(string $title): bool
+ {
+ $sku = Sku::withObjectTenantContext($this)->where('title', $title)->first();
+
+ if (!$sku) {
+ return false;
+ }
+
+ return $this->entitlements()->where('sku_id', $sku->id)->count() > 0;
+ }
+
+ /**
+ * Remove a number of entitlements for the SKU.
+ *
+ * @param \App\Sku $sku The SKU
+ * @param int $count The number of entitlements to remove
+ *
+ * @return $this
+ */
+ public function removeSku(Sku $sku, int $count = 1)
+ {
+ $entitlements = $this->entitlements()
+ ->where('sku_id', $sku->id)
+ ->orderBy('cost', 'desc')
+ ->orderBy('created_at')
+ ->get();
+
+ $entitlements_count = count($entitlements);
+
+ foreach ($entitlements as $entitlement) {
+ if ($entitlements_count <= $sku->units_free) {
+ continue;
+ }
+
+ if ($count > 0) {
+ $entitlement->delete();
+ $entitlements_count--;
+ $count--;
+ }
+ }
+
+ return $this;
+ }
+
/**
* Returns the wallet by which the object is controlled
*
* @return ?\App\Wallet A wallet object
*/
- public function wallet(): ?\App\Wallet
+ public function wallet(): ?Wallet
{
$entitlement = $this->entitlements()->withTrashed()->orderBy('created_at', 'desc')->first();
if ($entitlement) {
return $entitlement->wallet;
}
// TODO: No entitlement should not happen, but in tests we have
// such cases, so we fallback to the user's wallet in this case
if ($this instanceof \App\User) {
return $this->wallets()->first();
}
return null;
}
}
diff --git a/src/app/User.php b/src/app/User.php
index 595cf940..6b086486 100644
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -1,909 +1,809 @@
<?php
namespace App;
use App\Entitlement;
use App\UserAlias;
use App\Sku;
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
use App\Traits\UserAliasesTrait;
use App\Traits\UserConfigTrait;
use App\Traits\UuidIntKeyTrait;
use App\Traits\SettingsTrait;
use App\Wallet;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Hash;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Iatstuti\Database\Support\NullableFields;
use Laravel\Passport\HasApiTokens;
use League\OAuth2\Server\Exception\OAuthServerException;
/**
* The eloquent definition of a User.
*
* @property string $email
* @property int $id
* @property string $password
* @property int $status
* @property int $tenant_id
*/
class User extends Authenticatable
{
use BelongsToTenantTrait;
use EntitleableTrait;
use HasApiTokens;
use NullableFields;
use UserConfigTrait;
use UserAliasesTrait;
use UuidIntKeyTrait;
use SettingsTrait;
use SoftDeletes;
// a new user, default on creation
public const STATUS_NEW = 1 << 0;
// it's been activated
public const STATUS_ACTIVE = 1 << 1;
// user has been suspended
public const STATUS_SUSPENDED = 1 << 2;
// user has been deleted
public const STATUS_DELETED = 1 << 3;
// user has been created in LDAP
public const STATUS_LDAP_READY = 1 << 4;
// user mailbox has been created in IMAP
public const STATUS_IMAP_READY = 1 << 5;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'id',
'email',
'password',
'password_ldap',
'status',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'password_ldap',
'role'
];
protected $nullable = [
'password',
'password_ldap'
];
/**
* Any wallets on which this user is a controller.
*
* This does not include wallets owned by the user.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function accounts()
{
return $this->belongsToMany(
'App\Wallet', // The foreign object definition
'user_accounts', // The table name
'user_id', // The local foreign key
'wallet_id' // The remote foreign key
);
}
/**
* Email aliases of this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function aliases()
{
return $this->hasMany('App\UserAlias', 'user_id');
}
/**
* Assign a package to a user. The user should not have any existing entitlements.
*
* @param \App\Package $package The package to assign.
* @param \App\User|null $user Assign the package to another user.
*
* @return \App\User
*/
public function assignPackage($package, $user = null)
{
if (!$user) {
$user = $this;
}
- $wallet_id = $this->wallets()->first()->id;
-
- foreach ($package->skus as $sku) {
- for ($i = $sku->pivot->qty; $i > 0; $i--) {
- \App\Entitlement::create(
- [
- 'wallet_id' => $wallet_id,
- 'sku_id' => $sku->id,
- 'cost' => $sku->pivot->cost(),
- 'fee' => $sku->pivot->fee(),
- 'entitleable_id' => $user->id,
- 'entitleable_type' => User::class
- ]
- );
- }
- }
-
- return $user;
+ return $user->assignPackageAndWallet($package, $this->wallets()->first());
}
/**
* Assign a package plan to a user.
*
* @param \App\Plan $plan The plan to assign
* @param \App\Domain $domain Optional domain object
*
* @return \App\User Self
*/
public function assignPlan($plan, $domain = null): User
{
$this->setSetting('plan_id', $plan->id);
foreach ($plan->packages as $package) {
if ($package->isDomain()) {
$domain->assignPackage($package, $this);
} else {
$this->assignPackage($package);
}
}
return $this;
}
- /**
- * Assign a Sku to a user.
- *
- * @param \App\Sku $sku The sku to assign.
- * @param int $count Count of entitlements to add
- *
- * @return \App\User Self
- * @throws \Exception
- */
- public function assignSku(Sku $sku, int $count = 1): User
- {
- // TODO: I guess wallet could be parametrized in future
- $wallet = $this->wallet();
- $exists = $this->entitlements()->where('sku_id', $sku->id)->count();
-
- while ($count > 0) {
- \App\Entitlement::create([
- 'wallet_id' => $wallet->id,
- 'sku_id' => $sku->id,
- 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
- 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
- 'entitleable_id' => $this->id,
- 'entitleable_type' => User::class
- ]);
-
- $exists++;
- $count--;
- }
-
- return $this;
- }
-
/**
* Check if current user can delete another object.
*
* @param mixed $object A user|domain|wallet|group object
*
* @return bool True if he can, False otherwise
*/
public function canDelete($object): bool
{
if (!method_exists($object, 'wallet')) {
return false;
}
$wallet = $object->wallet();
// TODO: For now controller can delete/update the account owner,
// this may change in future, controllers are not 0-regression feature
return $wallet && ($wallet->user_id == $this->id || $this->accounts->contains($wallet));
}
/**
* Check if current user can read data of another object.
*
* @param mixed $object A user|domain|wallet|group object
*
* @return bool True if he can, False otherwise
*/
public function canRead($object): bool
{
if ($this->role == 'admin') {
return true;
}
if ($object instanceof User && $this->id == $object->id) {
return true;
}
if ($this->role == 'reseller') {
if ($object instanceof User && $object->role == 'admin') {
return false;
}
if ($object instanceof Wallet && !empty($object->owner)) {
$object = $object->owner;
}
return isset($object->tenant_id) && $object->tenant_id == $this->tenant_id;
}
if ($object instanceof Wallet) {
return $object->user_id == $this->id || $object->controllers->contains($this);
}
if (!method_exists($object, 'wallet')) {
return false;
}
$wallet = $object->wallet();
return $wallet && ($wallet->user_id == $this->id || $this->accounts->contains($wallet));
}
/**
* Check if current user can update data of another object.
*
* @param mixed $object A user|domain|wallet|group object
*
* @return bool True if he can, False otherwise
*/
public function canUpdate($object): bool
{
if ($object instanceof User && $this->id == $object->id) {
return true;
}
if ($this->role == 'admin') {
return true;
}
if ($this->role == 'reseller') {
if ($object instanceof User && $object->role == 'admin') {
return false;
}
if ($object instanceof Wallet && !empty($object->owner)) {
$object = $object->owner;
}
return isset($object->tenant_id) && $object->tenant_id == $this->tenant_id;
}
return $this->canDelete($object);
}
/**
* Return the \App\Domain for this user.
*
* @return \App\Domain|null
*/
public function domain()
{
list($local, $domainName) = explode('@', $this->email);
$domain = \App\Domain::withTrashed()->where('namespace', $domainName)->first();
return $domain;
}
/**
* List the domains to which this user is entitled.
*
* @param bool $with_accounts Include domains assigned to wallets
* the current user controls but not owns.
* @param bool $with_public Include active public domains (for the user tenant).
*
* @return Domain[] List of Domain objects
*/
public function domains($with_accounts = true, $with_public = true): array
{
$domains = [];
if ($with_public) {
if ($this->tenant_id) {
$domains = Domain::where('tenant_id', $this->tenant_id);
} else {
$domains = Domain::withEnvTenantContext();
}
$domains = $domains->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC))
->whereRaw(sprintf('(status & %s)', Domain::STATUS_ACTIVE))
->get()
->all();
}
foreach ($this->wallets as $wallet) {
$entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get();
foreach ($entitlements as $entitlement) {
$domains[] = $entitlement->entitleable;
}
}
if ($with_accounts) {
foreach ($this->accounts as $wallet) {
$entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get();
foreach ($entitlements as $entitlement) {
$domains[] = $entitlement->entitleable;
}
}
}
return $domains;
}
/**
* Find whether an email address exists as a user (including deleted users).
*
* @param string $email Email address
* @param bool $return_user Return User instance instead of boolean
*
* @return \App\User|bool True or User model object if found, False otherwise
*/
public static function emailExists(string $email, bool $return_user = false)
{
if (strpos($email, '@') === false) {
return false;
}
$email = \strtolower($email);
$user = self::withTrashed()->where('email', $email)->first();
if ($user) {
return $return_user ? $user : true;
}
return false;
}
/**
* Helper to find user by email address, whether it is
* main email address, alias or an external email.
*
* If there's more than one alias NULL will be returned.
*
* @param string $email Email address
* @param bool $external Search also for an external email
*
* @return \App\User|null User model object if found
*/
public static function findByEmail(string $email, bool $external = false): ?User
{
if (strpos($email, '@') === false) {
return null;
}
$email = \strtolower($email);
$user = self::where('email', $email)->first();
if ($user) {
return $user;
}
$aliases = UserAlias::where('alias', $email)->get();
if (count($aliases) == 1) {
return $aliases->first()->user;
}
// TODO: External email
return null;
}
/**
* Return groups controlled by the current user.
*
* @param bool $with_accounts Include groups assigned to wallets
* the current user controls but not owns.
*
* @return \Illuminate\Database\Eloquent\Builder Query builder
*/
public function groups($with_accounts = true)
{
$wallets = $this->wallets()->pluck('id')->all();
if ($with_accounts) {
$wallets = array_merge($wallets, $this->accounts()->pluck('wallet_id')->all());
}
return Group::select(['groups.*', 'entitlements.wallet_id'])
->distinct()
->join('entitlements', 'entitlements.entitleable_id', '=', 'groups.id')
->whereIn('entitlements.wallet_id', $wallets)
->where('entitlements.entitleable_type', Group::class);
}
- /**
- * Check if user has an entitlement for the specified SKU.
- *
- * @param string $title The SKU title
- *
- * @return bool True if specified SKU entitlement exists
- */
- public function hasSku(string $title): bool
- {
- $sku = Sku::withObjectTenantContext($this)->where('title', $title)->first();
-
- if (!$sku) {
- return false;
- }
-
- return $this->entitlements()->where('sku_id', $sku->id)->count() > 0;
- }
-
/**
* Returns whether this domain is active.
*
* @return bool
*/
public function isActive(): bool
{
return ($this->status & self::STATUS_ACTIVE) > 0;
}
/**
* Returns whether this domain is deleted.
*
* @return bool
*/
public function isDeleted(): bool
{
return ($this->status & self::STATUS_DELETED) > 0;
}
/**
* Returns whether this (external) domain has been verified
* to exist in DNS.
*
* @return bool
*/
public function isImapReady(): bool
{
return ($this->status & self::STATUS_IMAP_READY) > 0;
}
/**
* Returns whether this user is registered in LDAP.
*
* @return bool
*/
public function isLdapReady(): bool
{
return ($this->status & self::STATUS_LDAP_READY) > 0;
}
/**
* Returns whether this user is new.
*
* @return bool
*/
public function isNew(): bool
{
return ($this->status & self::STATUS_NEW) > 0;
}
/**
* Returns whether this domain is suspended.
*
* @return bool
*/
public function isSuspended(): bool
{
return ($this->status & self::STATUS_SUSPENDED) > 0;
}
/**
* A shortcut to get the user name.
*
* @param bool $fallback Return "<aa.name> User" if there's no name
*
* @return string Full user name
*/
public function name(bool $fallback = false): string
{
$settings = $this->getSettings(['first_name', 'last_name']);
$name = trim($settings['first_name'] . ' ' . $settings['last_name']);
if (empty($name) && $fallback) {
return trim(\trans('app.siteuser', ['site' => \App\Tenant::getConfig($this->tenant_id, 'app.name')]));
}
return $name;
}
- /**
- * Remove a number of entitlements for the SKU.
- *
- * @param \App\Sku $sku The SKU
- * @param int $count The number of entitlements to remove
- *
- * @return User Self
- */
- public function removeSku(Sku $sku, int $count = 1): User
- {
- $entitlements = $this->entitlements()
- ->where('sku_id', $sku->id)
- ->orderBy('cost', 'desc')
- ->orderBy('created_at')
- ->get();
-
- $entitlements_count = count($entitlements);
-
- foreach ($entitlements as $entitlement) {
- if ($entitlements_count <= $sku->units_free) {
- continue;
- }
-
- if ($count > 0) {
- $entitlement->delete();
- $entitlements_count--;
- $count--;
- }
- }
-
- return $this;
- }
-
/**
* Return resources controlled by the current user.
*
* @param bool $with_accounts Include resources assigned to wallets
* the current user controls but not owns.
*
* @return \Illuminate\Database\Eloquent\Builder Query builder
*/
public function resources($with_accounts = true)
{
$wallets = $this->wallets()->pluck('id')->all();
if ($with_accounts) {
$wallets = array_merge($wallets, $this->accounts()->pluck('wallet_id')->all());
}
return \App\Resource::select(['resources.*', 'entitlements.wallet_id'])
->distinct()
->join('entitlements', 'entitlements.entitleable_id', '=', 'resources.id')
->whereIn('entitlements.wallet_id', $wallets)
->where('entitlements.entitleable_type', \App\Resource::class);
}
/**
* Return shared folders controlled by the current user.
*
* @param bool $with_accounts Include folders assigned to wallets
* the current user controls but not owns.
*
* @return \Illuminate\Database\Eloquent\Builder Query builder
*/
public function sharedFolders($with_accounts = true)
{
$wallets = $this->wallets()->pluck('id')->all();
if ($with_accounts) {
$wallets = array_merge($wallets, $this->accounts()->pluck('wallet_id')->all());
}
return \App\SharedFolder::select(['shared_folders.*', 'entitlements.wallet_id'])
->distinct()
->join('entitlements', 'entitlements.entitleable_id', '=', 'shared_folders.id')
->whereIn('entitlements.wallet_id', $wallets)
->where('entitlements.entitleable_type', \App\SharedFolder::class);
}
public function senderPolicyFrameworkWhitelist($clientName)
{
$setting = $this->getSetting('spf_whitelist');
if (!$setting) {
return false;
}
$whitelist = json_decode($setting);
$matchFound = false;
foreach ($whitelist as $entry) {
if (substr($entry, 0, 1) == '/') {
$match = preg_match($entry, $clientName);
if ($match) {
$matchFound = true;
}
continue;
}
if (substr($entry, 0, 1) == '.') {
if (substr($clientName, (-1 * strlen($entry))) == $entry) {
$matchFound = true;
}
continue;
}
if ($entry == $clientName) {
$matchFound = true;
continue;
}
}
return $matchFound;
}
/**
* Suspend this domain.
*
* @return void
*/
public function suspend(): void
{
if ($this->isSuspended()) {
return;
}
$this->status |= User::STATUS_SUSPENDED;
$this->save();
}
/**
* Unsuspend this domain.
*
* @return void
*/
public function unsuspend(): void
{
if (!$this->isSuspended()) {
return;
}
$this->status ^= User::STATUS_SUSPENDED;
$this->save();
}
/**
* Return users controlled by the current user.
*
* @param bool $with_accounts Include users assigned to wallets
* the current user controls but not owns.
*
* @return \Illuminate\Database\Eloquent\Builder Query builder
*/
public function users($with_accounts = true)
{
$wallets = $this->wallets()->pluck('id')->all();
if ($with_accounts) {
$wallets = array_merge($wallets, $this->accounts()->pluck('wallet_id')->all());
}
return $this->select(['users.*', 'entitlements.wallet_id'])
->distinct()
->leftJoin('entitlements', 'entitlements.entitleable_id', '=', 'users.id')
->whereIn('entitlements.wallet_id', $wallets)
->where('entitlements.entitleable_type', User::class);
}
/**
* Verification codes for this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function verificationcodes()
{
return $this->hasMany('App\VerificationCode', 'user_id', 'id');
}
/**
* Wallets this user owns.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function wallets()
{
return $this->hasMany('App\Wallet');
}
/**
* User password mutator
*
* @param string $password The password in plain text.
*
* @return void
*/
public function setPasswordAttribute($password)
{
if (!empty($password)) {
$this->attributes['password'] = bcrypt($password, [ "rounds" => 12 ]);
$this->attributes['password_ldap'] = '{SSHA512}' . base64_encode(
pack('H*', hash('sha512', $password))
);
}
}
/**
* User LDAP password mutator
*
* @param string $password The password in plain text.
*
* @return void
*/
public function setPasswordLdapAttribute($password)
{
$this->setPasswordAttribute($password);
}
/**
* User status mutator
*
* @throws \Exception
*/
public function setStatusAttribute($status)
{
$new_status = 0;
$allowed_values = [
self::STATUS_NEW,
self::STATUS_ACTIVE,
self::STATUS_SUSPENDED,
self::STATUS_DELETED,
self::STATUS_LDAP_READY,
self::STATUS_IMAP_READY,
];
foreach ($allowed_values as $value) {
if ($status & $value) {
$new_status |= $value;
$status ^= $value;
}
}
if ($status > 0) {
throw new \Exception("Invalid user status: {$status}");
}
$this->attributes['status'] = $new_status;
}
/**
* Validate the user credentials
*
* @param string $username The username.
* @param string $password The password in plain text.
* @param bool $updatePassword Store the password if currently empty
*
* @return bool true on success
*/
public function validateCredentials(string $username, string $password, bool $updatePassword = true): bool
{
$authenticated = false;
if ($this->email === \strtolower($username)) {
if (!empty($this->password)) {
if (Hash::check($password, $this->password)) {
$authenticated = true;
}
} elseif (!empty($this->password_ldap)) {
if (substr($this->password_ldap, 0, 6) == "{SSHA}") {
$salt = substr(base64_decode(substr($this->password_ldap, 6)), 20);
$hash = '{SSHA}' . base64_encode(
sha1($password . $salt, true) . $salt
);
if ($hash == $this->password_ldap) {
$authenticated = true;
}
} elseif (substr($this->password_ldap, 0, 9) == "{SSHA512}") {
$salt = substr(base64_decode(substr($this->password_ldap, 9)), 64);
$hash = '{SSHA512}' . base64_encode(
pack('H*', hash('sha512', $password . $salt)) . $salt
);
if ($hash == $this->password_ldap) {
$authenticated = true;
}
}
} else {
\Log::error("Incomplete credentials for {$this->email}");
}
}
if ($authenticated) {
\Log::info("Successful authentication for {$this->email}");
// TODO: update last login time
if ($updatePassword && (empty($this->password) || empty($this->password_ldap))) {
$this->password = $password;
$this->save();
}
} else {
// TODO: Try actual LDAP?
\Log::info("Authentication failed for {$this->email}");
}
return $authenticated;
}
/**
* Retrieve and authenticate a user
*
* @param string $username The username.
* @param string $password The password in plain text.
* @param string $secondFactor The second factor (secondfactor from current request is used as fallback).
*
* @return array ['user', 'reason', 'errorMessage']
*/
public static function findAndAuthenticate($username, $password, $secondFactor = null): ?array
{
$user = User::where('email', $username)->first();
if (!$user) {
return ['reason' => 'notfound', 'errorMessage' => "User not found."];
}
if (!$user->validateCredentials($username, $password)) {
return ['reason' => 'credentials', 'errorMessage' => "Invalid password."];
}
if (!$secondFactor) {
// Check the request if there is a second factor provided
// as fallback.
$secondFactor = request()->secondfactor;
}
try {
(new \App\Auth\SecondFactor($user))->validate($secondFactor);
} catch (\Exception $e) {
return ['reason' => 'secondfactor', 'errorMessage' => $e->getMessage()];
}
return ['user' => $user];
}
/**
* Hook for passport
*
* @throws \Throwable
*
* @return \App\User User model object if found
*/
public function findAndValidateForPassport($username, $password): User
{
$result = self::findAndAuthenticate($username, $password);
if (isset($result['reason'])) {
if ($result['reason'] == 'secondfactor') {
// This results in a json response of {'error': 'secondfactor', 'error_description': '$errorMessage'}
throw new OAuthServerException($result['errorMessage'], 6, 'secondfactor', 401);
}
throw OAuthServerException::invalidCredentials();
}
return $result['user'];
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Feb 1, 5:22 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426755
Default Alt Text
(65 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment