From fef6ed5f340fab464baa7db33c4a2ca6674833b9 Mon Sep 17 00:00:00 2001 From: yuri Date: Fri, 26 Oct 2018 12:06:20 +0300 Subject: [PATCH] api key authorization --- application/Espo/Controllers/User.php | 14 +++++ application/Espo/Core/Utils/Api/Auth.php | 33 +++++----- application/Espo/Core/Utils/Auth.php | 4 +- .../Espo/Core/Utils/Authentication/ApiKey.php | 50 +++++++++++++++ .../Espo/Core/Utils/Authentication/Hmac.php | 3 +- application/Espo/Repositories/User.php | 21 ++++++- .../Espo/Resources/i18n/en_US/User.json | 7 ++- .../Resources/metadata/entityDefs/User.json | 9 +++ application/Espo/Services/Record.php | 34 ++++++++-- application/Espo/Services/User.php | 62 ++++++++++++------- client/src/views/user/record/detail.js | 13 +++- client/src/views/user/record/edit.js | 9 +++ 12 files changed, 211 insertions(+), 48 deletions(-) create mode 100644 application/Espo/Core/Utils/Authentication/ApiKey.php diff --git a/application/Espo/Controllers/User.php b/application/Espo/Controllers/User.php index 3409f14d78..b023a0c5f3 100644 --- a/application/Espo/Controllers/User.php +++ b/application/Espo/Controllers/User.php @@ -113,4 +113,18 @@ class User extends \Espo\Core\Controllers\Record return $this->getRecordService()->generateNewApiKeyForEntity($data->id)->getValueMap(); } + + public function actionCreateLink($params, $data, $request) + { + if (!$this->getUser()->isAdmin()) throw new Forbidden(); + + return parent::actionCreateLink($params, $data, $request); + } + + public function actionRemoveLink($params, $data, $request) + { + if (!$this->getUser()->isAdmin()) throw new Forbidden(); + + return parent::actionRemoveLink($params, $data, $request); + } } diff --git a/application/Espo/Core/Utils/Api/Auth.php b/application/Espo/Core/Utils/Api/Auth.php index 409519693f..11233677d3 100644 --- a/application/Espo/Core/Utils/Api/Auth.php +++ b/application/Espo/Core/Utils/Api/Auth.php @@ -58,15 +58,21 @@ class Auth extends \Slim\Middleware $authenticationMethod = null; - $hmacAuthorizationHeader = $request->headers('X-Hmac-Authorization'); - - if ($hmacAuthorizationHeader) { - $authenticationMethod = 'Hmac'; - list($username, $password) = explode(':', base64_decode($hmacAuthorizationHeader), 2); + $espoAuthorizationHeader = $request->headers('Http-Espo-Authorization'); + if (isset($espoAuthorizationHeader)) { + list($username, $password) = explode(':', base64_decode($espoAuthorizationHeader), 2); } else { - $espoAuthorizationHeader = $request->headers('HTTP_ESPO_AUTHORIZATION'); - if (isset($espoAuthorizationHeader)) { - list($username, $password) = explode(':', base64_decode($espoAuthorizationHeader), 2); + $hmacAuthorizationHeader = $request->headers('X-Hmac-Authorization'); + if ($hmacAuthorizationHeader) { + $authenticationMethod = 'Hmac'; + list($username, $password) = explode(':', base64_decode($hmacAuthorizationHeader), 2); + } else { + $apiKeyHeader = $request->headers('X-Api-Key'); + if ($apiKeyHeader) { + $authenticationMethod = 'ApiKey'; + $username = $apiKeyHeader; + $password = null; + } } } @@ -78,9 +84,9 @@ class Auth extends \Slim\Middleware } if (!isset($username) && !isset($password)) { - $espoCgiAuth = $request->headers('HTTP_ESPO_CGI_AUTH'); + $espoCgiAuth = $request->headers('Http-Espo-Cgi-Auth'); if (empty($espoCgiAuth)) { - $espoCgiAuth = $request->headers('REDIRECT_HTTP_ESPO_CGI_AUTH'); + $espoCgiAuth = $request->headers('Redirect-Http-Espo-Cgi-Auth'); } if (!empty($espoCgiAuth)) { list($username, $password) = explode(':' , base64_decode(substr($espoCgiAuth, 6))); @@ -120,7 +126,7 @@ class Auth extends \Slim\Middleware } } - if ($username && $password) { + if ($username) { try { $isAuthenticated = $this->auth->login($username, $password, $authenticationMethod); } catch (\Exception $e) { @@ -165,9 +171,8 @@ class Auth extends \Slim\Middleware { $request = $this->app->request(); - $httpXRequestedWith = $request->headers('HTTP_X_REQUESTED_WITH'); - - if (isset($httpXRequestedWith) && strtolower($httpXRequestedWith) == 'xmlhttprequest') { + $httpXRequestedWith = $request->headers('Http-X-Requested-With'); + if ($httpXRequestedWith && strtolower($httpXRequestedWith) == 'xmlhttprequest') { return true; } diff --git a/application/Espo/Core/Utils/Auth.php b/application/Espo/Core/Utils/Auth.php index 8fc4a96f64..4ce7e75564 100644 --- a/application/Espo/Core/Utils/Auth.php +++ b/application/Espo/Core/Utils/Auth.php @@ -137,7 +137,7 @@ class Auth $isByTokenOnly = false; if (!$authenticationMethod) { - if ($this->request->headers->get('HTTP_ESPO_AUTHORIZATION_BY_TOKEN') === 'true') { + if ($this->request->headers->get('Http-Espo-Authorization-By-Token') === 'true') { $isByTokenOnly = true; } } @@ -247,7 +247,7 @@ class Auth $this->getEntityManager()->setUser($user); $this->getContainer()->setUser($user); - if ($this->request->headers->get('HTTP_ESPO_AUTHORIZATION')) { + if ($this->request->headers->get('Http-Espo-Authorization')) { if (!$authToken) { $authToken = $this->getEntityManager()->getEntity('AuthToken'); $token = $this->generateToken(); diff --git a/application/Espo/Core/Utils/Authentication/ApiKey.php b/application/Espo/Core/Utils/Authentication/ApiKey.php new file mode 100644 index 0000000000..9baa3ffd66 --- /dev/null +++ b/application/Espo/Core/Utils/Authentication/ApiKey.php @@ -0,0 +1,50 @@ +getEntityManager()->getRepository('User')->findOne([ + 'whereClause' => [ + 'type' => 'api', + 'apiKey' => $apiKey, + 'authMethod' => 'ApiKey' + ] + ]); + + return $user; + } +} diff --git a/application/Espo/Core/Utils/Authentication/Hmac.php b/application/Espo/Core/Utils/Authentication/Hmac.php index 821949d25b..a2dcf968b1 100644 --- a/application/Espo/Core/Utils/Authentication/Hmac.php +++ b/application/Espo/Core/Utils/Authentication/Hmac.php @@ -41,7 +41,8 @@ class Hmac extends Base $user = $this->getEntityManager()->getRepository('User')->findOne([ 'whereClause' => [ 'type' => 'api', - 'apiKey' => $apiKey + 'apiKey' => $apiKey, + 'authMethod' => 'Hmac' ] ]); diff --git a/application/Espo/Repositories/User.php b/application/Espo/Repositories/User.php index d62055baba..1c69f72dc2 100644 --- a/application/Espo/Repositories/User.php +++ b/application/Espo/Repositories/User.php @@ -70,6 +70,13 @@ class User extends \Espo\Core\ORM\Repositories\RDB if ($entity->isAttributeChanged('userName')) { $entity->set('lastName', $entity->get('userName')); } + if ($entity->has('authMethod') && $entity->get('authMethod') !== 'Hmac') { + $entity->clear('secretKey'); + } + } else { + if ($entity->isAttributeChanged('type')) { + $entity->set('authMethod', null); + } } parent::beforeSave($entity, $options); @@ -126,10 +133,20 @@ class User extends \Espo\Core\ORM\Repositories\RDB parent::afterSave($entity, $options); if ($entity->isApi()) { - if ($entity->get('apiKey') && $entity->get('secretKey') && $entity->isAttributeChanged('apiKey')) { + if ( + $entity->get('apiKey') && $entity->get('secretKey') && + ( + $entity->isAttributeChanged('apiKey') || $entity->isAttributeChanged('authMethod') + ) + ) { $apiKeyUtil = new \Espo\Core\Utils\ApiKey($this->getConfig()); $apiKeyUtil->storeSecretKeyForUserId($entity->id, $entity->get('secretKey')); } + + if ($entity->isAttributeChanged('authMethod') && $entity->get('authMethod') !== 'Hmac') { + $apiKeyUtil = new \Espo\Core\Utils\ApiKey($this->getConfig()); + $apiKeyUtil->removeSecretKeyForUserId($entity->id); + } } } @@ -137,7 +154,7 @@ class User extends \Espo\Core\ORM\Repositories\RDB { parent::afterRemove($entity, $options); - if ($entity->isApi()) { + if ($entity->isApi() && $entity->get('authMethod') === 'Hmac') { $apiKeyUtil = new \Espo\Core\Utils\ApiKey($this->getConfig()); $apiKeyUtil->removeSecretKeyForUserId($entity->id); } diff --git a/application/Espo/Resources/i18n/en_US/User.json b/application/Espo/Resources/i18n/en_US/User.json index 2ea0e61fcd..daa5edab00 100644 --- a/application/Espo/Resources/i18n/en_US/User.json +++ b/application/Espo/Resources/i18n/en_US/User.json @@ -32,7 +32,8 @@ "isSuperAdmin": "Is Super Admin", "lastAccess": "Last Access", "apiKey": "API Key", - "secretKey": "Secret Key" + "secretKey": "Secret Key", + "authMethod": "Authentication Method" }, "links": { "teams": "Teams", @@ -99,6 +100,10 @@ "system": "System", "super-admin": "Super-Admin", "api": "API" + }, + "authMethod": { + "ApiKey": "API Key", + "Hmac": "HMAC" } }, "boolFilters": { diff --git a/application/Espo/Resources/metadata/entityDefs/User.json b/application/Espo/Resources/metadata/entityDefs/User.json index bcd74413ce..4db34dc8fa 100644 --- a/application/Espo/Resources/metadata/entityDefs/User.json +++ b/application/Espo/Resources/metadata/entityDefs/User.json @@ -37,6 +37,15 @@ "disabled": true, "notStorable": true }, + "authMethod": { + "type": "enum", + "options": ["ApiKey", "Hmac"], + "maxLength": 24, + "layoutMassUpdateDisabled": true, + "layoutDetailDisabled": true, + "layoutFiltersDisabled": true, + "layoutListDisabled": true + }, "apiKey": { "type": "varchar", "maxLength": 100, diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index 09ffa7f568..40d618a88d 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -69,8 +69,12 @@ class Record extends \Espo\Core\Services\Base protected $internalAttributeList = []; + protected $onlyAdminAttributeList = []; + protected $readOnlyAttributeList = []; + protected $nonAdminReadOnlyAttributeList = []; + protected $readOnlyLinkList = []; protected $linkSelectParams = []; @@ -625,9 +629,21 @@ class Record extends \Espo\Core\Services\Base $data->$key = $this->filterInputAttribute($key, $data->$key); } + if (!$this->getUser()->isAdmin()) { + foreach ($this->onlyAdminAttributeList as $attribute) { + unset($data->$attribute); + } + } + foreach ($this->getAcl()->getScopeForbiddenAttributeList($this->entityType, 'edit') as $attribute) { unset($data->$attribute); } + + if (!$this->getUser()->isAdmin()) { + foreach ($this->nonAdminReadOnlyAttributeList as $attribute) { + unset($data->$attribute); + } + } } protected function handleInput($data) @@ -640,10 +656,10 @@ class Record extends \Espo\Core\Services\Base if (empty($data->forceDuplicate)) { $duplicates = $this->checkEntityForDuplicate($entity, $data); if (!empty($duplicates)) { - $reason = array( + $reason = [ 'reason' => 'Duplicate', 'data' => $duplicates - ); + ]; throw new Conflict(json_encode($reason)); } } @@ -1595,6 +1611,11 @@ class Record extends \Espo\Core\Services\Base if (in_array($attribute, $this->internalAttributeList)) { return false; } + + if (!$this->getUser()->isAdmin() && in_array($attribute, $this->onlyAdminAttributeList)) { + return true; + } + if (!$isExportAllFields) { return true; } @@ -1858,8 +1879,13 @@ class Record extends \Espo\Core\Services\Base public function prepareEntityForOutput(Entity $entity) { - foreach ($this->internalAttributeList as $field) { - $entity->clear($field); + foreach ($this->internalAttributeList as $attribute) { + $entity->clear($attribute); + } + if (!$this->getUser()->isAdmin()) { + foreach ($this->onlyAdminAttributeList as $attribute) { + $entity->clear($attribute); + } } foreach ($this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read') as $attribute) { $entity->clear($attribute); diff --git a/application/Espo/Services/User.php b/application/Espo/Services/User.php index 190b047766..bb7491d563 100644 --- a/application/Espo/Services/User.php +++ b/application/Espo/Services/User.php @@ -62,14 +62,24 @@ class User extends Record 'isAdmin', 'isPortalUser', 'teamsIds', + 'teamsColumns', + 'teamsNames', 'rolesIds', + 'rolesNames', 'password', 'portalsIds', 'portalRolesIds', 'contactId', 'accountsIds', 'type', - 'apiKey' + 'apiKey', + 'secretKey' + ]; + + protected $onlyAdminAttributeList = [ + 'authMethod', + 'apiKey', + 'secretKey' ]; protected $mandatorySelectAttributeList = [ @@ -80,13 +90,13 @@ class User extends Record 'type' ]; - protected $linkSelectParams = array( - 'targetLists' => array( - 'additionalColumns' => array( + protected $linkSelectParams = [ + 'targetLists' => [ + 'additionalColumns' => [ 'optedOut' => 'isOptedOut' - ) - ) - ); + ] + ] + ]; protected function getMailSender() { @@ -258,9 +268,6 @@ class User extends Record } if (!$this->getUser()->isAdmin()) { - foreach ($this->nonAdminReadOnlyAttributeList as $attribute) { - unset($data->$attribute); - } if (!$this->getAcl()->checkScope('Team')) { unset($data->defaultTeamId); } @@ -324,8 +331,10 @@ class User extends Record if ($entity->isApi()) { if ($this->getUser()->isAdmin()) { - $secretKey = $this->getSecretKeyForUserId($entity->id); - $entity->set('secretKey', $secretKey); + if ($entity->get('authMethod') === 'Hmac') { + $secretKey = $this->getSecretKeyForUserId($entity->id); + $entity->set('secretKey', $secretKey); + } } else { $entity->clear('apiKey'); $entity->clear('secretKey'); @@ -350,8 +359,10 @@ class User extends Record $apiKey = \Espo\Core\Utils\Util::generateApiKey(); $entity->set('apiKey', $apiKey); - $secretKey = \Espo\Core\Utils\Util::generateKey(); - $entity->set('secretKey', $secretKey); + if ($entity->get('authMethod') === 'Hmac') { + $secretKey = \Espo\Core\Utils\Util::generateKey(); + $entity->set('secretKey', $secretKey); + } $this->getEntityManager()->saveEntity($entity); @@ -396,18 +407,20 @@ class User extends Record $apiKey = \Espo\Core\Utils\Util::generateApiKey(); $entity->set('apiKey', $apiKey); - $secretKey = \Espo\Core\Utils\Util::generateKey(); - $entity->set('secretKey', $secretKey); + if ($entity->get('authMethod') === 'Hmac') { + $secretKey = \Espo\Core\Utils\Util::generateKey(); + $entity->set('secretKey', $secretKey); + } } } - protected function beforeUpdateEntity(Entity $user, $data) + protected function beforeUpdateEntity(Entity $entity, $data) { if ($this->getConfig()->get('userLimit') && !$this->getUser()->isSuperAdmin()) { if ( - ($user->get('isActive') && $user->isAttributeChanged('isActive') && !$user->isPortal()) + ($entity->get('isActive') && $entity->isAttributeChanged('isActive') && !$entity->isPortal()) || - (!$user->isPortal() && $user->isAttributeChanged('type')) + (!$entity->isPortal() && $entity->isAttributeChanged('type')) ) { $userCount = $this->getInternalUserCount(); if ($userCount >= $this->getConfig()->get('userLimit')) { @@ -417,9 +430,9 @@ class User extends Record } if ($this->getConfig()->get('portalUserLimit') && !$this->getUser()->isSuperAdmin()) { if ( - ($user->get('isActive') && $user->isAttributeChanged('isActive') && $user->isPortal()) + ($entity->get('isActive') && $entity->isAttributeChanged('isActive') && $entity->isPortal()) || - ($user->isPortal() && $user->isAttributeChanged('type')) + ($entity->isPortal() && $entity->isAttributeChanged('type')) ) { $portalUserCount = $this->getPortalUserCount(); if ($portalUserCount >= $this->getConfig()->get('portalUserLimit')) { @@ -427,6 +440,13 @@ class User extends Record } } } + + if ($entity->isApi()) { + if ($entity->isAttributeChanged('authMethod') && $entity->get('authMethod') === 'Hmac') { + $secretKey = \Espo\Core\Utils\Util::generateKey(); + $entity->set('secretKey', $secretKey); + } + } } protected function sendPassword(Entity $user, $password) diff --git a/client/src/views/user/record/detail.js b/client/src/views/user/record/detail.js index 493ee5cadd..08596b45f6 100644 --- a/client/src/views/user/record/detail.js +++ b/client/src/views/user/record/detail.js @@ -75,7 +75,7 @@ Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { setupActionItems: function () { Dep.prototype.setupActionItems.call(this); - if (this.model.isApi()) { + if (this.model.isApi() && this.getUser().isAdmin()) { this.addDropdownItem({ 'label': 'Generate New API Key', 'name': 'generateNewApiKey' @@ -144,6 +144,12 @@ Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { this.hideField('name'); this.hideField('gender'); + if (this.model.get('authMethod') === 'Hmac') { + this.showField('secretKey'); + } else { + this.hideField('secretKey'); + } + } else { this.showField('title'); } @@ -240,9 +246,10 @@ Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { if (this.getUser().isAdmin() && this.model.isApi()) { layout.push({ - "name": "apiKey", + "name": "auth", "rows": [ - [{"name":"apiKey"}, {"name":"secretKey"}] + [{"name":"authMethod"}, false], + [{"name":"apiKey"}, {"name":"secretKey"}], ] }); } diff --git a/client/src/views/user/record/edit.js b/client/src/views/user/record/edit.js index ba56073eb8..d44a98c77f 100644 --- a/client/src/views/user/record/edit.js +++ b/client/src/views/user/record/edit.js @@ -161,6 +161,15 @@ Espo.define('views/user/record/edit', ['views/record/edit', 'views/user/record/d }); } + if (this.getUser().isAdmin() && this.model.isApi()) { + layout.push({ + "name": "auth", + "rows": [ + [{"name":"authMethod"}, false] + ] + }); + } + var gridLayout = { type: 'record', layout: this.convertDetailLayout(layout),