From 91a70eac8c9d239f395b3b80ba7bac4fe49bcfc5 Mon Sep 17 00:00:00 2001 From: Shayna Atkinson Date: Fri, 28 Jul 2023 10:56:27 -0400 Subject: [PATCH 1/3] Add capability to generate, print, and delete Privacy Idea backup codes as an authenticator for a CO Person --- .../Config/Schema/schema.xml | 32 +++ .../Controller/PaperTokensController.php | 198 ++++++++++++++++++ .../Controller/PrivacyIdeasController.php | 3 +- .../PrivacyIdeaAuthenticator/Lib/enum.php | 2 + .../PrivacyIdeaAuthenticator/Lib/lang.php | 14 +- .../Model/PaperToken.php | 67 ++++++ .../Model/PrivacyIdea.php | 56 +++-- .../Model/PrivacyIdeaAuthenticator.php | 154 ++++++++------ .../View/PaperTokens/fields.inc | 123 +++++++++++ .../View/PaperTokens/generate.ctp | 118 +++++++++++ .../View/PaperTokens/index.ctp | 125 +++++++++++ app/Lib/lang.php | 1 + .../Model/CoLdapProvisionerTarget.php | 12 +- app/webroot/css/co-base.css | 6 + 14 files changed, 831 insertions(+), 80 deletions(-) create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PaperToken.php create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/fields.inc create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Config/Schema/schema.xml b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Config/Schema/schema.xml index ad6d648af..4892f3fcb 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Config/Schema/schema.xml +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Config/Schema/schema.xml @@ -86,4 +86,36 @@ totp_token_id + + + + + + + + REFERENCES cm_privacy_idea_authenticators(id) + + + REFERENCES cm_co_people(id) + + + + + + REFERENCES cm_paper_tokens(id) + + + + + + + co_person_id + + + serial + + + paper_token_id + +
\ No newline at end of file diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php new file mode 100644 index 000000000..1407f7b1b --- /dev/null +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php @@ -0,0 +1,198 @@ + array('Server') + ); + + /** + * Add a Standard Object. + * + * @since COmanage Registry v4.3.0 + */ + + public function generate() { + if($this->request->is('get')) { + parent::add(); + + //$this->set('title_for_layout', 'Generated '.$this->viewVars['vv_authenticator']['Authenticator']['description']); + $this->set('title_for_layout', 'Generated Backup Codes'); + + if(!empty($this->request->params['named']['onFinish'])) { + $this->set('vv_on_finish_url', $this->request->params['named']['onFinish']); + } + + try { + $tokenInfo = $this->PrivacyIdea->createToken($this->viewVars['vv_authenticator']['PrivacyIdeaAuthenticator'], + $this->viewVars['vv_co_person']['CoPerson']['id']); + + $this->set('vv_token_info', $tokenInfo); + + $newdata = array( + 'PaperToken' => array( + 'co_person_id' => $this->viewVars['vv_co_person']['CoPerson']['id'], + 'serial' => $tokenInfo['serial'] + ) + ); + + $this->generateHistory('generate', $newdata, array()); + + if(!empty($tokenInfo['otps'])) { + $this->set('vv_otps', (array)$tokenInfo['otps']); + debug($vv_otps); + } + } + catch(Exception $e) { + $this->Flash->set($e->getMessage(), array('key' => 'error')); + } + } + } + + /** + * Callback before other controller methods are invoked or views are rendered. + * + * @since COmanage Registry v4.3.0 + */ + + public function beforeFilter() { + // We operate as a virtual controller, and tweak the settings to pull + // records for the token type that this instantiation is configured for + + $this->uses[] = 'PrivacyIdeaAuthenticator.PrivacyIdea'; + + parent::beforeFilter(); + } + + /** + * Perform any dependency checks required prior to a delete operation. + * This method is intended to be overridden by model-specific controllers. + * - postcondition: Session flash message updated (HTML) or HTTP status returned (REST) + * + * @since COmanage Registry v4.3.0 + * @param Array Current data + * @return boolean true if dependency checks succeed, false otherwise. + */ + + function checkDeleteDependencies($curdata) { + // Remove the Token from the server. This should really happen in + // PaperToken::beforeDelete(), but for some reason (probably some weird + // namespace management issue) that callback isn't being called. It's also + // slightly easier to make the API call from the controller (using the + // PrivacyIdea model). Ultimately, this will need to be rewritten for + // Cake 4. + + // We need the Serial ID, not the token ID + + if(empty($curdata['PaperToken']['serial'])) { + throw new InvalidArgumentException(_txt('er.notprov.id', array(_txt('pl.privacyideaauthenticator.fd.serial')))); + } + + $this->PrivacyIdea->deleteToken($this->viewVars['vv_authenticator']['PrivacyIdeaAuthenticator'], + $curdata['PaperToken']['serial']); + + return true; + } + + /** + * Generate history records for a transaction. This method is intended to be + * overridden by model-specific controllers, and will be called from within a + * try{} block so that HistoryRecord->record() may be called without worrying + * about catching exceptions. + * + * @since COmanage Registry v4.3.0 + * @param String Controller action causing the change + * @param Array Data provided as part of the action (for add/edit) + * @param Array Previous data (for delete/edit) + * @return boolean Whether the function completed successfully (which does not necessarily imply history was recorded) + */ + + public function generateHistory($action, $newdata, $olddata) { + // Build a change string + $cstr = ""; + $cop = null; + $act = null; + + switch($action) { + case 'generate': + $cstr = _txt('rs.generated-a2', array(_txt('ct.paper_tokens.1'), $newdata['PaperToken']['serial'])); + $cop = $newdata['PaperToken']['co_person_id']; + $act = PrivacyIDEActionEnum::TokenGenerated; + break; + case 'delete': + $cstr = _txt('rs.deleted-a2', array(_txt('ct.paper_tokens.1'), $olddata['PaperToken']['serial'])); + $cop = $olddata['PaperToken']['co_person_id']; + $act = PrivacyIDEActionEnum::TokenDeleted; + break; + } + + $this->Co->CoPerson->HistoryRecord->record($cop, + null, + null, + $this->Session->read('Auth.User.co_person_id'), + $act, + $cstr); + + return true; + } + + /** + * Authorization for this Controller, called by Auth component + * - precondition: Session.Auth holds data used for authz decisions + * - postcondition: $permissions set with calculated permissions + * + * @since COmanage Registry v4.3.0 + * @return Array Permissions + */ + + function isAuthorized() { + $roles = $this->Role->calculateCMRoles(); + + // Construct the permission set for this user, which will also be passed to the view. + $p = array(); + + // Determine what operations this user can perform + + // Merge in the permissions calculated by our parent + $p = array_merge($p, $this->calculateParentPermissions(true)); + + // Tokens can't be edited, only deleted + $p['edit'] = false; + + $p['generate'] = isset($p['manage']) ? $p['manage'] : false; + + $this->set('permissions', $p); + return($p[$this->action]); + } +} \ No newline at end of file diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PrivacyIdeasController.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PrivacyIdeasController.php index 1647bd705..a951cb0b2 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PrivacyIdeasController.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PrivacyIdeasController.php @@ -32,7 +32,8 @@ class PrivacyIdeasController extends SAMController { public $name = "PrivacyIdeas"; protected $pi_token_models = array( - PrivacyIDEATokenTypeEnum::TOTP => 'TotpToken' + PrivacyIDEATokenTypeEnum::TOTP => 'TotpToken', + PrivacyIDEATokenTypeEnum::Paper => 'PaperToken' ); /** diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/enum.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/enum.php index e6dd30e2f..a2c2b0cd6 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/enum.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/enum.php @@ -31,9 +31,11 @@ class PrivacyIDEActionEnum const TokenConfirmed = 'MFAC'; const TokenDeleted = 'MFAD'; const TokenEdited = 'MFAE'; + const TokenGenerated = 'MFAG'; } class PrivacyIDEATokenTypeEnum { const TOTP = 'TO'; + const Paper = 'PP'; } diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php index e41bd9246..8eb162970 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php @@ -39,6 +39,8 @@ 'ct.privacy_ideas.pl' => 'PrivacyIDEA Tokens', 'ct.totp_tokens.1' => 'TOTP Token', 'ct.totp_tokens.pl' => 'TOTP Tokens', + 'ct.paper_tokens.1' => 'Backup Codes Token', + 'ct.paper_tokens.pl' => 'Backup Codes Tokens', // Enumerations 'pl.privacyideaauthenticator.en.action' => array( @@ -49,7 +51,8 @@ ), 'en.privacyideaauthenticator.token_type' => array( - PrivacyIDEATokenTypeEnum::TOTP => 'Time-Based OTP (TOTP)' + PrivacyIDEATokenTypeEnum::TOTP => 'Time-Based OTP (TOTP)', + PrivacyIDEATokenTypeEnum::Paper => 'Backup Codes' ), // Error messages @@ -58,13 +61,18 @@ // Plugin texts 'pl.privacyideaauthenticator.alt.google' => 'QR Code for Google Authenticator', + 'pl.privacyideaauthenticator.alt.paper' => 'Paper token list', 'pl.privacyideaauthenticator.fd.identifier_type' => 'Identifier Type', 'pl.privacyideaauthenticator.fd.realm' => 'PrivacyIDEA Realm', 'pl.privacyideaauthenticator.fd.serial' => 'Serial', 'pl.privacyideaauthenticator.fd.token_type' => 'Token Type', 'pl.privacyideaauthenticator.fd.validation_server' => 'Validation API Server', - 'pl.privacyideaauthenticator.status' => '%1$s token(s) registered, %2$s confirmed', + 'pl.privacyideaauthenticator.totpstatus' => '%1$s token(s) registered, %2$s confirmed', + 'pl.privacyideaauthenticator.paperstatus' => '%1$s token(s) registered', 'pl.privacyideaauthenticator.token.confirmed' => 'Token Confirmed', 'pl.privacyideaauthenticator.totp.step1' => 'First, scan the QR Code to add this token to Google Authenticator', - 'pl.privacyideaauthenticator.totp.step2' => 'Then, enter the current code from the Google Authenticator app to confirm' + 'pl.privacyideaauthenticator.totp.step2' => 'Then, enter the current code from the Google Authenticator app to confirm', + 'pl.privacyideaauthenticator.paper.intro' => 'Use the backup codes in order, one after the other. Mark off used values.', + 'pl.privacyideaauthenticator.paper.caution' => 'Backup codes are a weak second factor. Please assure no one has access to these values. Store them in a safe location', + 'pl.privacyideaauthenticator.paper.warning' => 'Before you leave this page, please confirm that you have copied your backup codes. YOU WILL NOT SEE THEM AGAIN.' ); diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PaperToken.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PaperToken.php new file mode 100644 index 000000000..1be5a86d5 --- /dev/null +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PaperToken.php @@ -0,0 +1,67 @@ + array('priority' => 5), + 'Provisioner'); + + // Association rules from this model to other models + public $belongsTo = array( + "PrivacyIdeaAuthenticator.PrivacyIdeaAuthenticator", + "CoPerson" + ); + + // Default display field for cake generated views + public $displayField = "password_type"; + + // Validation rules for table elements + public $validate = array( + 'privacy_idea_authenticator_id' => array( + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ), + 'co_person_id' => array( + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ), + 'serial' => array( + 'rule' => 'notBlank', + 'required' => true, + 'allowEmpty' => false + ) + ); +} \ No newline at end of file diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php index 20582f657..ffbffff0d 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php @@ -28,6 +28,7 @@ App::uses("Identifier", "Model"); App::uses("Server", "Model"); App::uses("TotpToken", "PrivacyIdeaAuthenticator.Model"); +App::uses("PaperToken", "PrivacyIdeaAuthenticator.Model"); class PrivacyIdea extends AppModel { // Define class name for cake @@ -164,18 +165,27 @@ public function createToken($privacyIdeaAuthenticator, $coPersonId) { $identifier = $this->lookupIdentifier($privacyIdeaAuthenticator['identifier_type'], $coPersonId); $Http = $this->connect($privacyIdeaAuthenticator['server_id']); - - // XXX For now we only set params for TOTP tokens. We'll need to refactor - // this section when we add support for additional token types. - + + $token_type = $privacyIdeaAuthenticator['token_type']; + $params = array( - 'type' => 'totp', 'user' => $identifier, 'realm' => $privacyIdeaAuthenticator['realm'], 'genkey' => '1', - 'optlen' => '6' ); - + + switch ($token_type) { + case PrivacyIDEATokenTypeEnum::TOTP: + $params['type'] = 'totp'; + $params['otplen'] = '6'; + break; + + case PrivacyIDEATokenTypeEnum::Paper: + $params['type'] = 'paper'; + $params['otplen'] = '8'; + break; + } + $response = $Http->post("/token/init", $params, $this->requestCfg); $jresponse = json_decode($response); @@ -188,15 +198,31 @@ public function createToken($privacyIdeaAuthenticator, $coPersonId) { 'privacy_idea_authenticator_id' => $privacyIdeaAuthenticator['id'], 'co_person_id' => $coPersonId, 'serial' => $jresponse->detail->serial, - 'confirmed' => false ); - - $TotpToken = new TotpToken(); - $TotpToken->save($token); - - // We don't persist the QR Data, but we do need to return it for rendering - $token['qr_data'] = $jresponse->detail->googleurl->img; - + + switch ($token_type) { + case PrivacyIDEATokenTypeEnum::TOTP: + $token['confirmed'] = false; + $TotpToken = new TotpToken(); + $TotpToken->save($token); + + // We don't persist the QR Data, but we do need to return it for rendering + $token['qr_data'] = $jresponse->detail->googleurl->img; + break; + + case PrivacyIDEATokenTypeEnum::Paper: + $PaperToken = new PaperToken(); + $PaperToken->save($token); + + // We don't persist the codes themselves but need to present them to the user for copying/printing + $token['otps'] = (array)$jresponse->detail->otps; + break; + } + + if(!$jresponse->result->status) { + throw new RuntimeException($jresponse->result->error->message); + } + return $token; } diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php index 3f6b153d6..1819cdca3 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php @@ -35,27 +35,28 @@ class PrivacyIdeaAuthenticator extends AuthenticatorBackend { // Required by COmanage Plugins public $cmPluginType = "authenticator"; - // Add behaviors + // Add behaviors public $actsAs = array('Containable'); // Document foreign keys public $cmPluginHasMany = array( - "CoPerson" => array("TotpToken") - ); + "CoPerson" => array("TotpToken", "PaperToken"), + ); - // Association rules from this model to other models - public $belongsTo = array( - "Authenticator", + // Association rules from this model to other models + public $belongsTo = array( + "Authenticator", "Server", "ValidationServer" => array( 'className' => 'Server', 'foreignKey' => 'validation_server_id' ) - ); + ); - public $hasMany = array( - "PrivacyIdeaAuthenticator.TotpToken" - ); + public $hasMany = array( + "PrivacyIdeaAuthenticator.TotpToken", + "PrivacyIdeaAuthenticator.PaperToken" + ); // Default display field for cake generated views public $displayField = "realm"; @@ -92,7 +93,7 @@ class PrivacyIdeaAuthenticator extends AuthenticatorBackend { 'allowEmpty' => false ), 'token_type' => array( - 'rule' => array('inList', array(PrivacyIDEATokenTypeEnum::TOTP)), + 'rule' => array('inList', array(PrivacyIDEATokenTypeEnum::TOTP, PrivacyIDEATokenTypeEnum::Paper)), 'required' => true, 'allowEmpty' => false ), @@ -125,7 +126,7 @@ public function cmPluginMenus() { return array(); } - /** + /** * Obtain current data suitable for passing to manage() and provisioners. * * @since COmanage Registry v4.0.0 @@ -134,16 +135,27 @@ public function cmPluginMenus() { * @param integer $coPersonId CO Person ID * @return Array As returned by find * @throws RuntimeException - */ + */ + + public function current($id, $backendId, $coPersonId) { - public function current($id, $backendId, $coPersonId) { $args = array(); - $args['conditions']['TotpToken.privacy_idea_authenticator_id'] = $backendId; + $args['conditions']['TotpToken.co_person_id'] = $coPersonId; + $args['conditions']['TotpToken.privacy_idea_authenticator_id'] = $backendId; $args['contain'] = false; - - return $this->TotpToken->find('all', $args); - } + $results = $this->TotpToken->find('all', $args); + + if(empty($results)) { + unset($args); + $args['conditions']['PaperToken.co_person_id'] = $coPersonId; + $args['conditions']['PaperToken.privacy_idea_authenticator_id'] = $backendId; + $args['contain'] = false; + $results = $this->PaperToken->find('all', $args); + } + + return $results; + } /** * Perform backend specific actions on a lock operation. @@ -172,57 +184,79 @@ public function lock($id, $coPersonId) { return true; } - /** - * Reset Authenticator data for a CO Person. - * - * @since COmanage Registry v3.1.0 - * @param integer $coPersonId CO Person ID - * @param integer $actorCoPersonId Actor CO Person ID - * @return boolean true on success - */ + /** + * Reset Authenticator data for a CO Person. + * + * @since COmanage Registry v3.1.0 + * @param integer $coPersonId CO Person ID + * @param integer $actorCoPersonId Actor CO Person ID + * @return boolean true on success + */ public function reset($coPersonId, $actorCoPersonId) { - // It's not immediately obvious what we should do on a reset... so for now + // It's not immediately obvious what we should do on a reset... so for now // we return false until we have better requirements. - return false; - } + return false; + } - /** - * Obtain the current Authenticator status for a CO Person. - * - * @since COmanage Registry v4.0.0 - * @param integer $coPersonId CO Person ID - * @return Array Array with values - * status: AuthenticatorStatusEnum - * comment: Human readable string, visible to the CO Person - */ + /** + * Obtain the current Authenticator status for a CO Person. + * + * @since COmanage Registry v4.0.0 + * @param integer $coPersonId CO Person ID + * @return Array Array with values + * status: AuthenticatorStatusEnum + * comment: Human readable string, visible to the CO Person + */ - public function status($coPersonId) { - // We can have more than one Authenticator, but only of the type token_type. - // For now, we only work with TotpTokens. - + public function status($coPersonId) { + $status = AuthenticatorStatusEnum::NotSet; $comment = _txt('fd.set.not'); - + + $pcfg = $this->getConfig(); + + $token_type = $pcfg['PrivacyIdeaAuthenticator']['token_type']; + $piauth_id = $pcfg['PrivacyIdeaAuthenticator']['id']; + $args = array(); - $args['conditions']['TotpToken.co_person_id'] = $coPersonId; - $args['contain'] = false; - - $tokens = $this->TotpToken->find('all', $args); - - if(count($tokens) > 0) { - $confirmed = Hash::extract($tokens, '{n}.TotpToken[confirmed=true]'); - - $status = AuthenticatorStatusEnum::Active; - $comment = _txt('pl.privacyideaauthenticator.status', array(count($tokens), count($confirmed))); + + switch ($token_type) { + case PrivacyIDEATokenTypeEnum::TOTP: + $args['conditions']['TotpToken.co_person_id'] = $coPersonId; + $args['conditions']['TotpToken.privacy_idea_authenticator_id'] = $piauth_id; + $args['contain'] = false; + + $tokens = $this->TotpToken->find('all', $args); + + if(count($tokens) > 0) { + $confirmed = Hash::extract($tokens, '{n}.TotpToken[confirmed=true]'); + + $status = AuthenticatorStatusEnum::Active; + $comment = _txt('pl.privacyideaauthenticator.totpstatus', array(count($tokens), count($confirmed))); + } + break; + + case PrivacyIDEATokenTypeEnum::Paper: + $args['conditions']['PaperToken.co_person_id'] = $coPersonId; + $args['conditions']['PaperToken.privacy_idea_authenticator_id'] = $piauth_id; + $args['contain'] = false; + + $tokens = $this->PaperToken->find('all', $args); + + if(count($tokens) > 0) { + $status = AuthenticatorStatusEnum::Active; + $comment = _txt('pl.privacyideaauthenticator.paperstatus', array(count($tokens))); + } + break; } - - return array( - 'status' => $status, - 'comment' => $comment - ); - } - + + return array( + 'status' => $status, + 'comment' => $comment + ); + } + /** * Perform backend specific actions on an unlock operation. * diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/fields.inc b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/fields.inc new file mode 100644 index 000000000..924370312 --- /dev/null +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/fields.inc @@ -0,0 +1,123 @@ + +Form->hidden('privacy_idea_authenticator_id', + array('default' => $vv_authenticator['PrivacyIdeaAuthenticator']['id'])) . "\n"; + print $this->Form->hidden('co_person_id', array('default' => $vv_co_person['CoPerson']['id'])) . "\n"; + + if(!empty($vv_token_info['otps'])) { + print $this->Form->hidden('otps', array('default' => $vv_token_info['otps'])); + } + + if(!empty($vv_token_info['serial'])) { + print $this->Form->hidden('serial', array('default' => $vv_token_info['serial'])); + } + + if(!empty($vv_on_finish_url)) { + print $this->Form->hidden('onFinish', array('default' => $vv_on_finish_url)); + } + + // Add breadcrumbs + print $this->element("coCrumb", array('authenticator' => 'PrivacyIdea')); +?> + + +

+ +

+
+ info + +
+
+ warning + +
+ +
+ + + + + + + + + $otp): ?> + + + + + + + +
#OTP
+
+ +action == 'view'): ?> + + + diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp new file mode 100644 index 000000000..06fa5d16f --- /dev/null +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp @@ -0,0 +1,118 @@ + +request->params['named']['onFinish'])) { + $params['topLinks'][] = $this->Html->link(_txt('op.skip'), + urldecode($this->request->params['named']['onFinish']), + array('class' => 'forwardbutton')); + } + print $this->element("pageTitleAndButtons", $params); + + // Add breadcrumbs + print $this->element("coCrumb", array('authenticator' => 'PrivacyIdea')); +?> + + +

