Allow namespaces to be between 2 and 255 characters in length

[Delivers #137924329]
This commit is contained in:
Joseph Schorr 2017-01-18 17:42:27 -05:00
parent e2748fccd9
commit 7c7a07fb5a
8 changed files with 43 additions and 39 deletions

View file

@ -9,14 +9,12 @@
<form class="form-signup" name="signupForm" ng-submit="register()" ng-show="!awaitingConfirmation && !registering"> <form class="form-signup" name="signupForm" ng-submit="register()" ng-show="!awaitingConfirmation && !registering">
<label for="username">Username:</label> <label for="username">Username:</label>
<span class="namespace-input" binding="newUser.username" is-back-incompat="isBackIncompat" namespace-title="Requested username"></span> <span class="namespace-input" binding="newUser.username" back-incompat-message="backIncompatMessage" namespace-title="Requested username"></span>
<div class="expandable" ng-class="{'expanded': isBackIncompat || (!signupForm.namespaceField.$error.required && signupForm.namespaceField.$invalid)}"> <div class="expandable" ng-class="{'expanded': backIncompatMessage || (!signupForm.namespaceField.$error.required && signupForm.namespaceField.$invalid)}">
<div class="co-alert co-alert-warning thin" ng-show="isBackIncompat"> <div class="co-alert co-alert-warning thin" ng-show="backIncompatMessage">{{ backIncompatMessage }}</div>
Usernames with dots or dashes are incompatible with Docker version 1.8 or older
</div>
<div class="co-alert co-alert-danger thin" ng-show="!signupForm.namespaceField.$error.required && signupForm.namespaceField.$invalid"> <div class="co-alert co-alert-danger thin" ng-show="!signupForm.namespaceField.$error.required && signupForm.namespaceField.$invalid">
Usernames must be alphanumeric and between four and thirty characters in length Usernames must be alphanumeric and between 2 and 255 characters in length
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
var TEAM_PATTERN = '^[a-z][a-z0-9]+$'; var TEAM_PATTERN = '^[a-z][a-z0-9]+$';
var ROBOT_PATTERN = '^[a-z][a-z0-9_]{3,29}$'; var ROBOT_PATTERN = '^[a-z][a-z0-9_]{1,254}$';
var USERNAME_PATTERN = '^(?=.{4,30}$)([a-z0-9]+(?:[._-][a-z0-9]+)*)$'; var USERNAME_PATTERN = '^(?=.{2,255}$)([a-z0-9]+(?:[._-][a-z0-9]+)*)$';
// Define the pages module. // Define the pages module.
quayPages = angular.module('quayPages', [], function(){}); quayPages = angular.module('quayPages', [], function(){});

View file

@ -10,7 +10,7 @@ angular.module('quay').directive('namespaceInput', function () {
restrict: 'C', restrict: 'C',
scope: { scope: {
'binding': '=binding', 'binding': '=binding',
'isBackIncompat': '=isBackIncompat', 'backIncompatMessage': '=backIncompatMessage',
'hasExternalError': '=?hasExternalError', 'hasExternalError': '=?hasExternalError',
'namespaceTitle': '@namespaceTitle', 'namespaceTitle': '@namespaceTitle',
@ -21,11 +21,17 @@ angular.module('quay').directive('namespaceInput', function () {
$scope.$watch('binding', function(binding) { $scope.$watch('binding', function(binding) {
if (!binding) { if (!binding) {
$scope.isBackIncompat = false; $scope.backIncompatMessage = null;
return; return;
} }
$scope.isBackIncompat = (binding.indexOf('-') > 0 || binding.indexOf('.') > 0); if (binding.indexOf('-') > 0 || binding.indexOf('.') > 0) {
$scope.backIncompatMessage = 'Namespaces with dashes or dots are only compatible with Docker 1.9+';
} else if (binding.length < 4 || binding.length > 30) {
$scope.backIncompatMessage = 'Namespaces less than 4 or more than 30 characters are only compatible with Docker 1.6+';
} else {
$scope.backIncompatMessage = null;
}
}) })
} }
}; };

View file

@ -42,16 +42,14 @@
<label for="orgName">Organization Name</label> <label for="orgName">Organization Name</label>
<div class="field-row"> <div class="field-row">
<span class="field-container"> <span class="field-container">
<span class="namespace-input" binding="org.name" is-back-incompat="isBackIncompat" namespace-title="Organization name"></span> <span class="namespace-input" binding="org.name" back-incompat-message="backIncompatMessage" namespace-title="Organization name"></span>
</span>
<span class="co-alert co-alert-warning thin" ng-show="isBackIncompat">
Organization names with dots or dashes are incompatible with Docker version 1.8 or older
</span> </span>
<span class="co-alert co-alert-warning thin" ng-show="backIncompatMessage">{{ backIncompatMessage }}</span>
<span class="co-alert co-alert-danger thin" ng-show="!newOrgForm.namespaceField.$error.required && newOrgForm.namespaceField.$invalid"> <span class="co-alert co-alert-danger thin" ng-show="!newOrgForm.namespaceField.$error.required && newOrgForm.namespaceField.$invalid">
Organization names must be alphanumeric, be at least four characters in length and max thirty characters in length Organization names must be alphanumeric, be at least 2 characters in length and max 255 characters in length
</span> </span>
</div> </div>
<span class="description">This will also be the namespace for your repositories. Must be alphanumeric, all lowercase, at least four characters long and at most thirty characters long.</span> <span class="description">This will also be the namespace for your repositories. Must be alphanumeric, all lowercase, at least 2 characters long and at most 255 characters long.</span>
</div> </div>
<div class="form-group nested" quay-require="['MAILING']"> <div class="form-group nested" quay-require="['MAILING']">

View file

@ -9,7 +9,7 @@
</p> </p>
<p>Please confirm the selected username or enter a different username below:</p> <p>Please confirm the selected username or enter a different username below:</p>
<form name="usernameForm" ng-submit="updateUser({'username': username})"> <form name="usernameForm" ng-submit="updateUser({'username': username})">
<div class="namespace-input" binding="username" is-back-incompat="isBackIncompat" <div class="namespace-input" binding="username" back-incompat-message="backIncompatMessage"
namespace-title="Username" style="margin-bottom: 20px;" namespace-title="Username" style="margin-bottom: 20px;"
has-external-error="state == 'existing'"></div> has-external-error="state == 'existing'"></div>
@ -17,7 +17,7 @@
ng-disabled="usernameForm.$invalid || state != 'confirmed'" ng-disabled="usernameForm.$invalid || state != 'confirmed'"
value="Confirm Username"> value="Confirm Username">
<span class="cor-loader-inline" ng-show="state == 'confirming'"></span> <span class="cor-loader-inline" ng-show="state == 'confirming'"></span>
<span class="username-status" ng-show="state == 'confirmed' && !isBackIncompat"> <span class="username-status" ng-show="state == 'confirmed' && !backIncompatMessage">
<i class="fa fa-check-circle"></i> Username valid <i class="fa fa-check-circle"></i> Username valid
</span> </span>
<span class="username-status" ng-show="state == 'existing'"> <span class="username-status" ng-show="state == 'existing'">
@ -29,8 +29,8 @@
<span class="username-status" ng-show="state == 'editing' && usernameForm.$invalid"> <span class="username-status" ng-show="state == 'editing' && usernameForm.$invalid">
Usernames must be alphanumeric and be at least four characters in length Usernames must be alphanumeric and be at least four characters in length
</span> </span>
<span class="username-status" ng-show="state == 'confirmed' && isBackIncompat"> <span class="username-status" ng-show="state == 'confirmed' && backIncompatMessage">
<i class="fa fa-exclamation-triangle"></i> Note: Usernames with dots or dashes are incompatible with Docker version 1.8 or older <i class="fa fa-exclamation-triangle"></i>{{ backIncompatMessage }}
</span> </span>
</form> </form>
</div> </div>

View file

@ -651,7 +651,7 @@ class TestCreateNewUser(ApiTestCase):
email='test@example.com'), email='test@example.com'),
expected_code=400) expected_code=400)
self.assertEquals('Invalid namespace a: Namespace must be between 4 and 30 characters in length', self.assertEquals('Invalid namespace a: Namespace must be between 2 and 255 characters in length',
json['detail']) json['detail'])
def test_trycreateregexmismatch(self): def test_trycreateregexmismatch(self):

