<?php
namespace Platform\SecurityBundle\Entity\Identity;
use Cms\ContainerBundle\Entity\Container;
use Cms\CoreBundle\Model\Interfaces\OneRosterable\OneRosterableInterface;
use Cms\CoreBundle\Model\Interfaces\OneRosterable\OneRosterableTrait;
use Cms\ImportBundle\Model\Interfaces\Importable\ImportableInterface;
use Cms\ImportBundle\Model\Interfaces\Importable\ImportableTrait;
use Common\Util\Passwords;
use DateTime;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use League\OAuth2\Server\Entities\UserEntityInterface;
use Doctrine\Common\Collections\Collection;
use Platform\SecurityBundle\Entity\Access\RoleAssociation\AccountRoleAssociation;
use Platform\SecurityBundle\Entity\Access\SpecialPermissions;
use Cms\TenantBundle\Entity\TenantedEntity;
use Doctrine\ORM\Mapping as ORM;
use Platform\SecurityBundle\Entity\Profiles\SystemProfile;
use Platform\SecurityBundle\Service\Sentry;
use Serializable;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
use App\Entity\OAuth2\Admin\AbstractAdminToken;
/**
* Defines a user account in the system that a person can use to gain access to the system.
*
* Class Account
* @package Platform\SecurityBundle\Entity\Identity
*
* @ORM\Entity(repositoryClass = "Platform\SecurityBundle\Doctrine\Identity\AccountRepository")
* @ORM\Table(
* name = "cms__security__identity__account",
* uniqueConstraints = {
* @ORM\UniqueConstraint(
* name = "uidx__only_one_email_per_tenant",
* columns = {
* "tenant",
* "email"
* }
* )
* }
* )
*/
class Account extends TenantedEntity implements
Serializable,
UserInterface,
EquatableInterface,
OneRosterableInterface,
ImportableInterface,
UserEntityInterface,
PasswordAuthenticatedUserInterface
{
use OneRosterableTrait;
use ImportableTrait;
/**
* The email address of the user.
* Any user in the system requires an email address, and they must be unique among the tenant.
* This also acts as a username in the system.
*
* @var string|null
*
* @ORM\Column(type = "string", nullable = false)
*/
protected ?string $email;
/**
* Determines whether the user is able to log in.
* This is distinct from the verification stuff.
*
* @var bool
*
* @ORM\Column(type = "boolean", nullable = false)
*/
protected bool $active = true;
/**
* Any kind of special permissions as designated by our system.
*
* @var SpecialPermissions
*
* @ORM\Embedded(
* class = "Platform\SecurityBundle\Entity\Access\SpecialPermissions",
* columnPrefix = "specialPermissions_"
* )
*/
protected SpecialPermissions $specialPermissions;
/**
* @var SystemProfile
*
* @ORM\Embedded(
* class = "Platform\SecurityBundle\Entity\Profiles\SystemProfile",
* columnPrefix = "systemProfile_"
* )
*/
protected SystemProfile $systemProfile;
/**
* @var Collection|AccountRoleAssociation[]
*
* @ORM\OneToMany(
* targetEntity = "Platform\SecurityBundle\Entity\Access\RoleAssociation\AccountRoleAssociation",
* mappedBy = "account"
* )
*/
protected Collection $accountRoles;
/**
* @var DateTimeInterface|null
*
* @ORM\Column(
* name = "policyAcceptedOn",
* type = "datetime",
* nullable = true,
* )
*/
protected ?DateTimeInterface $policyAcceptedOn = null;
/**
* @var DateTimeInterface|null
*
* @ORM\Column(
* type = "datetime",
* nullable = true,
* )
*/
protected ?DateTimeInterface $termsAcceptedOn = null;
/**
* @var Collection
*
* @ORM\ManyToMany(
* targetEntity = "Cms\ContainerBundle\Entity\Container"
* )
* @ORM\JoinTable(
* name = "cms__container__favorite_container",
* joinColumns = {
* @ORM\JoinColumn(
* name = "account",
* referencedColumnName = "id",
* onDelete = "CASCADE"
* )
* },
* inverseJoinColumns = {
* @ORM\JoinColumn(
* name = "container",
* referencedColumnName = "id",
* onDelete = "CASCADE"
* )
* }
* )
*/
protected Collection $favorites;
/**
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = true
* )
*/
protected ?string $password;
/**
* @var DateTime|null
*
* @ORM\Column(
* type = "datetime",
* nullable = true
* )
*/
protected ?DateTime $passwordChangedAt;
/**
* @var DateTime|null
*
* @ORM\Column(
* type = "datetime",
* nullable = true
* )
*/
protected ?DateTime $lastLoggedInAt;
/**
* @var array|null
*
* @ORM\Column(
* type = "json",
* nullable = true,
* )
*/
protected ?array $metadata = [];
/**
* @var Collection|AbstractAdminToken[]
*
* @ORM\OneToMany(
* targetEntity = AbstractAdminToken::class,
* mappedBy = "account",
* )
* @ORM\OrderBy({
* "createdAt" = "DESC",
* })
*/
protected Collection $tokens;
/**
* Constructor
*/
public function __construct()
{
$this->specialPermissions = new SpecialPermissions();
$this->systemProfile = new SystemProfile();
$this->accountRoles = new ArrayCollection();
$this->favorites = new ArrayCollection();
$this->tokens = new ArrayCollection();
}
/**
* {@inheritdoc}
*/
public function serialize(): ?string
{
return serialize($this->getId());
}
/**
* {@inheritdoc}
*/
public function unserialize($data): void
{
$this->id = unserialize($data);
}
/**
* TODO: this really needs implemented as a permission check...
*
* Determines whether or not this user is a core company team member.
* Such members may have extended access.
*
* @return bool
*/
public function isInternal(): bool
{
$emails = array(
// TODO: this method needs to be like a service call so we can do more with it, like be able to use the param that sets the csadmin account email
'csadmin@innersync.com',
);
foreach ($emails as $email) {
if ($this->getEmail() === $email) {
return true;
}
}
return false;
}
/**
* TODO: this really needs implemented as a permission check...
*
* Determines whether or not this user is a core company contractor.
* Such members may have extended access.
*
* @return bool
*/
public function isContractor(): bool
{
if ($this->isInternal()) {
return true;
}
$emails = array(
'arif.ews@gmail.com',
);
foreach ($emails as $email) {
if ($this->getEmail() === $email) {
return true;
}
}
return false;
}
/**
* Implemented to satisfy Symfony security needs.
* In our system, the "username" for an account is the ID of the account.
* "Alternate" usernames could be UIDs or emails.
*
* {@inheritdoc}
*/
public function getUsername(): string
{
// TODO: remove the getUsername function once we are on Symfony 6
return $this->getUserIdentifier();
}
/**
* {@inheritdoc}
*/
public function getUserIdentifier(): string
{
return (string) $this->getId();
}
/**
* Implemented to satisfy Symfony security needs.
* Salts are not tracked by us (PHP is used for password checking).
*
* {@inheritdoc}
*/
public function getSalt(): ?string
{
// TODO: this function is deprecated
return null;
}
/**
* Implemented to satisfy Symfony security needs.
* Password checking is done with custom code.
*
* {@inheritdoc}
*/
public function getPassword(): ?string
{
// TODO: this function is deprecated
return $this->password;
}
/**
* @param string|null $value
* @param bool|DateTime|null $timestamp
* @return $this
*/
public function setPassword(?string $value, $timestamp = true): self
{
switch ($timestamp) {
case $timestamp === true:
$timestamp = new DateTime();
break;
case $timestamp instanceof DateTime:
break;
default:
$timestamp = null;
}
if ($timestamp instanceof DateTime) {
$this->setPasswordChangedAt($timestamp);
}
$this->password = $value;
return $this;
}
/**
* @param string $value
* @param bool|DateTime|null $timestamp
* @return $this
*/
public function setPasswordRaw(string $value, $timestamp = true): self
{
return $this->setPassword(Passwords::hash($value), $timestamp);
}
/**
* @return bool
*/
public function hasPassword(): bool
{
return ( ! empty($this->getPassword()));
}
/**
* @return DateTime|null
*/
public function getPasswordChangedAt(): ?DateTime
{
return $this->passwordChangedAt;
}
/**
* @param DateTime $value
* @return $this
*/
public function setPasswordChangedAt(DateTime $value): self
{
$this->passwordChangedAt = $value;
return $this;
}
/**
* @return DateTime|null
*/
public function getLastLoggedInAt(): ?DateTime
{
return $this->lastLoggedInAt;
}
/**
* @param DateTime $value
* @return $this
*/
public function setLastLoggedInAt(DateTime $value): self
{
$this->lastLoggedInAt = $value;
return $this;
}
/**
* Implemented to satisfy Symfony security needs.
* We have more advanced, custom roles that require special handling.
*
* @return array
*/
public function getRoles(): array
{
return [];
}
/**
* Implemented to satisfy Symfony security needs.
* As we do not store passwords or anything sensitive in this class,
* this does nothing and returns a successful response.
*
* {@inheritdoc}
*/
public function eraseCredentials(): bool
{
return true;
}
/**
* @return string|null
*/
public function getEmail(): ?string
{
return $this->email;
}
/**
* @return bool
*/
public function isActive(): bool
{
return $this->active;
}
/**
* @param string $value
* @return $this
*/
public function setEmail(string $value): self
{
$this->email = $value;
return $this;
}
/**
* @param bool $value
* @return $this
*/
public function setActive(bool $value): self
{
$this->active = ($value === true);
return $this;
}
/**
* Implemented to satisfy Symfony security needs.
* Ensures that our usernames are both not null AND they are equal.
*
* {@inheritdoc}
* @param Account $user
*/
public function isEqualTo(UserInterface $user): bool
{
return (
($this->getUserIdentifier() !== null && $user->getUserIdentifier() !== null)
&&
($this->getUserIdentifier() === $user->getUserIdentifier())
);
}
/**
* @param bool $default
* @return string|null
*/
public function getDisplayName(bool $default = true): ?string
{
$display = $this->getSystemProfile()->getDisplayName()
?: $this->getSystemProfile()->getFullName()
?: null;
if ( ! $display && $default) {
$display = $this->getEmail();
}
return $display ?: null;
}
/**
* @return SystemProfile
*/
public function getSystemProfile(): SystemProfile
{
return $this->systemProfile ?: new SystemProfile();
}
/**
* @return SpecialPermissions
*/
public function getSpecialPermissions(): SpecialPermissions
{
return $this->specialPermissions;
}
/**
* @param bool $value
* @return $this
*/
public function setSuperUserSpecialPermission(bool $value): self
{
$this->getSpecialPermissions()->setSuperUser($value);
return $this;
}
/**
* @return ArrayCollection|AccountRoleAssociation[]
*/
public function getAccountRoles(): Collection
{
return $this->accountRoles;
}
/**
* @return DateTimeInterface|null
*/
public function getPolicyAcceptedOn(): ?DateTimeInterface
{
return $this->policyAcceptedOn;
}
/**
* @param DateTimeInterface|null $policyAcceptedOn
* @return $this
*/
public function setPolicyAcceptedOn(?DateTimeInterface $policyAcceptedOn): self
{
$this->policyAcceptedOn = $policyAcceptedOn;
return $this;
}
/**
* @return DateTimeInterface|null
*/
public function getTermsAcceptedOn(): ?DateTimeInterface
{
return $this->termsAcceptedOn;
}
/**
* @param DateTimeInterface|null $termsAcceptedOn
* @return $this
*/
public function setTermsAcceptedOn(DateTimeInterface $termsAcceptedOn): self
{
$this->termsAcceptedOn = $termsAcceptedOn;
return $this;
}
/**
* @param Container $container
* @return $this
*/
public function addFavorite(Container $container): self
{
$this->favorites->add($container);
return $this;
}
/**
* @return Collection
*/
public function getFavorites(): Collection
{
return $this->favorites;
}
/**
* @param Container $container
* @return $this
*/
public function removeFavorite(Container $container): self
{
$this->favorites->removeElement($container);
return $this;
}
/**
* @return bool
*/
public function canManagePassword(): bool
{
//return ( ! empty($this->getPassword()) && ! $this->isOneRoster());
return ( ! empty($this->getPassword()));
}
/**
* @return array|null
*/
public function getMetadata(): ?array
{
return $this->metadata;
}
/**
* @param array|null $metadata
* @return Account
*/
public function setMetadata(?array $metadata): self
{
$this->metadata = $metadata ?: null;
return $this;
}
/**
* @param array $metadata
* @return $this
*/
public function mergeMetadata(array $metadata): self
{
$this->setMetadata(
array_merge(
$this->getMetadata(),
$metadata,
),
);
return $this;
}
/**
* @return array
*/
public function getMetadataSchools(): array
{
$metadata = $this->getMetadata();
return ($metadata && isset($metadata['_schools'])) ? $metadata['_schools'] : [];
}
/**
* {@inheritDoc}
*/
public function getIdentifier(): string
{
return $this->getUidString();
}
/**
* @param $criteria
* @return iterable
*/
public function getTokens($criteria = null): iterable
{
if ( ! $this->tokens instanceof Collection) {
$this->tokens = new ArrayCollection();
}
$criteria = ($criteria === true) ? new Criteria() : ($criteria ?: null);
if ( ! empty($criteria)) {
return $this->tokens->matching($criteria);
}
return $this->tokens;
}
/**
* @return array
*/
public function getInternalPermissions(): array
{
// TODO: if ever more internal permissions are made, this needs to report only the ones applicable to the account...
return $this->isInternal() ? Sentry::INTERNAL_PERMISSIONS : [];
}
}