Skip to content

CFM-166_REST_API_username_prefix_should_not_be_editable #23

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions app/resources/locales/en_US/error.po
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@
msgid "api.object"
msgstr "Did not find \"{0}\" object in request"

msgid "api.username.prefix"
msgstr "API username must begin with \"{0}\""

msgid "api.username.suffix"
msgstr "API username requires a suffix"

msgid "auth.api.expired"
msgstr "API User \"{0}\" has expired"

Expand Down Expand Up @@ -108,6 +102,9 @@ msgstr "Invalid character found"
msgid "input.invalid.email"
msgstr "The provided value is not a valid email address"

msgid "input.invalid.prefix"
Ioannis marked this conversation as resolved.
Show resolved Hide resolved
msgstr "The provided value is not valid. \"{0}\" prefix is required"

msgid "input.invalid.url"
msgstr "The provided value is not a valid URL"

Expand Down
54 changes: 46 additions & 8 deletions app/src/Lib/Traits/ValidationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
namespace App\Lib\Traits;

use Cake\Core\Configure;
use Cake\Database\Schema\TableSchemaInterface;
use Cake\ORM\TableRegistry;
use Cake\Validation\Validator;

Expand Down Expand Up @@ -67,20 +68,34 @@ public function registerPrimaryKeyValidation(Validator $validator, array $primar
* Register validation rules for the provided field, as a string.
*
* @since COmanage Registry v5.0.0
* @param Validator $validator Cake Validator
* @param Schema $schema Cake Schema
* @param string $field Field name
* @param bool $required Whether this field is required
* @param Validator $validator Cake Validator
* @param TableSchemaInterface $schema Cake Schema
* @param string $field Field name
* @param bool $required Whether this field is required
* @return Validator Cake Validator
*/

public function registerStringValidation(Validator $validator, $schema, string $field, bool $required): Validator {
$validator->add($field, [
public function registerStringValidation(Validator $validator,
TableSchemaInterface $schema,
string $field,
bool $required,
string $prefix = ''): Validator {
$rules = [
'size' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn($field)]],
'provider' => 'table'],
'filter' => ['rule' => ['validateInput'],
'provider' => 'table']
]);
];

if(!empty($prefix)) {
$rules['prefix'] = [
'rule' => ['validatePrefix'],
'pass' => [$prefix],
'provider' => 'table'
];
}

$validator->add($field, $rules);

if($required) {
$validator->notEmptyString($field);
Expand Down Expand Up @@ -301,4 +316,27 @@ public function validateTimeZone($value, array $context) {

return true;
}
}

/**
* Determine if a string submitted from a form has a valid prefix.
*
* @since COmanage Registry v5.0.0
* @param string $value Value to validate
* @param string $prefix Prefix value
* @param array $context Validation context
* @return mixed True if $value validates, or an error string otherwise
*/

public function validatePrefix(string $value, string $prefix, array $context) {
$coid = $context['data']['co_id'] ?? '';
if($prefix === "co_id") {
$prefix = !empty($coid) ? "co_" . $coid . "." : "";
}

if (!preg_match('/^' . $prefix . '(?:.*)/m', $value)) {
return __d('error', 'input.invalid.prefix', [$prefix]);
}

return true;
}
}
83 changes: 27 additions & 56 deletions app/src/Model/Table/ApiUsersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@

namespace App\Model\Table;

use \Cake\Auth\FallbackPasswordHasher;
use \Cake\Chronos\Chronos;
use \Cake\ORM\Query;
use \Cake\ORM\RulesChecker;
use \Cake\ORM\Table;
use \Cake\ORM\TableRegistry;
use \Cake\Validation\Validator;
use \App\Lib\Enum\SuspendableStatusEnum;
use \App\Lib\Random\RandomString;
use ArrayObject;
use Cake\Auth\FallbackPasswordHasher;
use Cake\Chronos\Chronos;
use Cake\Event\EventInterface;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Validation\Validator;
use App\Lib\Enum\SuspendableStatusEnum;
use App\Lib\Random\RandomString;

class ApiUsersTable extends Table {
use \App\Lib\Traits\AutoViewVarsTrait;
Expand Down Expand Up @@ -94,6 +95,22 @@ public function initialize(array $config): void {
]
]);
}

/**
* Add namespace prefix to username
*
* @since COmanage Registry v5.0.0
* @param EventInterface $event beforeMarshal event
* @param ArrayObject $data Entity data
* @param ArrayObject $options Callback options
*/

public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
{
if (isset($data['username'])) {
$data['username'] = "co_" . $data['co_id'] . "." . $data['username'];
}
}

/**
* Define business rules to supplement the default trait implementation.
Expand All @@ -109,13 +126,6 @@ public function buildTableRules(RulesChecker $rules): RulesChecker {
// rule building.

$rules->add(function($entity, $options) use($rules) {
// AR-ApiUser-3 For namespacing purposes, API Users are named with a prefix consisting of the string "co_#.".
$ret = $this->ruleIsUsernameValid($entity, $options);

if($ret !== true) {
// Return the error message
return $ret;
}

// AR-ApiUser-3 API usernames must be unique across the entire platform.
$rule = $rules->isUnique(['username'], __d('error', 'exists', [__d('controller', 'ApiUsers', [1])]));
Expand Down Expand Up @@ -175,45 +185,6 @@ public function getUserPrivilege(string $username) {

return false;
}

/**
* Application Rule to determine if the current entity username is valid.
*
* @since COmanage Registry v5.0.0
* @param Entity $entity Entity to be validated
* @param array $options Application rule options
* @return boolean true if the Rule check passes, false otherwise
*/