+ +

+
+ info + +
+
+ warning + +
+ +
+ + + + + + + + + $otp): ?> + + + + + + + +
#OTP
+
+ +action == 'view'): ?> + + + diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp new file mode 100644 index 000000000..b6cc885c9 --- /dev/null +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp @@ -0,0 +1,125 @@ +element("coCrumb", array('authenticator' => 'PrivacyIdea')); + + // Add page title + $params = array(); + $params['title'] = $title_for_layout; + + // Add top links + $params['topLinks'] = array(); + + if($permissions['add']) { + $params['topLinks'][] = $this->Html->link( + _txt('op.generate', array($title_for_layout)), + array( + 'plugin' => 'privacy_idea_authenticator', // XXX can inflect from $vv_authenticator['Authenticator']['plugin'] + 'controller' => 'paper_tokens', + 'action' => 'generate', + 'authenticatorid' => $vv_authenticator['Authenticator']['id'], + 'copersonid' => $vv_co_person['CoPerson']['id'] + ), + array('class' => 'addbutton') + ); + } + + print $this->element("pageTitleAndButtons", $params); +?> + + + + + + + + + + + + + + + + + + + + +
Paginator->sort('token_type', _txt('pl.privacyideaauthenticator.fd.token_type')); ?>Paginator->sort('serial', _txt('pl.privacyideaauthenticator.fd.serial')); ?>
+ Html->link( + // $t['PaperToken']['serial'], + // array( + // 'plugin' => 'privacy_idea_authenticator', // XXX can inflect from $vv_authenticator['Authenticator']['plugin'] + // 'controller' => 'paper_tokens', + // 'action' => 'view', + // $t['PaperToken']['id'] + // ) + // ); + print $t['PaperToken']['serial']; + ?> + + Html->link( + // _txt('op.view'), + // array( + // 'plugin' => 'privacy_idea_authenticator', + // 'controller' => 'paper_tokens', + // 'action' => 'view', + // $t['PaperToken']['id'] + // ), + // array('class' => 'viewbutton') + // ) . "\n"; + // } + + if($permissions['delete']) { + print ''; + } + ?> +
+ +element("pagination"); diff --git a/app/Lib/lang.php b/app/Lib/lang.php index 66775d709..62d092d0b 100644 --- a/app/Lib/lang.php +++ b/app/Lib/lang.php @@ -2424,6 +2424,7 @@ 'rs.ev.ver-a' => 'Email Address %1$s verified by %2$s', 'rs.expunged' => 'Record Expunged', 'rs.found.cnt' => '%1$s Record(s) Found', + 'rs.generated-a2' => '%1$s "%2$s" Generated', 'rs.gr.added' => 'Added CO Group "%1$s"', 'rs.gr.deleted' => 'Deleted CO Group "%1$s"', 'rs.gr.reconcile.ok' => 'Reconciliation Successful', diff --git a/app/Plugin/LdapProvisioner/Model/CoLdapProvisionerTarget.php b/app/Plugin/LdapProvisioner/Model/CoLdapProvisionerTarget.php index 6705e26a2..a9398230d 100644 --- a/app/Plugin/LdapProvisioner/Model/CoLdapProvisionerTarget.php +++ b/app/Plugin/LdapProvisioner/Model/CoLdapProvisionerTarget.php @@ -220,6 +220,7 @@ protected function assembleAttributes($coProvisioningTargetData, $dns, $dnAttributes, $uam) { + // First see if we're working with a Group record or a Person record $person = isset($provisioningData['CoPerson']['id']); $group = isset($provisioningData['CoGroup']['id']); @@ -901,7 +902,16 @@ protected function assembleAttributes($coProvisioningTargetData, } } } - + if(!empty($provisioningData['PaperToken'])) { + foreach($provisioningData['PaperToken'] as $pt) { + if($attropts) { + $lrattr = $lattr . ";type-paper"; + $attributes[$lrattr][] = $pt['serial']; + } else { + $attributes[$attr][] = $pt['serial']; + } + } + } if(!$attropts && empty($attributes[$attr]) && !$modify) { // This is the same as the approach using $found, but without an extra variable unset($attributes[$attr]); diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css index a79e38ff8..9eae1b174 100644 --- a/app/webroot/css/co-base.css +++ b/app/webroot/css/co-base.css @@ -474,6 +474,12 @@ form#notificationStatus { font-size: 1.6rem; margin: 0 0.25em 0 0; } +#content .co-info-topbox.warn-level-a { + background-color: #fcc; +} +#content .co-info-topbox.warn-level-a .material-icons { + color: #c00; +} .material-icons.error { color: #d00; } From 2d87d4ec33688c6e15ad08007e317b7ec53ca6ab Mon Sep 17 00:00:00 2001 From: Shayna Atkinson Date: Fri, 8 Sep 2023 11:36:39 -0400 Subject: [PATCH 2/3] PR review changes; new dialog --- .../Controller/PaperTokensController.php | 33 ++++-- .../PrivacyIdeaAuthenticator/Lib/lang.php | 5 +- .../Model/PrivacyIdea.php | 14 ++- .../Model/PrivacyIdeaAuthenticator.php | 2 +- .../View/PaperTokens/generate.ctp | 100 +++++++++++------- .../View/PaperTokens/index.ctp | 2 +- app/Config/bootstrap.php | 4 +- 7 files changed, 99 insertions(+), 61 deletions(-) diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php index 1407f7b1b..fafb3cb06 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php @@ -21,7 +21,7 @@ * * @link http://www.internet2.edu/comanage COmanage Project * @package registry-plugin - * @since COmanage Registry v4.3.0 + * @since COmanage Registry v4.4.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ @@ -38,16 +38,28 @@ class PaperTokensController extends SAMController { ); /** - * Add a Standard Object. + * Add action to be used when adding a PaperToke as part of an Enrollment Flow * - * @since COmanage Registry v4.3.0 + * @since COmanage Registry v4.4.0 + */ + + public function add() { + $this->setAction('generate'); + } + + /** + * Generate a Paper Token (backup codes) + * + * @since COmanage Registry v4.4.0 */ public function generate() { - if($this->request->is('get')) { + + if(!$this->request->is('get')) { + throw new MethodNotAllowedException(); + } else { parent::add(); - //$this->set('title_for_layout', 'Generated '.$this->viewVars['vv_authenticator']['Authenticator']['description']); $this->set('title_for_layout', 'Generated Backup Codes'); if(!empty($this->request->params['named']['onFinish'])) { @@ -71,7 +83,6 @@ public function generate() { if(!empty($tokenInfo['otps'])) { $this->set('vv_otps', (array)$tokenInfo['otps']); - debug($vv_otps); } } catch(Exception $e) { @@ -83,7 +94,7 @@ public function generate() { /** * Callback before other controller methods are invoked or views are rendered. * - * @since COmanage Registry v4.3.0 + * @since COmanage Registry v4.4.0 */ public function beforeFilter() { @@ -100,7 +111,7 @@ public function beforeFilter() { * This method is intended to be overridden by model-specific controllers. * - postcondition: Session flash message updated (HTML) or HTTP status returned (REST) * - * @since COmanage Registry v4.3.0 + * @since COmanage Registry v4.4.0 * @param Array Current data * @return boolean true if dependency checks succeed, false otherwise. */ @@ -131,7 +142,7 @@ function checkDeleteDependencies($curdata) { * try{} block so that HistoryRecord->record() may be called without worrying * about catching exceptions. * - * @since COmanage Registry v4.3.0 + * @since COmanage Registry v4.4.0 * @param String Controller action causing the change * @param Array Data provided as part of the action (for add/edit) * @param Array Previous data (for delete/edit) @@ -172,7 +183,7 @@ public function generateHistory($action, $newdata, $olddata) { * - precondition: Session.Auth holds data used for authz decisions * - postcondition: $permissions set with calculated permissions * - * @since COmanage Registry v4.3.0 + * @since COmanage Registry v4.4.0 * @return Array Permissions */ @@ -195,4 +206,4 @@ function isAuthorized() { $this->set('permissions', $p); return($p[$this->action]); } -} \ No newline at end of file +} diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php index 8eb162970..f0993c7fb 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php @@ -74,5 +74,8 @@ 'pl.privacyideaauthenticator.totp.step2' => 'Then, enter the current code from the Google Authenticator app to confirm', 'pl.privacyideaauthenticator.paper.intro' => 'Use the backup codes in order, one after the other. Mark off used values.', 'pl.privacyideaauthenticator.paper.caution' => 'Backup codes are a weak second factor. Please assure no one has access to these values. Store them in a safe location', - 'pl.privacyideaauthenticator.paper.warning' => 'Before you leave this page, please confirm that you have copied your backup codes. YOU WILL NOT SEE THEM AGAIN.' + 'pl.privacyideaauthenticator.paper.warning' => 'Before you leave this page, please confirm that you have copied your backup codes. YOU WILL NOT SEE THEM AGAIN.', + 'pl.privacyideaauthenticator.paper.dialog' => 'Before you leave this page you must save your backup codes by copying or printing.', + 'pl.privacyideaauthenticator.paper.dialog.btn' => 'I understand', + 'pl.privacyideaauthenticator.paper.continue' => 'Once you have copied your backup codes, you must continue to the next step', ); diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php index ffbffff0d..074c72562 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php @@ -204,25 +204,23 @@ public function createToken($privacyIdeaAuthenticator, $coPersonId) { case PrivacyIDEATokenTypeEnum::TOTP: $token['confirmed'] = false; $TotpToken = new TotpToken(); - $TotpToken->save($token); - + if (!$TotpToken->save($token)) { + throw new RuntimeException(_txt('er.db.save-a', array('TotpToken'))); + } // We don't persist the QR Data, but we do need to return it for rendering $token['qr_data'] = $jresponse->detail->googleurl->img; break; case PrivacyIDEATokenTypeEnum::Paper: $PaperToken = new PaperToken(); - $PaperToken->save($token); - + if (!$PaperToken->save($token)) { + throw new RuntimeException(_txt('er.db.save-a', array('PaperToken'))); + } // We don't persist the codes themselves but need to present them to the user for copying/printing $token['otps'] = (array)$jresponse->detail->otps; break; } - if(!$jresponse->result->status) { - throw new RuntimeException($jresponse->result->error->message); - } - return $token; } diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php index 1819cdca3..b3dcaa220 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php @@ -147,7 +147,7 @@ public function current($id, $backendId, $coPersonId) { $results = $this->TotpToken->find('all', $args); if(empty($results)) { - unset($args); + $args = array(); $args['conditions']['PaperToken.co_person_id'] = $coPersonId; $args['conditions']['PaperToken.privacy_idea_authenticator_id'] = $backendId; $args['contain'] = false; diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp index 06fa5d16f..5fbae8210 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp @@ -21,7 +21,7 @@ * * @link http://www.internet2.edu/comanage COmanage Project * @package registry-plugin - * @since COmanage Registry v4.3.0 + * @since COmanage Registry v4.4.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ --> @@ -55,6 +55,9 @@
warning + request->params['named']['onFinish'])): ?> + +
-
- - +
+
+ - - - - - - $otp): ?> - - - - - - - -
#OTP
-
- + # + OTP + + + + $otp): ?> + + + + + + + + + request->params['named']['onFinish'])): ?> + Html->link(_txt('op.cont'), + urldecode($this->request->params['named']['onFinish']), + array('class' => 'btn btn-primary btn-lg')); + ?> + + + + +
+

