ldc-crm/app/Models/Service.php
Jérémy Munsch 81aabdab72
Version 3.0.0
This removes api authentification with token in favor of signature method.
Refactorize code and clean up. It add more unit test plus code coverage
in test/converage.

- Added config.api.timestampTimeout
- Added Helper::signRequest This calculates the request signature
- Added more unit tests and extend code coverage.
- Added rate limiter for web routes
- Added RemoveSpaces Sanitizer on api requests
- Added stress test suite
- Changed AuthServiceProvider api-token to api-hmac
- Changed CustomerSeeder to have at least of customer activated (for unit tests)
- Changed used transation for saveing multiple models
- Changed, in ServiceController generateApiToken to generateServicePassword
- Changed, timestamp has to be send in request with a DateTime format, it also has to a UTC  value
- Composer added brianium/paratest
- Composer update
- Fixed case where removing single optin could be the wrong one (not for the good service)
- Fixed customer delete
- Fixed customer would not be resolved by brand.
- Fixed Really check that request is json checking body content, preventing sending malformed requests
- Remove ApiError::needAuth
- Removed api_token
- Removed ApiController::authenticate
- Removed config.api.tokenTimeout
- Removed Header check or usage because of HAProxy doing weird stuff
- Removed lock files from gitignore
- Removed memcached dependency
- Removed unused questions resources api routes
- Removed workspace file

Migrations
- Added unique index on services.bearer_token
- Changed customer_id on customer_answer is nullable
- Changed services.bearer_pwd length to 312 (the size of the token)
- Dropped services.api_token and service.api_token_date
2022-01-06 12:11:05 +01:00

266 lines
7.5 KiB
PHP

<?php
namespace App\Models;
use App\Jobs\SetServiceStats;
use App\Lib\Helper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
use Kwaadpepper\CrudPolicies\Enums\CrudType;
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
use Rennokki\QueryCache\Traits\QueryCacheable;
use RuntimeException;
class Service extends Model
{
use IsCrudModel;
use HasFactory;
use QueryCacheable;
/** @var string */
protected $crudLabelColumn = 'name';
/** @var string */
protected $crudValueColumn = 'id';
/**
* The timeout cache for this model
*
* @var integer
*/
protected $cacheFor = 0;
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'name', 'brand_id'
];
/**
* Service
*
* @param array $attributes
*/
public function __construct(array $attributes = [])
{
$this->cacheFor = config('api.servicesDBCache') * 60;
self::$editableProperties = [
'name' => [
'label' => Str::ucfirst(trans('models.common.name')),
'type' => CrudType::string(),
'validate' => [
'min:5',
'max:255'
],
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update()
]
],
'brand_id' => [
'label' => Str::ucfirst(trans('models.classes.brand')),
'type' => CrudType::belongsTo(),
'belongsTo' => Brand::class,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update()
]
],
];
parent::__construct($attributes);
}
/**
* Set the base cache tags that will be present
* on all queries.
*
* ! This is used to flush caching
*
* @return array
*/
protected function getCacheBaseTags(): array
{
return [
'crm_services',
];
}
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted()
{
static::creating(function (self $service) {
$service->generateBearerToken();
$service->generateBearerPassword();
});
static::created(function () {
// * Flush Eloquent cache.
self::flushQueryCache();
});
static::updated(function () {
// * Flush Eloquent cache.
self::flushQueryCache();
});
static::deleted(function () {
// * Flush Eloquent cache.
self::flushQueryCache();
});
}
// * RELATIONS
/**
* Brand BelongsTo relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
/**
* ServiceStat hasMany relation
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function servicesStats()
{
return $this->hasMany(ServiceStat::class);
}
// * METHODS
/**
* Generates new bearer token
*
* @return string
*/
public function generateBearerToken(): string
{
$this->bearer_token = hash('ripemd160', Str::uuid());
$this->bearer_token_date = now();
return $this->bearer_token;
}
/**
* Generates new bearer password
* It store the crypted password in model
* and returns the plain password
*
* @return string
*/
public function generateBearerPassword(): string
{
$pwd = hash('sha256', Str::uuid());
$this->bearer_pwd = Crypt::encryptString($pwd);
return $pwd;
}
/**
* Increment API CRM Stat
*
* @return void
*/
public function incrementStat(): void
{
SetServiceStats::dispatch($this)->afterResponse()
->onConnection('database')->onQueue('stats');
}
/**
* Get Service from bearerToken with its brand
*
* TODO remove this method when dropping V1
*
* @param string $bearerToken
* @return \App\Models\Service
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException If the token matches no known service.
*/
public static function getFromBearerToken(string $bearerToken): self
{
$services = static::all();
/** @var \App\Models\Service $service */
foreach ($services as $service) {
// * Keep compatibility with V3.
$tokenEquals = false;
try {
$tokenEquals = Hash::check($bearerToken, $service->bearer_token);
} catch (RuntimeException $e) {
$tokenEquals = $bearerToken === $service->bearer_token;
}
if ($service->bearer_token and $tokenEquals) {
return $service->load([
'brand' => function ($query) use ($service) {
return $query->cacheTags(["service:{$service->id}:brand"]);
},
]);
}
}
throw new ModelNotFoundException('Could not find service from Service Token');
}
/**
* Authenticate the request for this service
*
* This will check hash-mac and that the request is wellformed
*
* The request should have the minimum following parameters
* - timestamp using format Y-m-d H:i:s, and must be UTC (Greenwich Mean Time)
* - bearerToken the public token generated by the crm
* - signature the hashmac signature of the request
*
* ! Signature must be calculated with every parameter urlencode once only
* ! All parameters to calculate the signature must be sorted (natural sort asc)
*
* ! Signature should look like
* * httpVerb\n
* * uri\n
* * queryString
*
* ! httpVerb as well as uri must be lower case
* ! uri should include scheme, host and path and nothing else, if path is empty a trailing slash should be added
* ! eg: https://ldc-crm.fr/customer/1 or https://ldc-crm.fr/
* ! The query string shall be separated with '&', eg param1=value1&param2=value2
* ! Key names and values of the query string must be url encoded using RFC 3986
* ! http://www.faqs.org/rfcs/rfc3986.html
* ! Signature must be hmac sha256 using the private key generatedby the crm
*
* * ! They should then be concat in a string separated by \n
*
*
*
* @param \Illuminate\Http\Request $request
* @return boolean
* @throws \App\Exceptions\Api\V2\CrmException If an error occurs.
*/
public function authenticateApi(Request $request): bool
{
$calculatedHash = Helper::signRequest(
$this->bearer_pwd,
$request->getUri(),
$request->getMethod(),
$request->all()
);
// ! Très important hash_equals https://fr.wikipedia.org/wiki/Attaque_temporelle
return \hash_equals($calculatedHash, $request->signature);
}
}