View file

@ -20,6 +20,8 @@ class TestUsernameValidation(unittest.TestCase):
self.assertFalse(result) self.assertFalse(result)
def test_valid(self): def test_valid(self):
self.assertValid('ja')
self.assertValid('jak')
self.assertValid('jake') self.assertValid('jake')
self.assertValid('ja_ke') self.assertValid('ja_ke')
self.assertValid('te-st') self.assertValid('te-st')
@ -27,14 +29,13 @@ class TestUsernameValidation(unittest.TestCase):
def test_properlength(self): def test_properlength(self):
self.assertValid('z' * 30) self.assertValid('z' * 30)
self.assertValid('z' * 255)
def test_tooshort(self): def test_tooshort(self):
self.assertInvalid('j') self.assertInvalid('j')
self.assertInvalid('ja')
self.assertInvalid('jk')
def test_toolong(self): def test_toolong(self):
self.assertInvalid('z' * 31) self.assertInvalid('z' * 256)
def test_invalids(self): def test_invalids(self):
self.assertInvalid('_test') self.assertInvalid('_test')
@ -158,20 +159,21 @@ class TestUsernameGenerator(unittest.TestCase):
self.assert_generated_output('ja___ke', 'ja_ke') self.assert_generated_output('ja___ke', 'ja_ke')
def test_trailing_underscores(self): def test_trailing_underscores(self):
self.assert_generated_output('ja__', 'ja00') self.assert_generated_output('ja__', 'ja')
self.assert_generated_output('jake__', 'jake') self.assert_generated_output('jake__', 'jake')
def test_starting_underscore(self): def test_starting_underscore(self):
self.assert_generated_output('_jake', 'jake') self.assert_generated_output('_jake', 'jake')
def test_short_names(self): def test_short_names(self):
self.assert_generated_output('a', 'a000') self.assert_generated_output('a', 'a0')
self.assert_generated_output('ab', 'ab00') self.assert_generated_output('ab', 'ab')
self.assert_generated_output('abc', 'abc0') self.assert_generated_output('abc', 'abc')
def test_long_names(self): def test_long_names(self):
self.assert_generated_output('abcdefghijklmnopqrstuvwxyz1234567890', self.assert_generated_output('abcdefghijklmnopqrstuvwxyz1234567890',
'abcdefghijklmnopqrstuvwxyz1234') 'abcdefghijklmnopqrstuvwxyz1234567890')
self.assert_generated_output('c' * 256, 'c' * 255)
def test_unicode_transliteration(self): def test_unicode_transliteration(self):
self.assert_generated_output(u'\xc6neid', 'aeneid') self.assert_generated_output(u'\xc6neid', 'aeneid')
@ -184,18 +186,18 @@ class TestUsernameGenerator(unittest.TestCase):
self.assert_generated_output(u'\u0985\u09ad\u09bf\u099c\u09c0\u09a4', 'abhijiit') self.assert_generated_output(u'\u0985\u09ad\u09bf\u099c\u09c0\u09a4', 'abhijiit')
self.assert_generated_output(u'\u0d05\u0d2d\u0d3f\u0d1c\u0d40\u0d24', 'abhijiit') self.assert_generated_output(u'\u0d05\u0d2d\u0d3f\u0d1c\u0d40\u0d24', 'abhijiit')
self.assert_generated_output(u'\u0d2e\u0d32\u0d2f\u0d3e\u0d32\u0d2e\u0d4d', 'mlyaalm') self.assert_generated_output(u'\u0d2e\u0d32\u0d2f\u0d3e\u0d32\u0d2e\u0d4d', 'mlyaalm')
self.assert_generated_output(u'\ue000', '0000') self.assert_generated_output(u'\ue000', '00')
self.assert_generated_output(u'\u03ff', '0000') self.assert_generated_output(u'\u03ff', '00')
self.assert_generated_output(u'\u0d2e\u0d32\u03ff\u03ff\u0d2e\u0d32', 'mlml') self.assert_generated_output(u'\u0d2e\u0d32\u03ff\u03ff\u0d2e\u0d32', 'mlml')
def test_multiple_suggestions(self): def test_multiple_suggestions(self):
name_gen = generate_valid_usernames('a') name_gen = generate_valid_usernames('a')
generated_output = list(islice(name_gen, 4)) generated_output = list(islice(name_gen, 4))
self.assertEquals('a000', generated_output[0]) self.assertEquals('a0', generated_output[0])
self.assertEquals('a001', generated_output[1]) self.assertEquals('a1', generated_output[1])
self.assertEquals('a002', generated_output[2]) self.assertEquals('a2', generated_output[2])
self.assertEquals('a003', generated_output[3]) self.assertEquals('a3', generated_output[3])
class TestDockerVersionParsing(unittest.TestCase): class TestDockerVersionParsing(unittest.TestCase):

