/** * Checks if current user has access to the current rest request. * * @param RequestBag $requestBag * * @return bool * @throws \Webiny\Component\Rest\RestException */ public static function hasAccess(RequestBag $requestBag) { // first we check if method requires a special access level if (!$requestBag->getApiConfig()->get('Security', false)) { return true; // no special access level required } // get the required role if (isset($requestBag->getMethodData()['role'])) { $role = $requestBag->getMethodData()['role']; } else { $role = $requestBag->getApiConfig()->get('Security.Role', 'ROLE_ANONYMOUS'); } // check if user has the access level required if ($requestBag->getClassData()['accessInterface']) { return $requestBag->getClassInstance()->hasAccess($role); } else { // get firewall name $firewallName = $requestBag->getApiConfig()->get('Security.Firewall', false); if (!$firewallName) { throw new RestException('When using Rest access rule, you must specify a Firewall in your configuration. Alternatively you can implement the AccessInterface and do the check on your own.'); } return self::security()->firewall($firewallName)->getUser()->hasRole($role); } }
/** * Checks if user is within rate limits. * * @param RequestBag $requestBag * @param CallbackResult $cr * * @return bool * @throws \Webiny\Component\Rest\RestException */ public static function isWithinRateLimits(RequestBag $requestBag, CallbackResult $cr) { // do we have rate control in place? if (!($rateControl = $requestBag->getApiConfig()->get('RateControl', false))) { return true; // if rate control is not set, user is within his limits } // check if we should ignore rate control for this particular method if (isset($requestBag->getMethodData()['rateControl']['ignore'])) { return true; } // verify that we have a Cache service set if (!($cache = $requestBag->getApiConfig()->get('Cache', false))) { throw new RestException('Rest Rate Control requires that you have a Cache service defined under the Rest configuration.'); } // set the limit in response header $cr->attachDebugHeader('RateControl-Limit', $rateControl->Limit, true); // get current usage $cacheKey = md5('Webiny.Rest.RateLimit.' . self::httpRequest()->getClientIp()); $cacheData = self::cache($cache)->read($cacheKey); if (!$cacheData) { $cacheData = ['usage' => 0, 'penalty' => 0, 'ttl' => time() + 60 * $rateControl->Interval]; } else { $cacheData = self::unserialize($cacheData); // validate the ttl if (time() > $cacheData['ttl']) { $cacheData = ['usage' => 0, 'penalty' => 0, 'ttl' => time() + 60 * $rateControl->Interval]; } } // check if user is already in penalty if ($cacheData['penalty'] > time()) { // when in penalty the reset value, equals the penalty value $cr->attachDebugHeader('RateControl-Reset', $cacheData['penalty'] - time(), true); // and the remaining equals 0 $cr->attachDebugHeader('RateControl-Remaining', 0, true); return false; } // check if rate is reached if ($cacheData['usage'] >= $rateControl->Limit) { // set penalty for reaching the limit $cr->attachDebugHeader('RateControl-Reset', $rateControl->Penalty * 60 + time(), true); // and the remaining 0 $cr->attachDebugHeader('RateControl-Remaining', 0, true); return false; } // if limit not reached, increment the usage and save the data $cacheData['usage']++; $cr->attachDebugHeader('RateControl-Remaining', $rateControl->Limit - $cacheData['usage'], true); $cr->attachDebugHeader('RateControl-Reset', $cacheData['ttl'], true); $cacheTtl = $rateControl->Interval > $rateControl->Penalty ? $rateControl->Interval : $rateControl->Penalty; self::cache($cache)->save($cacheKey, self::serialize($cacheData), $cacheTtl * 60); return true; }
/** * Saves the result into cache. * * @param mixed $result Result that should be saved. * * @return bool * @throws \Webiny\Component\Rest\RestException */ public function saveCallbackResult($result) { // get cache key $cacheKey = $this->getCacheKey(); // cache the result try { $cache = $this->cache($this->requestBag->getApiConfig()->get('Cache')); return $cache->save($cacheKey, $result, $this->requestBag->getMethodData()['cache']['ttl']); } catch (\Exception $e) { throw new RestException('Unable to save the result into cache. ' . $e->getMessage()); } }
public function testSetGetMethodData() { $rb = new RequestBag(); $rb->setMethodData(['data' => 'foo']); $this->assertSame(['data' => 'foo'], $rb->getMethodData()); }
/** * Processes the callback and returns an instance of CallbackResult. * * @return CallbackResult * @throws \Webiny\Component\Rest\RestException */ public function getCallbackResult() { $class = $this->requestBag->getClassData()['class']; $this->requestBag->setClassInstance(new $class()); // create CallbackResult instance $cr = new CallbackResult(); $env = 'production'; if ($this->requestBag->getApiConfig()->get('Environment', 'production') == 'development') { $cr->setEnvToDevelopment(); $env = 'development'; } // attach some metadata $cr->attachDebugHeader('Class', $class); $cr->attachDebugHeader('ClassVersion', $this->requestBag->getClassData()['version']); $cr->attachDebugHeader('Method', strtoupper($this->httpRequest()->getRequestMethod())); if (!$this->requestBag->getMethodData()) { // if no method matched the request $cr->setHeaderResponse(404); $cr->setErrorResponse('No service matched the request.'); return $cr; } // check rate limit try { $rateControl = RateControl::isWithinRateLimits($this->requestBag, $cr); if (!$rateControl) { $cr->setHeaderResponse(429); $cr->setErrorResponse('Rate control limit reached.'); return $cr; } } catch (\Exception $e) { throw new RestException('Rate control verification failed. ' . $e->getMessage()); } // verify access role try { $hasAccess = Security::hasAccess($this->requestBag); if (!$hasAccess) { $cr->setHeaderResponse(403); $cr->setErrorResponse('You don\'t have the required access level.'); $cr->attachDebugHeader('RequestedRole', $this->requestBag->getMethodData()['role']); return $cr; } } catch (\Exception $e) { throw new RestException('Access role verification failed. ' . $e->getMessage()); } // verify cache try { $cachedResult = Cache::getFromCache($this->requestBag); } catch (\Exception $e) { throw new RestException('Reading result from cache failed. ' . $e->getMessage()); } // finalize output if ($cachedResult) { $cr->setData($cachedResult); $cr->attachDebugHeader('Cache', 'HIT'); } else { try { $result = call_user_func_array([$this->requestBag->getClassInstance(), $this->requestBag->getMethodData()['method']], $this->requestBag->getMethodParameters()); // check if method has custom headers set $cr->setHeaderResponse($this->requestBag->getMethodData()['header']['status']['success']); $cr->attachDebugHeader('Cache', 'MISS'); // add result to output $cr->setData($result); // check if we need to attach the cache headers if ($this->requestBag->getMethodData()['header']['cache']['expires'] > 0) { $cr->setExpiresIn($this->requestBag->getMethodData()['header']['cache']['expires']); } // cache the result Cache::saveResult($this->requestBag, $result); } catch (RestErrorException $re) { // check if method has custom headers set $cr->setHeaderResponse($statusCode = $re->getResponseCode(), $this->requestBag->getMethodData()['header']['status']['errorMessage']); // check if a custom http response code is set $cr->setErrorResponse($re->getErrorMessage(), $re->getErrorDescription(), $re->getErrorCode()); $errors = $re->getErrors(); foreach ($errors as $e) { $cr->addErrorMessage($e); } if ($env == 'development') { $cr->addDebugMessage(['file' => $re->getFile(), 'line' => $re->getLine(), 'traces' => explode('#', $re->getTraceAsString())]); } } catch (\Exception $e) { // check if method has custom headers set $cr->setHeaderResponse($this->requestBag->getMethodData()['header']['status']['error'], $this->requestBag->getMethodData()['header']['status']['errorMessage']); $cr->setErrorResponse('There has been an error processing the request.'); if ($env == 'development') { $cr->addErrorMessage(['message' => $e->getMessage()]); $cr->addDebugMessage(['file' => $e->getFile(), 'line' => $e->getLine(), 'traces' => explode('#', $e->getTraceAsString())]); } } } return $cr; }