Page MenuHomePhorge

No OneTemporary

Size
65 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline