~ubuntu-branches/ubuntu/saucy/glance/saucy-proposed

1.1.49 by Chuck Short
Import upstream version 2013.1.g3
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3
# Copyright 2013 OpenStack LLC.
4
# All Rights Reserved.
5
#
6
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
7
#    not use this file except in compliance with the License. You may obtain
8
#    a copy of the License at
9
#
10
#         http://www.apache.org/licenses/LICENSE-2.0
11
#
12
#    Unless required by applicable law or agreed to in writing, software
13
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
#    License for the specific language governing permissions and limitations
16
#    under the License.
17
18
"""
19
This migration handles migrating encrypted image location values from
20
the unquoted form to the quoted form.
21
22
If 'metadata_encryption_key' is specified in the config then this
23
migration performs the following steps for every entry in the images table:
24
1. Decrypt the location value with the metadata_encryption_key
25
2. Changes the value to its quoted form
26
3. Encrypts the new value with the metadata_encryption_key
27
4. Inserts the new value back into the row
28
29
Fixes bug #1081043
30
"""
31
import types
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
32
import urllib
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
33
import urlparse
34
35
from oslo.config import cfg
36
import sqlalchemy
37
38
from glance.common import crypt
39
from glance.common import exception
40
import glance.openstack.common.log as logging
41
import glance.store.swift
42
43
LOG = logging.getLogger(__name__)
44
CONF = cfg.CONF
45
1.1.50 by Chuck Short
Import upstream version 2013.1~rc1
46
CONF.import_opt('metadata_encryption_key', 'glance.common.config')
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
47
48
49
def upgrade(migrate_engine):
50
    migrate_location_credentials(migrate_engine, to_quoted=True)
51
52
53
def downgrade(migrate_engine):
54
    migrate_location_credentials(migrate_engine, to_quoted=False)
55
56
57
def migrate_location_credentials(migrate_engine, to_quoted):
58
    """
59
    Migrate location credentials for encrypted swift uri's between the
60
    quoted and unquoted forms.
61
62
    :param migrate_engine: The configured db engine
63
    :param to_quoted: If True, migrate location credentials from
64
                      unquoted to quoted form.  If False, do the
65
                      reverse.
66
    """
67
    if not CONF.metadata_encryption_key:
68
        msg = _("'metadata_encryption_key' was not specified in the config"
69
                " file or a config file was not specified. This means that"
70
                " this migration is a NOOP.")
71
        LOG.info(msg)
72
        return
73
74
    meta = sqlalchemy.schema.MetaData()
75
    meta.bind = migrate_engine
76
77
    images_table = sqlalchemy.Table('images', meta, autoload=True)
78
79
    images = list(images_table.select().execute())
80
81
    for image in images:
82
        try:
83
            fixed_uri = fix_uri_credentials(image['location'], to_quoted)
84
            images_table.update()\
85
                        .where(images_table.c.id == image['id'])\
86
                        .values(location=fixed_uri).execute()
87
        except exception.Invalid:
88
            msg = _("Failed to decrypt location value for image %s")
89
            LOG.warn(msg % image['id'])
90
91
92
def decrypt_location(uri):
93
    return crypt.urlsafe_decrypt(CONF.metadata_encryption_key, uri)
94
95
96
def encrypt_location(uri):
97
    return crypt.urlsafe_encrypt(CONF.metadata_encryption_key, uri, 64)
98
99
100
def fix_uri_credentials(uri, to_quoted):
101
    """
102
    Fix the given uri's embedded credentials by round-tripping with
103
    StoreLocation.
104
105
    If to_quoted is True, the uri is assumed to have credentials that
106
    have not been quoted, and the resulting uri will contain quoted
107
    credentials.
108
109
    If to_quoted is False, the uri is assumed to have credentials that
110
    have been quoted, and the resulting uri will contain credentials
111
    that have not been quoted.
112
    """
113
    if not uri:
114
        return
115
    try:
116
        decrypted_uri = decrypt_location(uri)
117
    #NOTE (ameade): If a uri is not encrypted or incorrectly encoded then we
118
    # we raise an exception.
119
    except (TypeError, ValueError) as e:
120
        raise exception.Invalid(str(e))
121
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
122
    return legacy_parse_uri(decrypted_uri, to_quoted)