View file

@ -9,8 +9,8 @@ INVALID_PASSWORD_MESSAGE = 'Invalid password, password must be at least ' + \
'8 characters and contain no whitespace.' '8 characters and contain no whitespace.'
VALID_CHARACTERS = string.digits + string.lowercase VALID_CHARACTERS = string.digits + string.lowercase
MIN_USERNAME_LENGTH = 4 MIN_USERNAME_LENGTH = 2
MAX_USERNAME_LENGTH = 30 MAX_USERNAME_LENGTH = 255
VALID_LABEL_KEY_REGEX = r'^[a-z0-9](([a-z0-9]|[-.](?![.-]))*[a-z0-9])?$' VALID_LABEL_KEY_REGEX = r'^[a-z0-9](([a-z0-9]|[-.](?![.-]))*[a-z0-9])?$'
VALID_USERNAME_REGEX = r'^([a-z0-9]+(?:[._-][a-z0-9]+)*)$' VALID_USERNAME_REGEX = r'^([a-z0-9]+(?:[._-][a-z0-9]+)*)$'
@ -63,7 +63,7 @@ def _gen_filler_chars(num_filler_chars):
def generate_valid_usernames(input_username): def generate_valid_usernames(input_username):
normalized = input_username.encode('unidecode', 'ignore').strip().lower() normalized = input_username.encode('unidecode', 'ignore').strip().lower()
prefix = re.sub(INVALID_USERNAME_CHARACTERS, '_', normalized)[:30] prefix = re.sub(INVALID_USERNAME_CHARACTERS, '_', normalized)[:MAX_USERNAME_LENGTH]
prefix = re.sub(r'_{2,}', '_', prefix) prefix = re.sub(r'_{2,}', '_', prefix)
if prefix.endswith('_'): if prefix.endswith('_'):