public function ruleIsUsernameValid($entity, $options) {
// We need to pull the CO data to check the name

if(!$entity->co_id) {
return __d('error', 'coid');
}

$Cos = TableRegistry::getTableLocator()->get('Cos');

$co = $Cos->get($entity->co_id);

if(!$co) {
return __d('error', 'notfound', [__d('controller', 'cos', [1])]);
}

$prefix = "co_" . $co->id . ".";

// Return false if the prefix doesn't match the CO ID
if(strncmp($entity->username, $prefix, strlen($prefix))) {
return __d('error', 'api.username.prefix', [$prefix]);
}

// Or if there's nothing after the dot
if(strlen($entity->username) == strlen($prefix)) {
return __d('error', 'api.username.suffix');
}

return true;
}

/**
* Validate an API Key.
Expand Down Expand Up @@ -309,7 +280,7 @@ public function validationDefault(Validator $validator): Validator {
]);
$validator->notEmptyString('co_id');

$this->registerStringValidation($validator, $schema, 'username', true);
$this->registerStringValidation($validator, $schema, 'username', true, 'co_id');

$validator->add('api_key', [
'length' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn('api_key')]],
Expand Down
40 changes: 37 additions & 3 deletions app/src/View/Helper/FieldHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ public function banner(string $info) {
* @param string $fieldName Form field
* @param array $options FormHelper control options
* @param string $labelText Label text (fieldName language key used by default)
* @param array $config Custom FormHelper configuration options
* @return string HTML for control
*/

public function control(string $fieldName,
array $options=[],
string $labelText=null) {
string $labelText=null,
array $config=[]){
$coptions = $options;
$coptions['label'] = false;
$coptions['readonly'] = !$this->editable || (isset($options['readonly']) && $options['readonly']);
Expand All @@ -84,7 +86,16 @@ public function control(string $fieldName,

// Generate HTML for the control itself
$liClass = "";


// Remove prefix from field value
if(isset($config['prefix'], $this->getView()->get('vv_obj')->$fieldName)) {
$vv_obj = $this->getView()->get('vv_obj');
$fieldValue = $vv_obj->$fieldName;
$fieldValueTemp = str_replace($config['prefix'], '', $fieldValue);
$vv_obj->$fieldName = $fieldValueTemp;
$this->getView()->set('vv_obj', $vv_obj);
}

// Handle datetime controls specially
if($fieldName == 'valid_from' || $fieldName == 'valid_through') {
// Append the timezone to the label
Expand Down Expand Up @@ -198,7 +209,9 @@ public function control(string $fieldName,

return $this->startLine($liClass)
. $this->formNameDiv($fieldName, $labelText)
. $this->formInfoDiv($controlCode)
. ( !empty($config['prefix']) ?
$this->formInfoWithPrefixDiv($controlCode, $config['prefix']) :
$this->formInfoDiv($controlCode) )
. $this->endLine();
}

Expand Down Expand Up @@ -239,6 +252,27 @@ protected function formInfoDiv(string $content) {
' . $content . '
</div>';
}

/**
* Generate a form info (control, value) box with a non editable prefix.
*
* @since COmanage Registry v5.0.0
* @param string $content Content HTML
* @param string $prefix Prefix value
* @return string Form Info HTML
*/

protected function formInfoWithPrefixDiv(string $context, string $prefix) {
$div = '<div class="field-info">' . PHP_EOL
. '<div class="input-group mb-3">' . PHP_EOL
. '<div class="input-group-prepend">' . PHP_EOL
. '<span class="input-group-text" id="basic-addon3">' . $prefix . '</span>'
. '</div>' . PHP_EOL
. $context
. '</div></div>';

return $div;
}

/**
* Generate a form name (label, description) box.
Expand Down
2 changes: 1 addition & 1 deletion app/templates/ApiUsers/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ if($vv_action == 'add' || $vv_action == 'edit') {
}

// AR-ApiUser-3 For namespacing purposes, API Users are named with a prefix consisting of the string "co_#.".
print $this->Field->control('username', ['default' => "co_" . $vv_cur_co->id .'.']);
print $this->Field->control('username', [], null, ['prefix' => "co_" . $vv_cur_co->id .'.']);

// We link to the "Generate" button on edit only
$generateLink = [];
Expand Down
12 changes: 12 additions & 0 deletions app/webroot/css/co-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,18 @@ ul.fields li.fields-datepicker .field-info {
display: flex;
align-items: center;
}
.field-info .input-group {
flex-wrap: unset;
}
.field-info .input-group .input.text {
width: inherit;
}
.field-info .input-group .input-group-text{
border-radius: unset;
font-size: inherit;
line-height: inherit;
padding: .25rem .75rem;
}
.cm-time-picker-panel {
position: absolute;
z-index: 100;
Expand Down