Merge "Validate Project and Project Groups names"

This commit is contained in:
Jenkins
2014-06-24 17:28:54 +00:00
committed by Gerrit Code Review
6 changed files with 171 additions and 10 deletions

View File

@@ -23,6 +23,7 @@ import wsmeext.pecan as wsme_pecan
import storyboard.api.auth.authorization_checks as checks
from storyboard.api.v1 import base
from storyboard.api.v1.projects import Project
from storyboard.common.custom_types import NameType
from storyboard.db.api import project_groups
@@ -32,9 +33,10 @@ CONF = cfg.CONF
class ProjectGroup(base.APIBase):
"""Represents a group of projects."""
name = wtypes.text
"""A unique name, used in URLs, identifying the project group. All
lowercase, no special characters. Examples: infra, compute.
name = NameType()
"""The Project Group unique name. This name will be displayed in the URL.
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
separators.
"""
title = wtypes.text

View File

@@ -25,6 +25,7 @@ import wsmeext.pecan as wsme_pecan
from storyboard.api.auth import authorization_checks as checks
from storyboard.api.v1 import base
from storyboard.common.custom_types import NameType
from storyboard.db.api import projects as projects_api
CONF = cfg.CONF
@@ -37,9 +38,10 @@ class Project(base.APIBase):
Storyboard as Projects, among others.
"""
name = wtypes.text
"""At least one lowercase letter or number, followed by letters, numbers,
dots, hyphens or pluses. Keep this name short; it is used in URLs.
name = NameType()
"""The Project unique name. This name will be displayed in the URL.
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
separators.
"""
description = wtypes.text

View File

@@ -0,0 +1,31 @@
# Copyright (c) 2014 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from wsme import types
class NameType(types.StringType):
"""This type should be applied to the name fields. Currently this type
should be applied to Projects and Project Groups.
This type allows alphanumeric characters with . - and / separators inside
the name. The name should be at least 3 symbols long.
"""
_name_regex = r'^[a-zA-Z0-9]+([\-\./]?[a-zA-Z0-9]+)*$'
def __init__(self):
super(NameType, self).__init__(min_length=3, pattern=self._name_regex)

View File

@@ -0,0 +1,80 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Existing projects should be renamed if they do not pass a new validation.
The previous name will be appended to the description so that it can be
restored.
Revision ID: 020
Revises: 019
Create Date: 2014-06-23 12:50:43.924601
"""
# revision identifiers, used by Alembic.
revision = '020'
down_revision = '019'
from alembic import op
import sqlalchemy as sa
from sqlalchemy import MetaData
from sqlalchemy.sql.expression import table
from storyboard.common.custom_types import NameType
def upgrade(active_plugins=None, options=None):
bind = op.get_bind()
validator = NameType()
projects = list(bind.execute(
sa.select(columns=['*'], from_obj=sa.Table('projects', MetaData()))))
projects_table = table(
'projects',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('description', sa.UnicodeText(), nullable=True),
)
last_idx = 0
for project in projects:
project_name = project["name"]
project_id = project["id"]
need_rename = False
try:
validator.validate(project_name)
except Exception:
need_rename = True
if need_rename:
# This project needs renaming
temp_name = "Project-%d" % last_idx
last_idx += 1
updated_description = "%s This project was renamed to fit new " \
"naming validation. Original name was: %s" \
% (project["description"], project_name)
bind.execute(projects_table.update()
.where(projects_table.c.id == project_id)
.values(name=temp_name,
description=updated_description))
def downgrade(active_plugins=None, options=None):
# No way back for invalid names
pass

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import six
import warnings
import yaml
@@ -21,12 +22,14 @@ from oslo.config import cfg
from sqlalchemy.exc import SADeprecationWarning
from storyboard.db.api import base as db_api
from storyboard.common.custom_types import NameType
from storyboard.db.models import Project
from storyboard.db.models import ProjectGroup
warnings.simplefilter("ignore", SADeprecationWarning)
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def do_load_models(filename):
@@ -34,20 +37,34 @@ def do_load_models(filename):
config_file = open(filename)
projects_list = yaml.load(config_file)
validator = NameType()
project_groups = dict()
for project in projects_list:
if not project.get('use-storyboard'):
continue
group_name = project.get("group") or "default"
if group_name not in project_groups:
project_groups[group_name] = list()
project_name = project.get("project")
try:
validator.validate(project_name)
except Exception:
# Skipping invalid project names
LOG.warn("Project %s was not loaded. Validation failed."
% project_name)
continue
project_description = project.get("description")
project_groups[group_name].append({"name": project_name,
"description": project_description})
project_groups[group_name].append(
{"name": project_name,
"description": project_description})
session = db_api.get_session()

View File

@@ -14,6 +14,8 @@
import json
from webtest.app import AppError
from storyboard.tests import base
@@ -25,7 +27,7 @@ class TestProjects(base.FunctionalTest):
self.resource = '/projects'
self.project_01 = {
'name': 'test_project',
'name': 'test-project',
'description': 'some description'
}
@@ -42,13 +44,21 @@ class TestProjects(base.FunctionalTest):
self.assertEqual(self.project_01['description'],
project['description'])
def test_create_invalid(self):
invalid_project = self.project_01.copy()
invalid_project["name"] = "name with spaces"
self.assertRaises(AppError, self.post_json, self.resource,
invalid_project)
def test_update(self):
response = self.post_json(self.resource, self.project_01)
original = json.loads(response.body)
delta = {
'id': original['id'],
'name': 'new name',
'name': 'new-name',
'description': 'new description'
}
@@ -62,3 +72,22 @@ class TestProjects(base.FunctionalTest):
self.assertNotEqual(original['name'], updated['name'])
self.assertNotEqual(original['description'],
updated['description'])
def test_update_invalid(self):
response = self.post_json(self.resource, self.project_01)
original = json.loads(response.body)
delta = {
'id': original['id'],
'name': 'new-name is invalid!',
}
url = "/projects/%d" % original['id']
# check for invalid characters like space and '!'
self.assertRaises(AppError, self.put_json, url, delta)
delta["name"] = "a"
# check for a too short name
self.assertRaises(AppError, self.put_json, url, delta)