From 1f4773d29264cde1939260babe2e9a0f8d1fbce4 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 9 Nov 2023 12:02:53 +0100 Subject: [PATCH 1/5] Some code cleanup for login, applications and session (/logging) stuff --- api/src/Controller/LoginController.php | 5 +- api/src/Controller/UserController.php | 31 +-- api/src/Repository/ObjectEntityRepository.php | 185 +++++++++--------- api/src/Security/ApiKeyAuthenticator.php | 23 +-- api/src/Security/OIDCAuthenticator.php | 26 +-- api/src/Security/TokenAuthenticator.php | 3 +- api/src/Service/ApplicationService.php | 79 ++++---- api/src/Service/AuthorizationService.php | 6 +- api/src/Service/EavService.php | 40 ++-- api/src/Service/ObjectEntityService.php | 14 +- 10 files changed, 181 insertions(+), 231 deletions(-) diff --git a/api/src/Controller/LoginController.php b/api/src/Controller/LoginController.php index 6df1818aa..0ca3a1caf 100644 --- a/api/src/Controller/LoginController.php +++ b/api/src/Controller/LoginController.php @@ -52,9 +52,8 @@ public function MeAction(Request $request, CommonGroundService $commonGroundServ 'last_name' => $this->getUser()->getLastName(), 'name' => $this->getUser()->getName(), 'email' => $this->getUser()->getEmail(), - // TODO: if we have no person connected to this user create one? with $this->createPersonForUser() - 'person' => $userService->getPersonForUser($this->getUser()), - 'organization' => $userService->getOrganizationForUser($this->getUser()), + 'person' => $userService->getPersonForUser($this->getUser()), // Get person ObjectEntity (->Entity with function = person) by id + 'organization' => $userService->getOrganizationForUser($this->getUser()), // Get organization ObjectEntity (->Entity with function = organization) by id ]; $result = json_encode($result); } else { diff --git a/api/src/Controller/UserController.php b/api/src/Controller/UserController.php index a4393d862..be91f2227 100644 --- a/api/src/Controller/UserController.php +++ b/api/src/Controller/UserController.php @@ -100,17 +100,8 @@ public function resetTokenAction(SerializerInterface $serializer, \CommonGateway $user = $this->entityManager->getRepository('App:User')->find($user->getUserIdentifier()); - if ($user->getOrganization() !== null) { - $organizations[] = $user->getOrganization(); - } - foreach ($user->getApplications() as $application) { - if ($application->getOrganization() !== null) { - $organizations[] = $application->getOrganization(); - } - } - - // If user has no organization, we default activeOrganization to an organization of a userGroup this user has and else the application organization; - $this->session->set('activeOrganization', $user->getOrganization()->getId()->toString()); + // Set organization in session + $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); $user->setJwtToken($authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session))); @@ -166,10 +157,11 @@ public function createAuthenticationUser(User $user): AuthenticationUser } /** - * Add the logged in user to session. + * Add the logged-in user to session. * - * @param User $user The user to log in. + * @param User $user The user to log in. * @param EventDispatcherInterface $eventDispatcher The event dispatcher. + * @param Request $request * * @return void */ @@ -203,17 +195,8 @@ public function apiLoginAction(Request $request, UserPasswordHasherInterface $ha return new Response(json_encode($response), 401, ['Content-type' => 'application/json']); } - if ($user->getOrganization() !== null) { - $organizations[] = $user->getOrganization(); - } - foreach ($user->getApplications() as $application) { - if ($application->getOrganization() !== null) { - $organizations[] = $application->getOrganization(); - } - } - - // If user has no organization, we default activeOrganization to an organization of a userGroup this user has and else the application organization; - $this->session->set('activeOrganization', $user->getOrganization()->getId()->toString()); + // Set organization in session + $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); $token = $authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session)); diff --git a/api/src/Repository/ObjectEntityRepository.php b/api/src/Repository/ObjectEntityRepository.php index 6823c370f..8da2f4a17 100644 --- a/api/src/Repository/ObjectEntityRepository.php +++ b/api/src/Repository/ObjectEntityRepository.php @@ -35,6 +35,96 @@ public function __construct(ManagerRegistry $registry, SessionInterface $session parent::__construct($registry, ObjectEntity::class); } + /** + * Gets and returns an array with the allowed filters on an Entity (including its subEntities / sub-filters). + * + * @param Entity $Entity The Entity we are currently doing a get collection on. + * @param string $prefix + * @param int $level + * @param bool $embedded + * + * @return array The array with allowed filters. + */ + public function getFilterParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array + { + $prefix = $embedded && $level === 2 ? "embedded.$prefix" : $prefix; + + //todo: we only check for the allowed keys/attributes to filter on, if this attribute is a dateTime (or date), we should also check if the value is a valid dateTime string? + // NOTE: + // Filter id looks for ObjectEntity id and externalId + // Filter _id looks specifically/only for ObjectEntity id + // Filter _externalId looks specifically/only for ObjectEntity externalId + + // defaults + $filters = [ + $prefix.'id', $prefix.'_id', $prefix.'_externalId', $prefix.'_uri', $prefix.'_self', $prefix.'_organization', + $prefix.'_application', $prefix.'_dateCreated', $prefix.'_dateModified', $prefix.'_mapping', + ]; + + foreach ($Entity->getAttributes() as $attribute) { + if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number', 'boolean']) && $attribute->getSearchable()) { + $filters[] = $prefix.$attribute->getName(); + } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { + $attribute->getSearchable() && $filters[] = $prefix.$attribute->getName(); + $embeddedString = $embedded && $level > 1 ? 'embedded.' : ''; + $filters = array_merge($filters, $this->getFilterParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1, $embedded)); + } + } + + return $filters; + } + + /** + * Gets and returns an array with the allowed sortable attributes on an Entity (including its subEntities). + * + * @param Entity $Entity The Entity we are currently doing a get collection on. + * @param string $prefix + * @param int $level + * @param bool $embedded + * + * @return array The array with allowed attributes to sort by. + */ + public function getOrderParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array + { + $prefix = $embedded && $level === 2 ? "embedded.$prefix" : $prefix; + // defaults + $sortable = [$prefix.'_dateCreated', $prefix.'_dateModified']; + + foreach ($Entity->getAttributes() as $attribute) { + if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number']) && $attribute->getSortable()) { + $sortable[] = $prefix.$attribute->getName(); + } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { + $embeddedString = $embedded && $level > 1 ? 'embedded.' : ''; + $sortable = array_merge($sortable, $this->getOrderParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1)); + } + } + + return $sortable; + } + + /** + * Finds object entities on their id or a sourceId of a synchronization this ObjectEntity has. + * + * @param string $identifier + * + * @throws NonUniqueResultException + * + * @return ObjectEntity The found object entity + */ + public function findByAnyId(string $identifier): ?ObjectEntity + { + $query = $this->createQueryBuilder('o') + ->leftJoin('o.synchronizations', 's') + ->where('s.sourceId = :identifier') + ->setParameter('identifier', $identifier); + + if (Uuid::isValid($identifier)) { + $query->orWhere('o.id = :identifier'); + } + + return $query->getQuery()->getOneOrNullResult(); + } + /** * Does the same as findByEntity(), but also returns an integer representing the total amount of results using the input to create a sql statement. $entity is required. * @@ -47,6 +137,7 @@ public function __construct(ManagerRegistry $registry, SessionInterface $session * @throws NoResultException|NonUniqueResultException * * @return array With a key 'objects' containing the actual objects found and a key 'total' with an integer representing the total amount of results found. + * @deprecated */ public function findAndCountByEntity(Entity $entity, array $filters = [], array $order = [], int $offset = 0, int $limit = 25): array { @@ -81,6 +172,7 @@ public function findAndCountByEntity(Entity $entity, array $filters = [], array * @throws Exception * * @return array Returns an array of ObjectEntity objects + * @deprecated */ public function findByEntity(Entity $entity, array $filters = [], array $order = [], int $offset = 0, int $limit = 25, QueryBuilder $query = null): array { @@ -103,6 +195,7 @@ public function findByEntity(Entity $entity, array $filters = [], array $order = * @throws NoResultException|NonUniqueResultException * * @return int Returns an integer, for the total ObjectEntities found with this Entity and with the given filters. + * @deprecated */ public function countByEntity(Entity $entity, array $filters = [], QueryBuilder $query = null): int { @@ -155,11 +248,11 @@ private function createQuery(Entity $entity, array $filters = [], array $order = // Multitenancy, only show objects this user is allowed to see. // Only show objects this user owns or object that have an organization this user is part of or that are inhereted down the line - $organizations = $this->session->get('organizations', []); + $organizations = []; $parentOrganizations = []; // Make sure we only check for parentOrganizations if inherited is true in the (ObjectEntity)->entity->inherited if ($entity->getInherited()) { - $parentOrganizations = $this->session->get('parentOrganizations', []); + $parentOrganizations = []; } // $query->andWhere('o.organization IN (:organizations) OR o.organization IN (:parentOrganizations) OR o.organization = :defaultOrganization OR o.owner = :userId') @@ -872,92 +965,4 @@ private function makeKeySqlFriendly(string $key): string // todo, probably add more special characters to replace... return str_replace('-', 'Dash', $key); } - - /** - * Gets and returns an array with the allowed filters on an Entity (including its subEntities / sub-filters). - * - * @param Entity $Entity The Entity we are currently doing a get collection on. - * @param string $prefix - * @param int $level - * - * @return array The array with allowed filters. - */ - public function getFilterParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array - { - $prefix = $embedded && $level === 2 ? "embedded.$prefix" : $prefix; - - //todo: we only check for the allowed keys/attributes to filter on, if this attribute is a dateTime (or date), we should also check if the value is a valid dateTime string? - // NOTE: - // Filter id looks for ObjectEntity id and externalId - // Filter _id looks specifically/only for ObjectEntity id - // Filter _externalId looks specifically/only for ObjectEntity externalId - - // defaults - $filters = [ - $prefix.'id', $prefix.'_id', $prefix.'_externalId', $prefix.'_uri', $prefix.'_self', $prefix.'_organization', - $prefix.'_application', $prefix.'_dateCreated', $prefix.'_dateModified', $prefix.'_mapping', - ]; - - foreach ($Entity->getAttributes() as $attribute) { - if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number', 'boolean']) && $attribute->getSearchable()) { - $filters[] = $prefix.$attribute->getName(); - } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { - $attribute->getSearchable() && $filters[] = $prefix.$attribute->getName(); - $embeddedString = $embedded && $level > 1 ? 'embedded.' : ''; - $filters = array_merge($filters, $this->getFilterParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1, $embedded)); - } - } - - return $filters; - } - - /** - * Gets and returns an array with the allowed sortable attributes on an Entity (including its subEntities). - * - * @param Entity $Entity The Entity we are currently doing a get collection on. - * @param string $prefix - * @param int $level - * - * @return array The array with allowed attributes to sort by. - */ - public function getOrderParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array - { - $prefix = $embedded && $level === 2 ? "embedded.$prefix" : $prefix; - // defaults - $sortable = [$prefix.'_dateCreated', $prefix.'_dateModified']; - - foreach ($Entity->getAttributes() as $attribute) { - if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number']) && $attribute->getSortable()) { - $sortable[] = $prefix.$attribute->getName(); - } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { - $embeddedString = $embedded && $level > 1 ? 'embedded.' : ''; - $sortable = array_merge($sortable, $this->getOrderParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1)); - } - } - - return $sortable; - } - - /** - * Finds object entities on their id or a sourceId of a synchronization this ObjectEntity has. - * - * @param string $identifier - * - * @throws NonUniqueResultException - * - * @return ObjectEntity The found object entity - */ - public function findByAnyId(string $identifier): ?ObjectEntity - { - $query = $this->createQueryBuilder('o') - ->leftJoin('o.synchronizations', 's') - ->where('s.sourceId = :identifier') - ->setParameter('identifier', $identifier); - - if (Uuid::isValid($identifier)) { - $query->orWhere('o.id = :identifier'); - } - - return $query->getQuery()->getOneOrNullResult(); - } } diff --git a/api/src/Security/ApiKeyAuthenticator.php b/api/src/Security/ApiKeyAuthenticator.php index d9c19f97e..4fbb33ea1 100644 --- a/api/src/Security/ApiKeyAuthenticator.php +++ b/api/src/Security/ApiKeyAuthenticator.php @@ -2,6 +2,7 @@ namespace App\Security; +use App\Entity\Application; use App\Entity\User; use App\Service\FunctionService; use Conduction\CommonGroundBundle\Service\AuthenticationService; @@ -146,20 +147,21 @@ public function authenticate(Request $request): PassportInterface { $key = $request->headers->get('Authorization'); $application = $this->entityManager->getRepository('App:Application')->findOneBy(['secret' => $key]); - if (!$application) { + if ($application === null) { throw new AuthenticationException('Invalid ApiKey'); } try { $user = $application->getOrganization()->getUsers()[0]; } catch (\Exception $exception) { - throw new AuthenticationException('Invalid User'); + throw new AuthenticationException('An invalid User is configured for this ApiKey'); } - $this->session->set('apiKeyApplication', $application->getId()->toString()); - if (!$user || !($user instanceof User)) { - throw new AuthenticationException('The provided token does not match the user it refers to'); + if ($user instanceof User === false) { + throw new AuthenticationException('An invalid User is configured for this ApiKey'); } + $this->session->set('apiKeyApplication', $application->getId()->toString()); + $roleArray = []; foreach ($user->getSecurityGroups() as $securityGroup) { $roleArray['roles'][] = "Role_{$securityGroup->getName()}"; @@ -175,15 +177,8 @@ public function authenticate(Request $request): PassportInterface } } - $organizations = []; - if ($user->getOrganization()) { - $organizations[] = $user->getOrganization(); - } - - $organizations[] = 'localhostOrganization'; - $this->session->set('organizations', $organizations); - // If user has no organization, we default activeOrganization to an organization of a userGroup this user has and else the application organization; - $this->session->set('activeOrganization', $user->getOrganization()); + // Set organization in session + $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); $userArray = [ 'id' => $user->getId()->toString(), diff --git a/api/src/Security/OIDCAuthenticator.php b/api/src/Security/OIDCAuthenticator.php index 8fbf5111d..15e9704be 100644 --- a/api/src/Security/OIDCAuthenticator.php +++ b/api/src/Security/OIDCAuthenticator.php @@ -113,13 +113,6 @@ public function authenticate(Request $request): PassportInterface } } - // Set default organization in session for multitenancy (see how this is done in other Authenticators, this can be different for each one!) - $defaultOrganization = $this->getDefaultOrganization(); - $organizations = [$defaultOrganization, 'localhostOrganization']; - $parentOrganizations[] = 'localhostOrganization'; - $this->session->set('organizations', $organizations); - $this->session->set('parentOrganizations', $parentOrganizations); - $this->session->set('activeOrganization', $defaultOrganization); // if (isset($accessToken['refresh_token'])) { // $this->session->set('refresh_token', $accessToken['refresh_token']); // $userIdentifier = $result['email']; @@ -133,6 +126,7 @@ public function authenticate(Request $request): PassportInterface $doctrineUser->setPassword(''); $doctrineUser->addApplication($this->applicationService->getApplication()); $doctrineUser->setOrganization($doctrineUser->getApplications()->first()->getOrganization()); + $this->session->set('organization', $doctrineUser->getApplications()->first()->getOrganization()); foreach ($result['groups'] as $group) { $securityGroup = $this->entityManager->getRepository('App:SecurityGroup')->findOneBy(['name' => $group]); @@ -178,24 +172,6 @@ function ($credentials, $user) { ); } - private function getDefaultOrganization(): string - { - // Find application->organization - if ($this->session->get('application')) { - $application = $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); - if (!empty($application) && $application->getOrganization()) { - return $application->getOrganization(); - } - } - // Else find and return 'the' default organization - $organization = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['id' => 'a1c8e0b6-2f78-480d-a9fb-9792142f4761']); - if (!empty($organization) && $organization->getOrganization()) { - return $organization->getOrganization(); - } - - return 'http://api/admin/organizations/a1c8e0b6-2f78-480d-a9fb-9792142f4761'; - } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return new RedirectResponse($this->session->get('backUrl', $this->parameterBag->get('defaultBackUrl')) ?? $request->headers->get('referer') ?? $request->getSchemeAndHttpHost()); diff --git a/api/src/Security/TokenAuthenticator.php b/api/src/Security/TokenAuthenticator.php index 10b2d63ac..f12845b68 100644 --- a/api/src/Security/TokenAuthenticator.php +++ b/api/src/Security/TokenAuthenticator.php @@ -99,6 +99,7 @@ public function getPublicKey(string $token): string * @param string $token The token provided by the user * * @return array The payload of the token + * @throws GatewayException */ public function validateToken(string $token): array { @@ -139,9 +140,7 @@ private function prefixRoles(array $roles): array * * @param Request $request * - * @throws CacheException * @throws GatewayException - * @throws InvalidArgumentException * * @return PassportInterface */ diff --git a/api/src/Service/ApplicationService.php b/api/src/Service/ApplicationService.php index 119049407..ce5db8001 100644 --- a/api/src/Service/ApplicationService.php +++ b/api/src/Service/ApplicationService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\Entity\Application; use App\Exception\GatewayException; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\EntityManagerInterface; @@ -29,67 +30,67 @@ public function __construct( } /** - * A function that finds an application or creates one. + * A function that finds an application. * * @throws GatewayException */ - public function getApplication() + public function getApplication(): Application { - if ($application = $this->session->get('application')) { + // If application is already in the session + if ($this->session->has('application')) { $application = $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); - if (!empty($application)) { + if ($application !== null) { return $application; } - } elseif ($this->session->get('apiKeyApplication')) { - // If an api-key is used for authentication we already know which application is used - return $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('apiKeyApplication')]); } - // get publickey + // If an api-key is used for authentication we already know which application is used + if ($this->session->has('apiKeyApplication')) { + $application = $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('apiKeyApplication')]); + if ($application !== null) { + $this->session->set('application', $application->getId()->toString()); + return $application; + } + } + + // Find application using the publicKey $public = ($this->request->headers->get('public') ?? $this->request->query->get('public')); + if (empty($public) === false) { + $application = $this->entityManager->getRepository('App:Application')->findOneBy(['public' => $public]); + if ($application !== null) { + $this->session->set('application', $application->getId()->toString()); + return $application; + } + } - // get host/domain + // Find application using the host/domain $host = ($this->request->headers->get('host') ?? $this->request->query->get('host')); -// $host = 'api.buren.commonground.nu'; - ($application = $this->entityManager->getRepository('App:Application')->findOneBy(['public' => $public])) && !empty($application) && $this->session->set('application', $application->getId()->toString()); - - if (!$application) { + if (empty($host) === false) { // @todo Create and use query in ApplicationRepository - $criteria = new Criteria(); - - // $application = $this->entityManager->getRepository('App:Application')->findAll()-> $applications = $this->entityManager->getRepository('App:Application')->findAll(); foreach ($applications as $app) { - $app->getDomains() !== null && in_array($host, $app->getDomains()) && $application = $app; - if (isset($application)) { - break; + if ($app->getDomains() !== null && in_array($host, $app->getDomains()) === true) { + $this->session->set('application', $app->getId()->toString()); + return $app; } } -// if(count($applications) > 0) { -// $application = $applications[0]; -// } } - if (!$application) { - $this->session->set('application', null); + // No application was found + $this->session->set('application', null); - // Set message - $public && $message = 'No application found with public '.$public; - $host && $message = 'No application found with host '.$host; - !$public && !$host && $message = 'No host or application given'; - - // Set data - $public && $data = ['public' => $public]; - $host && $data = ['host' => $host]; - - throw new GatewayException($message ?? null, null, null, [ - 'data' => $data ?? null, 'path' => $public ?? $host ?? 'Header', 'responseType' => Response::HTTP_FORBIDDEN, - ]); - } + // Set message + $public && $message = 'No application found with public '.$public; + $host && $message = 'No application found with host '.$host; + !$public && !$host && $message = 'No host or application given'; - $this->session->set('application', $application->getId()->toString()); + // Set data + $public && $data = ['public' => $public]; + $host && $data = ['host' => $host]; - return $application; + throw new GatewayException($message ?? null, null, null, [ + 'data' => $data ?? null, 'path' => $public ?? $host ?? 'Header', 'responseType' => Response::HTTP_FORBIDDEN, + ]); } } diff --git a/api/src/Service/AuthorizationService.php b/api/src/Service/AuthorizationService.php index 8f9bd00b0..13add636a 100644 --- a/api/src/Service/AuthorizationService.php +++ b/api/src/Service/AuthorizationService.php @@ -231,8 +231,7 @@ public function getScopesForAnonymous(): array $item = $this->cache->getItem('anonymousScopes'); $itemOrg = $this->cache->getItem('anonymousOrg'); if ($item->isHit() && $itemOrg->isHit()) { - $this->session->set('organizations', [$itemOrg->get()]); - $this->session->set('activeOrganization', $itemOrg->get()); + $this->session->set('organization', $itemOrg->get()); return $item->get(); } @@ -244,8 +243,7 @@ public function getScopesForAnonymous(): array foreach ($groups[0]['scopes'] as $scope) { $scopes[] = strtolower($scope['code']); } - $this->session->set('organizations', [$groups[0]['organization']]); - $this->session->set('activeOrganization', $groups[0]['organization']); + $this->session->set('organization', $groups[0]['organization']); $itemOrg->set($groups[0]['organization']); $itemOrg->tag('anonymousOrg'); $this->cache->save($itemOrg); diff --git a/api/src/Service/EavService.php b/api/src/Service/EavService.php index 29a8b63db..c5e68a4a1 100644 --- a/api/src/Service/EavService.php +++ b/api/src/Service/EavService.php @@ -175,7 +175,7 @@ public function getObject(?string $id, string $method, Entity $entity) $object = new ObjectEntity(); $object->setEntity($entity); // if entity->function == 'organization', organization for this ObjectEntity will be changed later in handleMutation - $this->session->get('activeOrganization') ? $object->setOrganization($this->session->get('activeOrganization')) : $object->setOrganization('http://testdata-organization'); + $this->session->get('organization') ? $object->setOrganization($this->session->get('organization')) : $object->setOrganization('http://testdata-organization'); $application = $this->em->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); $object->setApplication(!empty($application) ? $application : null); @@ -401,15 +401,9 @@ public function generateResult(Request $request, Entity $entity, array $requestB } } - if (!$this->session->get('activeOrganization') && $this->session->get('application')) { + if (!$this->session->get('organization') && $this->session->get('application')) { $application = $this->em->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); - $this->session->set('activeOrganization', !empty($application) ? $application->getOrganization() : null); - } - if (!$this->session->get('organizations') && $this->session->get('activeOrganization')) { - $this->session->set('organizations', [$this->session->get('activeOrganization')]); - } - if (!$this->session->get('parentOrganizations')) { - $this->session->set('parentOrganizations', []); + $this->session->set('organization', !empty($application) ? $application->getOrganization() : null); } // Lets create an object @@ -420,19 +414,19 @@ public function generateResult(Request $request, Entity $entity, array $requestB $result = $object; $object = null; } // Lets check if the user is allowed to view/edit this resource. - elseif (!$this->objectEntityService->checkOwner($object)) { - // TODO: do we want to throw a different error if there are nog organizations in the session? (because of logging out for example) - if ($object->getOrganization() && !in_array($object->getOrganization(), $this->session->get('organizations') ?? [])) { - $object = null; // Needed so we return the error and not the object! - $responseType = Response::HTTP_FORBIDDEN; - $result = [ - 'message' => 'You are forbidden to view or edit this resource.', - 'type' => 'Forbidden', - 'path' => $entity->getName(), - 'data' => ['id' => $requestBase['id']], - ]; - } - } +// elseif (!$this->objectEntityService->checkOwner($object)) { +// // TODO: do we want to throw a different error if there are nog organizations in the session? (because of logging out for example) +// if ($object->getOrganization() && !in_array($object->getOrganization(), [])) { +// $object = null; // Needed so we return the error and not the object! +// $responseType = Response::HTTP_FORBIDDEN; +// $result = [ +// 'message' => 'You are forbidden to view or edit this resource.', +// 'type' => 'Forbidden', +// 'path' => $entity->getName(), +// 'data' => ['id' => $requestBase['id']], +// ]; +// } +// } } // Check for scopes, if forbidden to view/edit overwrite result so far to this forbidden error @@ -836,7 +830,7 @@ public function handleCollectionEndpoint(Request $request, array $info): array public function handleMutation(ObjectEntity $object, array $body, $fields, Request $request): array { // Check if session contains an activeOrganization, so we can't do calls without it. So we do not create objects with no organization! - if ($this->parameterBag->get('app_auth') && empty($this->session->get('activeOrganization'))) { + if ($this->parameterBag->get('app_auth') && empty($this->session->get('organization'))) { return [ 'message' => 'An active organization is required in the session, please login to create a new session.', 'type' => 'Forbidden', diff --git a/api/src/Service/ObjectEntityService.php b/api/src/Service/ObjectEntityService.php index 4b4be82b8..bc35f4468 100644 --- a/api/src/Service/ObjectEntityService.php +++ b/api/src/Service/ObjectEntityService.php @@ -461,12 +461,12 @@ public function checkGetObject(?string $id, string $method, Entity $entity) throw new GatewayException($object['message'], null, null, ['data' => $object['data'], 'path' => $object['path'], 'responseType' => Response::HTTP_BAD_REQUEST]); } // Let's check if the user is allowed to view/edit this resource. - if (!$method == 'POST' && !$this->checkOwner($object)) { - // TODO: do we want to throw a different error if there are no organizations in the session? (because of logging out for example) - if ($object->getOrganization() && !in_array($object->getOrganization(), $this->session->get('organizations') ?? [])) { - throw new GatewayException('You are forbidden to view or edit this resource.', null, null, ['data' => ['id' => $id ?? null], 'path' => $entity->getName(), 'responseType' => Response::HTTP_FORBIDDEN]); - } - } +// if (!$method == 'POST' && !$this->checkOwner($object)) { +// // TODO: do we want to throw a different error if there are no organizations in the session? (because of logging out for example) +// if ($object->getOrganization() && !in_array($object->getOrganization(), [])) { +// throw new GatewayException('You are forbidden to view or edit this resource.', null, null, ['data' => ['id' => $id ?? null], 'path' => $entity->getName(), 'responseType' => Response::HTTP_FORBIDDEN]); +// } +// } if ($object instanceof ObjectEntity && $object->getId() !== null) { $this->session->set('object', $object->getId()->toString()); @@ -1045,7 +1045,7 @@ private function saveSubObject(ObjectEntity $subObject, $object): ObjectEntity $subObject->setOrganization($subObject->getSubresourceOf()->first()->getObjectEntity()->getOrganization()); $subObject->setApplication($subObject->getSubresourceOf()->first()->getObjectEntity()->getApplication()); } else { - $subObject->setOrganization($this->session->get('activeOrganization')); + $subObject->setOrganization($this->session->get('organization')); $application = $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); $subObject->setApplication(!empty($application) ? $application : null); } From a0e8f166efd5585b98456bb0866da2c836569e50 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 9 Nov 2023 13:01:51 +0100 Subject: [PATCH 2/5] Small changes for storing user id & org id in session (better logs) --- api/src/Controller/UserController.php | 6 ++++-- api/src/Logger/SessionDataProcessor.php | 3 ++- api/src/Security/ApiKeyAuthenticator.php | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/api/src/Controller/UserController.php b/api/src/Controller/UserController.php index be91f2227..3910fb7c5 100644 --- a/api/src/Controller/UserController.php +++ b/api/src/Controller/UserController.php @@ -100,7 +100,8 @@ public function resetTokenAction(SerializerInterface $serializer, \CommonGateway $user = $this->entityManager->getRepository('App:User')->find($user->getUserIdentifier()); - // Set organization in session + // Set organization id and user id in session + $this->session->set('user', $user->getId()->toString()); $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); $user->setJwtToken($authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session))); @@ -195,7 +196,8 @@ public function apiLoginAction(Request $request, UserPasswordHasherInterface $ha return new Response(json_encode($response), 401, ['Content-type' => 'application/json']); } - // Set organization in session + // Set organization id and user id in session + $this->session->set('user', $user->getId()->toString()); $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); $token = $authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session)); diff --git a/api/src/Logger/SessionDataProcessor.php b/api/src/Logger/SessionDataProcessor.php index ce568dbca..1e0152568 100644 --- a/api/src/Logger/SessionDataProcessor.php +++ b/api/src/Logger/SessionDataProcessor.php @@ -27,6 +27,7 @@ class SessionDataProcessor * @param SessionInterface $session * @param RequestStack $requestStack * @param EventDispatcherInterface $eventDispatcher + * @param EntityManagerInterface $entityManager */ public function __construct( SessionInterface $session, @@ -48,7 +49,7 @@ public function __construct( * * @return array The updated context. */ - public function updateContext($context): array + public function updateContext(array $context): array { $context['session'] = $this->session->getId(); $context['process'] = $this->session->has('process') ? $this->session->get('process') : ''; diff --git a/api/src/Security/ApiKeyAuthenticator.php b/api/src/Security/ApiKeyAuthenticator.php index 4fbb33ea1..c7af2224d 100644 --- a/api/src/Security/ApiKeyAuthenticator.php +++ b/api/src/Security/ApiKeyAuthenticator.php @@ -160,8 +160,14 @@ public function authenticate(Request $request): PassportInterface if ($user instanceof User === false) { throw new AuthenticationException('An invalid User is configured for this ApiKey'); } + + // Set apiKey Application id in session $this->session->set('apiKeyApplication', $application->getId()->toString()); + // Set organization id and user id in session + $this->session->set('user', $user->getId()->toString()); + $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); + $roleArray = []; foreach ($user->getSecurityGroups() as $securityGroup) { $roleArray['roles'][] = "Role_{$securityGroup->getName()}"; @@ -177,9 +183,6 @@ public function authenticate(Request $request): PassportInterface } } - // Set organization in session - $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); - $userArray = [ 'id' => $user->getId()->toString(), 'email' => $user->getEmail(), From eee5ed817ae0698fe3b409c2656dca240cadb737 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 9 Nov 2023 16:07:59 +0100 Subject: [PATCH 3/5] Improve login error responses & added dql finding applications by domain --- api/src/Controller/UserController.php | 42 ++++++++++++++++++-- api/src/Repository/ApplicationRepository.php | 17 ++++---- api/src/Security/OIDCAuthenticator.php | 7 ++++ api/src/Service/ApplicationService.php | 11 ++--- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/api/src/Controller/UserController.php b/api/src/Controller/UserController.php index 3910fb7c5..79e5fafd3 100644 --- a/api/src/Controller/UserController.php +++ b/api/src/Controller/UserController.php @@ -64,7 +64,7 @@ public function resetTokenAction(SerializerInterface $serializer, \CommonGateway $accessToken = $this->authenticationService->refreshAccessToken($session->get('refresh_token'), $session->get('authenticator')); $user = $this->getUser(); if ($user instanceof AuthenticationUser === false) { - return new Response('User not found', 401); + return new Response(json_encode(["Message" => 'User not found.']), 401, ['Content-type' => 'application/json']); } $serializeUser = new User(); @@ -95,7 +95,7 @@ public function resetTokenAction(SerializerInterface $serializer, \CommonGateway $status = 200; $user = $this->getUser(); if ($user instanceof AuthenticationUser === false) { - return new Response('User not found', 401); + return new Response(json_encode(["Message" => 'User not found.']), 401, ['Content-type' => 'application/json']); } $user = $this->entityManager->getRepository('App:User')->find($user->getUserIdentifier()); @@ -104,13 +104,17 @@ public function resetTokenAction(SerializerInterface $serializer, \CommonGateway $this->session->set('user', $user->getId()->toString()); $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); + $response = $this->validateUserApp($user); + if ($response !== null) + return $response; + $user->setJwtToken($authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session))); return new Response($serializer->serialize($user, 'json'), $status, ['Content-type' => 'application/json']); } /** - * Create an authentication user from a entity user. + * Create an authentication user from an entity user. * * @param User $user The user to log in. * @@ -200,6 +204,10 @@ public function apiLoginAction(Request $request, UserPasswordHasherInterface $ha $this->session->set('user', $user->getId()->toString()); $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); + $response = $this->validateUserApp($user); + if ($response !== null) + return $response; + $token = $authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session)); $user->setJwtToken($token); @@ -223,6 +231,34 @@ public function apiLoginAction(Request $request, UserPasswordHasherInterface $ha return new Response(json_encode($userArray), $status, ['Content-type' => 'application/json']); } + /** + * Checks if $user has an application and if that application has a PrivateKey set. If not return error Response. + * + * @param User $user A user to check. + * + * @return Response|null Error Response or null. + */ + private function validateUserApp(User $user): ?Response + { + if (empty($user->getApplications()) === true) { + return new Response( + json_encode(["Message" => 'This user is not yet connected to any application.']), + 409, + ['Content-type' => 'application/json'] + ); + } + + if (empty($user->getApplications()[0]->getPrivateKey()) === true) { + return new Response( + json_encode(["Message" => "Can't create a token because application ({$user->getApplications()[0]->getId()->toString()}) doesn't have a PrivateKey."]), + 409, + ['Content-type' => 'application/json'] + ); + } + + return null; + } + /** * Removes some sensitive data from the login response. * diff --git a/api/src/Repository/ApplicationRepository.php b/api/src/Repository/ApplicationRepository.php index 15765f22b..53752d1c2 100644 --- a/api/src/Repository/ApplicationRepository.php +++ b/api/src/Repository/ApplicationRepository.php @@ -21,24 +21,21 @@ public function __construct(ManagerRegistry $registry) } /** - * @param string $domain + * Find all applications that have the given $domain in there list of domains. * - * @throws NonUniqueResultException + * @param string $domain A domain to search with. * - * @return Application|null + * @return array|null */ - public function findByDomain(string $domain): ?Application + public function findByDomain(string $domain): ?array { - // TODO: something like this $query = $this->createQueryBuilder('a') - ->andWhere(':domain IN (a.domains)') - ->setParameters(['domain' => $domain]); - -// var_dump($query->getDQL()); + ->andWhere('a.domains LIKE :domain') + ->setParameters(['domain' => "%$domain%"]); return $query ->getQuery() - ->getOneOrNullResult(); + ->getResult(); } // /** diff --git a/api/src/Security/OIDCAuthenticator.php b/api/src/Security/OIDCAuthenticator.php index 15e9704be..4d4118028 100644 --- a/api/src/Security/OIDCAuthenticator.php +++ b/api/src/Security/OIDCAuthenticator.php @@ -4,6 +4,7 @@ use App\Entity\SecurityGroup; use App\Entity\User; +use App\Exception\GatewayException; use App\Security\User\AuthenticationUser; use App\Service\ApplicationService; use App\Service\AuthenticationService; @@ -140,6 +141,12 @@ public function authenticate(Request $request): PassportInterface $userIdentifier = $doctrineUser->getId()->toString(); + if (empty($doctrineUser->getApplications()[0]->getPrivateKey()) === true) { + throw new GatewayException("Can't create a token because application doesn't have a PrivateKey." ?? null, 409, null, [ + 'data' => ['application_id' => $doctrineUser->getApplications()[0]->getId()->toString()], 'path' => '', 'responseType' => Response::HTTP_CONFLICT, + ]); + } + $token = $this->coreAuthenticationService->createJwtToken($doctrineUser->getApplications()[0]->getPrivateKey(), $this->coreAuthenticationService->serializeUser($doctrineUser, $this->session)); $doctrineUser->setJwtToken($token); diff --git a/api/src/Service/ApplicationService.php b/api/src/Service/ApplicationService.php index ce5db8001..6536ff90c 100644 --- a/api/src/Service/ApplicationService.php +++ b/api/src/Service/ApplicationService.php @@ -66,14 +66,11 @@ public function getApplication(): Application // Find application using the host/domain $host = ($this->request->headers->get('host') ?? $this->request->query->get('host')); if (empty($host) === false) { - // @todo Create and use query in ApplicationRepository + $applications = $this->entityManager->getRepository('App:Application')->findByDomain($host); + if (count($applications) > 0) { + $this->session->set('application', $applications[0]->getId()->toString()); - $applications = $this->entityManager->getRepository('App:Application')->findAll(); - foreach ($applications as $app) { - if ($app->getDomains() !== null && in_array($host, $app->getDomains()) === true) { - $this->session->set('application', $app->getId()->toString()); - return $app; - } + return $applications[0]; } } From 8e9b1292586a241764102dfcde2dcbfb301325cb Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 9 Nov 2023 16:36:10 +0100 Subject: [PATCH 4/5] Added note for later, only allow user login on app/domain they may use --- api/src/Controller/UserController.php | 4 ++++ api/src/Security/OIDCAuthenticator.php | 2 ++ 2 files changed, 6 insertions(+) diff --git a/api/src/Controller/UserController.php b/api/src/Controller/UserController.php index 79e5fafd3..4cc28408a 100644 --- a/api/src/Controller/UserController.php +++ b/api/src/Controller/UserController.php @@ -108,6 +108,8 @@ public function resetTokenAction(SerializerInterface $serializer, \CommonGateway if ($response !== null) return $response; + // TODO: maybe do not just get the first Application here, but get application using ApplicationService->getApplication() and ... + // todo... if this returns an application check if the user is part of this application or one of the organizations of this application? $user->setJwtToken($authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session))); return new Response($serializer->serialize($user, 'json'), $status, ['Content-type' => 'application/json']); @@ -208,6 +210,8 @@ public function apiLoginAction(Request $request, UserPasswordHasherInterface $ha if ($response !== null) return $response; + // TODO: maybe do not just get the first Application here, but get application using ApplicationService->getApplication() and ... + // todo... if this returns an application check if the user is part of this application or one of the organizations of this application? $token = $authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session)); $user->setJwtToken($token); diff --git a/api/src/Security/OIDCAuthenticator.php b/api/src/Security/OIDCAuthenticator.php index 4d4118028..c7a000716 100644 --- a/api/src/Security/OIDCAuthenticator.php +++ b/api/src/Security/OIDCAuthenticator.php @@ -147,6 +147,8 @@ public function authenticate(Request $request): PassportInterface ]); } + // TODO: maybe do not just get the first Application here, but get application using ApplicationService->getApplication() and ... + // todo... if this returns an application check if the user is part of this application or one of the organizations of this application? $token = $this->coreAuthenticationService->createJwtToken($doctrineUser->getApplications()[0]->getPrivateKey(), $this->coreAuthenticationService->serializeUser($doctrineUser, $this->session)); $doctrineUser->setJwtToken($token); From b84ab13c71b505c7703da27257b93f395fddeeca Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Fri, 10 Nov 2023 08:57:50 +0100 Subject: [PATCH 5/5] remove one line if statements --- api/src/Repository/ObjectEntityRepository.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/api/src/Repository/ObjectEntityRepository.php b/api/src/Repository/ObjectEntityRepository.php index 8da2f4a17..9f3b68051 100644 --- a/api/src/Repository/ObjectEntityRepository.php +++ b/api/src/Repository/ObjectEntityRepository.php @@ -66,7 +66,10 @@ public function getFilterParameters(Entity $Entity, string $prefix = '', int $le $filters[] = $prefix.$attribute->getName(); } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { $attribute->getSearchable() && $filters[] = $prefix.$attribute->getName(); - $embeddedString = $embedded && $level > 1 ? 'embedded.' : ''; + $embeddedString = ''; + if ($embedded && $level > 1) { + $embeddedString = 'embedded.'; + } $filters = array_merge($filters, $this->getFilterParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1, $embedded)); } } @@ -86,7 +89,10 @@ public function getFilterParameters(Entity $Entity, string $prefix = '', int $le */ public function getOrderParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array { - $prefix = $embedded && $level === 2 ? "embedded.$prefix" : $prefix; + $prefix = $prefix; + if ($embedded && $level === 2) { + $prefix = "embedded.$prefix"; + } // defaults $sortable = [$prefix.'_dateCreated', $prefix.'_dateModified']; @@ -94,7 +100,10 @@ public function getOrderParameters(Entity $Entity, string $prefix = '', int $lev if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number']) && $attribute->getSortable()) { $sortable[] = $prefix.$attribute->getName(); } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { - $embeddedString = $embedded && $level > 1 ? 'embedded.' : ''; + $embeddedString = ''; + if ($embedded && $level > 1) { + $embeddedString = 'embedded.'; + } $sortable = array_merge($sortable, $this->getOrderParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1)); } }