+ + +

+
action == 'view'): ?> - + + diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp index b6cc885c9..5cdd62824 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp @@ -21,7 +21,7 @@ * * @link http://www.internet2.edu/comanage COmanage Project * @package registry-plugin - * @since COmanage Registry v4.3.0 + * @since COmanage Registry v4.4.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ diff --git a/app/Config/bootstrap.php b/app/Config/bootstrap.php index 81878787b..a3b093e52 100644 --- a/app/Config/bootstrap.php +++ b/app/Config/bootstrap.php @@ -128,12 +128,12 @@ */ App::uses('CakeLog', 'Log'); CakeLog::config('debug', array( - 'engine' => 'FileLog', + 'engine' => 'ConsoleLog', 'types' => array('notice', 'info', 'debug'), 'file' => 'debug', )); CakeLog::config('error', array( - 'engine' => 'FileLog', + 'engine' => 'ConsoleLog', 'types' => array('warning', 'error', 'critical', 'alert', 'emergency'), 'file' => 'error', )); From f533476093b4f07cfb577c6fb6c4b183760a92bb Mon Sep 17 00:00:00 2001 From: Shayna Atkinson Date: Tue, 31 Oct 2023 19:23:44 -0400 Subject: [PATCH 3/3] only allow one set up backup codes --- .../Controller/PaperTokensController.php | 56 ++++++++++++------- .../PrivacyIdeaAuthenticator/Lib/lang.php | 1 + .../View/PaperTokens/generate.ctp | 7 ++- .../View/PaperTokens/index.ctp | 2 +- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php index fafb3cb06..b4fc2edd5 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php @@ -58,35 +58,51 @@ public function generate() { if(!$this->request->is('get')) { throw new MethodNotAllowedException(); } else { - parent::add(); - $this->set('title_for_layout', 'Generated Backup Codes'); - if(!empty($this->request->params['named']['onFinish'])) { - $this->set('vv_on_finish_url', $this->request->params['named']['onFinish']); - } + $args = array(); + $args['conditions']['CoPerson.id'] = $this->viewVars['vv_co_person']['CoPerson']['id']; + + $ppt = $this->PaperToken->find('first', $args); + + + if($ppt) { + $this->set('title_for_layout', 'Token already Exists'); + $this->Flash->set(_txt('er.privacyideaauthenticator.singular'), array('key' => 'error')); + if(!empty($this->request->params['named']['onFinish'])) { + $this->set('vv_on_finish_url', $this->request->params['named']['onFinish']); + } + } else { + parent::add(); - try { - $tokenInfo = $this->PrivacyIdea->createToken($this->viewVars['vv_authenticator']['PrivacyIdeaAuthenticator'], + $this->set('title_for_layout', 'Generated Backup Codes'); + + if(!empty($this->request->params['named']['onFinish'])) { + $this->set('vv_on_finish_url', $this->request->params['named']['onFinish']); + } + + try { + $tokenInfo = $this->PrivacyIdea->createToken($this->viewVars['vv_authenticator']['PrivacyIdeaAuthenticator'], $this->viewVars['vv_co_person']['CoPerson']['id']); - $this->set('vv_token_info', $tokenInfo); + $this->set('vv_token_info', $tokenInfo); - $newdata = array( - 'PaperToken' => array( - 'co_person_id' => $this->viewVars['vv_co_person']['CoPerson']['id'], - 'serial' => $tokenInfo['serial'] - ) - ); + $newdata = array( + 'PaperToken' => array( + 'co_person_id' => $this->viewVars['vv_co_person']['CoPerson']['id'], + 'serial' => $tokenInfo['serial'] + ) + ); - $this->generateHistory('generate', $newdata, array()); + $this->generateHistory('generate', $newdata, array()); - if(!empty($tokenInfo['otps'])) { - $this->set('vv_otps', (array)$tokenInfo['otps']); + if(!empty($tokenInfo['otps'])) { + $this->set('vv_otps', (array)$tokenInfo['otps']); + } + } + catch(Exception $e) { + $this->Flash->set($e->getMessage(), array('key' => 'error')); } - } - catch(Exception $e) { - $this->Flash->set($e->getMessage(), array('key' => 'error')); } } } diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php index f0993c7fb..f03096910 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php @@ -58,6 +58,7 @@ // Error messages 'er.privacyideaauthenticator.code' => 'Invalid code, please try again', 'er.privacyideaauthenticator.identifier' => 'No Identifier of type "%1$s" found for CO Person', + 'er.privacyideaauthenticator.singular' => 'A token already exists; please delete that one before generating a new one', // Plugin texts 'pl.privacyideaauthenticator.alt.google' => 'QR Code for Google Authenticator', diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp index 5fbae8210..95f7b9ce3 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp @@ -38,7 +38,6 @@ urldecode($this->request->params['named']['onFinish']), array('class' => 'forwardbutton')); } - print $this->element("pageTitleAndButtons", $params); // Add breadcrumbs print $this->element("coCrumb", array('authenticator' => 'PrivacyIdea')); @@ -112,6 +111,12 @@

+request->params['named']['onFinish'])): ?> + Html->link(_txt('op.cont'), + urldecode($this->request->params['named']['onFinish']), + array('class' => 'btn btn-primary btn-lg')); + ?> action == 'view'): ?>
  • diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp index 5cdd62824..3e9a16cd7 100644 --- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp +++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp @@ -35,7 +35,7 @@ // Add top links $params['topLinks'] = array(); - if($permissions['add']) { + if($permissions['add'] && !$paper_tokens) { $params['topLinks'][] = $this->Html->link( _txt('op.generate', array($title_for_layout)), array(