123
124
125
def legacy_parse_uri(uri, to_quote):
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
126
    """
127
    Parse URLs. This method fixes an issue where credentials specified
128
    in the URL are interpreted differently in Python 2.6.1+ than prior
129
    versions of Python. It also deals with the peculiarity that new-style
130
    Swift URIs have where a username can contain a ':', like so:
131
132
        swift://account:user:pass@authurl.com/container/obj
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
133
134
    If to_quoted is True, the uri is assumed to have credentials that
135
    have not been quoted, and the resulting uri will contain quoted
136
    credentials.
137
138
    If to_quoted is False, the uri is assumed to have credentials that
139
    have been quoted, and the resulting uri will contain credentials
140
    that have not been quoted.
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
141
    """
142
    # Make sure that URIs that contain multiple schemes, such as:
143
    # swift://user:pass@http://authurl.com/v1/container/obj
144
    # are immediately rejected.
145
    if uri.count('://') != 1:
146
        reason = _("URI cannot contain more than one occurrence of a scheme."
147
                   "If you have specified a URI like "
148
                   "swift://user:pass@http://authurl.com/v1/container/obj"
149
                   ", you need to change it to use the swift+http:// scheme, "
150
                   "like so: "
151
                   "swift+http://user:pass@authurl.com/v1/container/obj")
152
153
        LOG.error(_("Invalid store uri %(uri)s: %(reason)s") % locals())
154
        raise exception.BadStoreUri(message=reason)
155
156
    pieces = urlparse.urlparse(uri)
157
    assert pieces.scheme in ('swift', 'swift+http', 'swift+https')
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
158
    scheme = pieces.scheme
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
159
    netloc = pieces.netloc
160
    path = pieces.path.lstrip('/')
161
    if netloc != '':
162
        # > Python 2.6.1
163
        if '@' in netloc:
164
            creds, netloc = netloc.split('@')
165
        else:
166
            creds = None
167
    else:
168
        # Python 2.6.1 compat
169
        # see lp659445 and Python issue7904
170
        if '@' in path:
171
            creds, path = path.split('@')
172
        else:
173
            creds = None
174
        netloc = path[0:path.find('/')].strip('/')
175
        path = path[path.find('/'):].strip('/')
176
    if creds:
177
        cred_parts = creds.split(':')
178
179
        # User can be account:user, in which case cred_parts[0:2] will be
180
        # the account and user. Combine them into a single username of
181
        # account:user
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
182
        if to_quote:
183
            if len(cred_parts) == 1:
184
                reason = (_("Badly formed credentials '%(creds)s' in Swift "
185
                            "URI") % locals())
186
                LOG.error(reason)
187
                raise exception.BadStoreUri()
188
            elif len(cred_parts) == 3:
189
                user = ':'.join(cred_parts[0:2])
190
            else:
191
                user = cred_parts[0]
192
            key = cred_parts[-1]
193
            user = user
194
            key = key
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
195
        else:
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
196
            if len(cred_parts) != 2:
197
                reason = (_("Badly formed credentials in Swift URI."))
198
                LOG.debug(reason)
199
                raise exception.BadStoreUri()
200
            user, key = cred_parts
201
            user = urllib.unquote(user)
202
            key = urllib.unquote(key)
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
203
    else:
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
204
        user = None
205
        key = None
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
206
    path_parts = path.split('/')
207
    try:
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
208
        obj = path_parts.pop()
209
        container = path_parts.pop()
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
210
        if not netloc.startswith('http'):
211
            # push hostname back into the remaining to build full authurl
212
            path_parts.insert(0, netloc)
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
213
            auth_or_store_url = '/'.join(path_parts)
1.1.49 by Chuck Short
Import upstream version 2013.1.g3
214
    except IndexError:
215
        reason = _("Badly formed S3 URI: %s") % uri
216
        LOG.error(message=reason)
217
        raise exception.BadStoreUri()
1.1.52 by Chuck Short
Import upstream version 2013.2~b1
218
219
    if auth_or_store_url.startswith('http://'):
220
        auth_or_store_url = auth_or_store_url[len('http://'):]
221
    elif auth_or_store_url.startswith('https://'):
222
        auth_or_store_url = auth_or_store_url[len('https://'):]
223
224
    credstring = ''
225
    if user and key:
226
        if to_quote:
227
            quote_user = urllib.quote(user)
228
            quote_key = urllib.quote(key)
229
        else:
230
            quote_user = user
231
            quote_key = key
232
        credstring = '%s:%s@' % (quote_user, quote_key)
233
234
    auth_or_store_url = auth_or_store_url.strip('/')
235
    container = container.strip('/')
236
    obj = obj.strip('/')
237
238
    uri = '%s://%s%s/%s/%s' % (scheme, credstring, auth_or_store_url,
239
                               container, obj)
240
    return encrypt_location(uri)