Merge "Drop EC2 credential download/recreation"

This commit is contained in:
Zuul
2025-07-23 09:57:17 +00:00
committed by Gerrit Code Review
11 changed files with 37 additions and 393 deletions

View File

@@ -840,35 +840,6 @@ def get_default_role(request):
return DEFAULT_ROLE
def ec2_manager(request):
client = keystoneclient(request)
if hasattr(client, 'ec2'):
return client.ec2
from keystoneclient.v3 import ec2
return ec2.EC2Manager(client)
@profiler.trace
def list_ec2_credentials(request, user_id):
return ec2_manager(request).list(user_id)
@profiler.trace
def create_ec2_credentials(request, user_id, tenant_id):
return ec2_manager(request).create(user_id, tenant_id)
@profiler.trace
def get_user_ec2_credentials(request, user_id, access_token):
return ec2_manager(request).get(user_id, access_token)
@profiler.trace
def delete_user_ec2_credentials(request, user_id, access_token):
return ec2_manager(request).delete(user_id, access_token)
def keystone_can_edit_domain():
can_edit_domain = setting_utils.get_dict_config(
'OPENSTACK_KEYSTONE_BACKEND', 'can_edit_domain')

View File

@@ -1,68 +0,0 @@
# Copyright 2016 NEC Corporation
#
# 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 django.utils.translation import gettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard import policy
def get_ec2_credentials(request):
if not policy.check((("identity", "identity:ec2_list_credentials"),),
request):
return None
project_id = request.user.project_id
all_keys = api.keystone.list_ec2_credentials(request,
request.user.id)
keys = [x for x in all_keys if x.tenant_id == project_id]
if not keys:
return None
return {'ec2_access_key': keys[0].access,
'ec2_secret_key': keys[0].secret}
class RecreateCredentials(forms.SelfHandlingForm):
def handle(self, request, context):
try:
credential = get_ec2_credentials(request)
if credential:
api.keystone.delete_user_ec2_credentials(
request,
request.user.id,
credential['ec2_access_key'])
except Exception:
exceptions.handle(
request, _('Unable to recreate ec2 credentials. '
'Failed to delete ec2 credentials.'))
return False
try:
api.keystone.create_ec2_credentials(
request,
request.user.id,
request.user.project_id)
message = _('Successfully recreated ec2 credentials.')
messages.success(request, message)
return True
except Exception:
exceptions.handle(
request, _('Unable to recreate ec2 credentials. '
'Failed to create ec2 credentials.'))
return False

View File

@@ -17,31 +17,17 @@ from django.template.defaultfilters import title
from django.utils.translation import gettext_lazy as _
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.api_access import forms
from openstack_dashboard import policy
def pretty_service_names(name):
name = name.replace('-', ' ')
if name in ['ec2', 's3']:
if name in ('s3',):
name = name.upper()
else:
name = title(name)
return name
class DownloadEC2(tables.LinkAction):
name = "download_ec2"
verbose_name = _("EC2 Credentials")
verbose_name_plural = _("EC2 Credentials")
icon = "download"
url = "horizon:project:api_access:ec2"
def allowed(self, request, datum=None):
return api.base.is_service_enabled(request, 'ec2')
class DownloadCloudsYaml(tables.LinkAction):
name = "download_clouds_yaml"
verbose_name = _("OpenStack clouds.yaml File")
@@ -72,29 +58,6 @@ class ViewCredentials(tables.LinkAction):
url = "horizon:project:api_access:view_credentials"
class RecreateCredentials(tables.LinkAction):
name = "recreate_credentials"
verbose_name = _("Recreate EC2 Credentials")
classes = ("ajax-modal",)
icon = "refresh"
url = "horizon:project:api_access:recreate_credentials"
policy_rules = (("compute", "os_compute_api:certificates:create"))
action_type = "danger"
def allowed(self, request, datum=None):
try:
target = {"target.credential.user_id": request.user.id}
if (api.base.is_service_enabled(request, 'ec2') and
forms.get_ec2_credentials(request) and
policy.check((("identity", "identity:ec2_create_credential"),
("identity", "identity:ec2_delete_credential")),
request, target=target)):
return True
except Exception:
pass
return False
class EndpointsTable(tables.DataTable):
api_name = tables.Column('type',
verbose_name=_("Service"),
@@ -106,8 +69,7 @@ class EndpointsTable(tables.DataTable):
name = "endpoints"
verbose_name = _("API Endpoints")
multi_select = False
table_actions = (ViewCredentials, RecreateCredentials)
table_actions = (ViewCredentials,)
table_actions_menu = (DownloadCloudsYaml,
DownloadOpenRC,
DownloadEC2)
DownloadOpenRC)
table_actions_menu_label = _('Download OpenStack RC File')

View File

@@ -4,57 +4,31 @@
{% block modal-body %}
<form>
{% if openrc_creds %}
<div class="{% if ec2_creds %}left{% endif %}">
<div class="form-group">
<label for="openrc-user">{% trans "User Name" %}</label>
<input type="text" class="form-control" id="openrc-user" readonly value="{{ openrc_creds.user.username }}">
</div>
<div class="form-group">
<label for="openrc-user">{% trans "User ID" %}</label>
<input type="text" class="form-control" id="openrc-userid" readonly value="{{ openrc_creds.user.id }}">
</div>
{% if "user_domain_name" in openrc_creds %}
<div class="form-group">
<label for="openrc-domain">{% trans "Domain Name" %}</label>
<input type="text" class="form-control" id="openrc-domain" readonly value="{{ openrc_creds.user_domain_name }}">
</div>
{% endif %}
<div class="form-group">
<label for="openrc-project-name">{% trans "Project Name" %}</label>
<input type="text" class="form-control" id="openrc-project-name" readonly value="{{ openrc_creds.tenant_name }}">
</div>
<div class="form-group">
<label for="openrc-project-id">{% trans "Project ID" %}</label>
<input type="text" class="form-control" id="openrc-project-id" readonly value="{{ openrc_creds.tenant_id }}">
</div>
<div class="form-group">
<label for="openrc-auth">{% trans "Authentication URL" %}</label>
<input type="text" id="openrc-auth" class="form-control" readonly value="{{ openrc_creds.auth_url }}">
</div>
<div class="form-group">
<label for="openrc-user">{% trans "User Name" %}</label>
<input type="text" class="form-control" id="openrc-user" readonly value="{{ openrc_creds.user.username }}">
</div>
{% endif %}
{% if ec2_creds %}
<div class="{% if openrc_creds %}right{% endif %}">
<div class="form-group">
<label for="ec2-url">{% trans "EC2 URL" %}</label>
<input type="text" id="ec2-url" class="form-control" readonly value="{{ ec2_creds.ec2_endpoint }}">
</div>
<div class="form-group">
<label for="s3-url">{% trans "S3 URL" %}</label>
<input type="text" id="s3-url" class="form-control" readonly value="{{ ec2_creds.s3_endpoint }}">
</div>
{% if ec2_creds.ec2_access_key %}
<div class="form-group">
<label for="ec2-access">{% trans "EC2 Access Key" %}</label>
<input type="text" id="ec2-access" class="form-control" readonly value="{{ ec2_creds.ec2_access_key }}">
</div>
{% endif %}
{% if ec2_creds.ec2_secret_key %}
<div class="form-group">
<label for="ec2-secret">{% trans "EC2 Secret Key" %}</label>
<input type="password" id="ec2-secret" class="form-control" readonly value="{{ ec2_creds.ec2_secret_key }}">
</div>
{% endif %}
<div class="form-group">
<label for="openrc-user">{% trans "User ID" %}</label>
<input type="text" class="form-control" id="openrc-userid" readonly value="{{ openrc_creds.user.id }}">
</div>
{% if "user_domain_name" in openrc_creds %}
<div class="form-group">
<label for="openrc-domain">{% trans "Domain Name" %}</label>
<input type="text" class="form-control" id="openrc-domain" readonly value="{{ openrc_creds.user_domain_name }}">
</div>
{% endif %}
<div class="form-group">
<label for="openrc-project-name">{% trans "Project Name" %}</label>
<input type="text" class="form-control" id="openrc-project-name" readonly value="{{ openrc_creds.tenant_name }}">
</div>
<div class="form-group">
<label for="openrc-project-id">{% trans "Project ID" %}</label>
<input type="text" class="form-control" id="openrc-project-id" readonly value="{{ openrc_creds.tenant_id }}">
</div>
<div class="form-group">
<label for="openrc-auth">{% trans "Authentication URL" %}</label>
<input type="text" id="openrc-auth" class="form-control" readonly value="{{ openrc_creds.auth_url }}">
</div>
{% endif %}
</form>

View File

@@ -1,15 +0,0 @@
#!/bin/bash
NOVARC=$(readlink -f "${BASH_SOURCE:-${0}}" 2>/dev/null) || NOVARC=$(python -c 'import os,sys; print os.path.abspath(os.path.realpath(sys.argv[1]))' "${BASH_SOURCE:-${0}}")
NOVA_KEY_DIR=${NOVARC%/*}
export EC2_ACCESS_KEY={{ ec2_access_key }}
export EC2_SECRET_KEY={{ ec2_secret_key }}
export EC2_URL={{ ec2_endpoint }}
export EC2_USER_ID=42 # nova does not use user id, but bundling requires it
export EC2_PRIVATE_KEY=${NOVA_KEY_DIR}/pk.pem
export EC2_CERT=${NOVA_KEY_DIR}/cert.pem
export NOVA_CERT=${NOVA_KEY_DIR}/cacert.pem
export EUCALYPTUS_CERT=${NOVA_CERT} # euca-bundle-image seems to require this set
{% if s3_endpoint %}export S3_URL={{ s3_endpoint }}{% endif %}
alias ec2-bundle-image="ec2-bundle-image --cert ${EC2_CERT} --privatekey ${EC2_PRIVATE_KEY} --user 42 --ec2cert ${NOVA_CERT}"
alias ec2-upload-bundle="ec2-upload-bundle -a ${EC2_ACCESS_KEY} -s ${EC2_SECRET_KEY} --url ${S3_URL} --ec2cert ${NOVA_CERT}"

View File

@@ -1,5 +0,0 @@
{% extends 'base.html' %}
{% block main %}
{% include 'project/api_access/_recreate_credentials.html' %}
{% endblock %}

View File

@@ -19,34 +19,16 @@ from django.template import loader
from django.test.utils import override_settings
from django.urls import reverse
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:api_access:index')
API_URL = "horizon:project:api_access"
EC2_URL = reverse(API_URL + ":ec2")
OPENRC_URL = reverse(API_URL + ":openrc")
CREDS_URL = reverse(API_URL + ":view_credentials")
RECREATE_CREDS_URL = reverse(API_URL + ":recreate_credentials")
class APIAccessTests(test.TestCase):
@test.create_mocks({api.keystone: ('create_ec2_credentials',
'list_ec2_credentials')})
def test_ec2_download_view(self):
creds = self.ec2.first()
self.mock_list_ec2_credentials.return_value = []
self.mock_create_ec2_credentials.return_value = creds
res = self.client.get(EC2_URL)
self.assertEqual(res.status_code, 200)
self.assertEqual(res['content-type'], 'application/zip')
self.mock_list_ec2_credentials.assert_called_once_with(
test.IsHttpRequest(), self.user.id)
self.mock_create_ec2_credentials.assert_called_once_with(
test.IsHttpRequest(), self.user.id, self.tenant.id)
@override_settings(OPENSTACK_API_VERSIONS={"identity": 3})
def test_openrc_credentials(self):
@@ -62,59 +44,6 @@ class APIAccessTests(test.TestCase):
self.assertIn(p_id.encode('utf-8'), res.content)
self.assertIn(domain.encode('utf-8'), res.content)
@test.create_mocks({api.keystone: ('list_ec2_credentials',)})
def test_credential_api(self):
certs = self.ec2.list()
self.mock_list_ec2_credentials.return_value = certs
res = self.client.get(CREDS_URL)
self.assertEqual(res.status_code, 200)
credentials = 'project/api_access/credentials.html'
self.assertTemplateUsed(res, credentials)
self.assertEqual(self.user.id, res.context['openrc_creds']['user'].id)
self.assertEqual(certs[0].access,
res.context['ec2_creds']['ec2_access_key'])
self.mock_list_ec2_credentials.assert_called_once_with(
test.IsHttpRequest(), self.user.id)
@test.create_mocks({api.keystone: ('create_ec2_credentials',
'list_ec2_credentials',
'delete_user_ec2_credentials')})
def _test_recreate_user_credentials(self, exists_credentials=True):
old_creds = self.ec2.list() if exists_credentials else []
new_creds = self.ec2.first()
self.mock_list_ec2_credentials.return_value = old_creds
if exists_credentials:
self.mock_delete_user_ec2_credentials.return_value = []
self.mock_create_ec2_credentials.return_value = new_creds
res_get = self.client.get(RECREATE_CREDS_URL)
self.assertEqual(res_get.status_code, 200)
credentials = \
'project/api_access/recreate_credentials.html'
self.assertTemplateUsed(res_get, credentials)
res_post = self.client.post(RECREATE_CREDS_URL)
self.assertNoFormErrors(res_post)
self.assertRedirectsNoFollow(res_post, INDEX_URL)
self.mock_list_ec2_credentials.assert_called_once_with(
test.IsHttpRequest(), self.user.id)
if exists_credentials:
self.mock_delete_user_ec2_credentials.assert_called_once_with(
test.IsHttpRequest(), self.user.id, old_creds[0].access)
else:
self.mock_delete_user_ec2_credentials.assert_not_called()
self.mock_create_ec2_credentials.assert_called_once_with(
test.IsHttpRequest(), self.user.id, self.tenant.id)
def test_recreate_user_credentials(self):
self._test_recreate_user_credentials()
def test_recreate_user_credentials_with_no_existing_creds(self):
self._test_recreate_user_credentials(exists_credentials=False)
class ASCIITenantNameRCTests(test.TestCase):
TENANT_NAME = 'tenant'

View File

@@ -22,13 +22,9 @@ from openstack_dashboard.dashboards.project.api_access import views
urlpatterns = [
re_path(r'^$', views.IndexView.as_view(), name='index'),
re_path(r'^ec2/$', views.download_ec2_bundle, name='ec2'),
re_path(r'^clouds.yaml/$',
views.download_clouds_yaml_file, name='clouds.yaml'),
re_path(r'^openrc/$', views.download_rc_file, name='openrc'),
re_path(r'^view_credentials/$', views.CredentialsView.as_view(),
name='view_credentials'),
re_path(r'^recreate_ec2_credentials/$',
views.RecreateCredentialsView.as_view(),
name='recreate_credentials'),
]

View File

@@ -12,16 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from contextlib import closing
import logging
import tempfile
import zipfile
from django.conf import settings
from django import http
from django import shortcuts
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from horizon import exceptions
@@ -31,44 +27,12 @@ from horizon import tables
from horizon import views
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.api_access \
import forms as api_access_forms
from openstack_dashboard.dashboards.project.api_access \
import tables as api_access_tables
LOG = logging.getLogger(__name__)
def _get_ec2_credentials(request):
tenant_id = request.user.tenant_id
all_keys = api.keystone.list_ec2_credentials(request,
request.user.id)
key = next((x for x in all_keys if x.tenant_id == tenant_id), None)
if not key:
key = api.keystone.create_ec2_credentials(request,
request.user.id,
tenant_id)
try:
s3_endpoint = api.base.url_for(request,
's3',
endpoint_type='publicURL')
except exceptions.ServiceCatalogException:
s3_endpoint = None
try:
ec2_endpoint = api.base.url_for(request,
'ec2',
endpoint_type='publicURL')
except exceptions.ServiceCatalogException:
ec2_endpoint = None
return {'ec2_access_key': key.access,
'ec2_secret_key': key.secret,
'ec2_endpoint': ec2_endpoint,
's3_endpoint': s3_endpoint}
def _get_openrc_credentials(request):
keystone_url = api.base.url_for(request,
'identity',
@@ -85,40 +49,6 @@ def _get_openrc_credentials(request):
}
# TODO(stephenfin): Migrate to CBV
def download_ec2_bundle(request):
tenant_name = request.user.tenant_name
# Gather or create our EC2 credentials
try:
context = _get_ec2_credentials(request)
except Exception:
exceptions.handle(request,
_('Unable to fetch EC2 credentials.'),
redirect=request.build_absolute_uri())
# Create our file bundle
template = 'project/api_access/ec2rc.sh.template'
try:
# pylint: disable-next=consider-using-with
temp_zip = tempfile.NamedTemporaryFile(delete=True)
with closing(zipfile.ZipFile(temp_zip.name, mode='w')) as archive:
archive.writestr('ec2rc.sh', render_to_string(template, context))
except Exception:
exceptions.handle(request,
_('Error writing zipfile: %(exc)s'),
redirect=request.build_absolute_uri())
# Send it back
response = http.HttpResponse(content_type='application/zip')
response.write(temp_zip.read())
response['Content-Disposition'] = ('attachment; '
'filename="%s-x509.zip"'
% tenant_name)
response['Content-Length'] = temp_zip.tell()
return response
# TODO(stephenfin): Migrate to CBV
def download_rc_file(request):
template = settings.OPENRC_CUSTOM_TEMPLATE
@@ -186,27 +116,9 @@ class CredentialsView(forms.ModalFormMixin, views.HorizonTemplateView):
except Exception:
exceptions.handle(self.request,
_('Unable to get openrc credentials'))
if api.base.is_service_enabled(self.request, 'ec2'):
try:
context['ec2_creds'] = _get_ec2_credentials(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to get EC2 credentials'))
return context
class RecreateCredentialsView(forms.ModalFormView):
form_class = api_access_forms.RecreateCredentials
form_id = "recreate_credentials"
page_title = _("Recreate EC2 Credentials")
template_name = \
'project/api_access/recreate_credentials.html'
submit_label = _("Recreate EC2 Credentials")
submit_url = reverse_lazy(
"horizon:project:api_access:recreate_credentials")
success_url = reverse_lazy('horizon:project:api_access:index')
class IndexView(tables.DataTableView):
table_class = api_access_tables.EndpointsTable
page_title = _("API Access")

View File

@@ -19,7 +19,6 @@ from django.conf import settings
from django.utils import datetime_safe
from keystoneclient import access
from keystoneclient.v2_0 import ec2
from keystoneclient.v2_0 import roles
from keystoneclient.v2_0 import tenants
from keystoneclient.v2_0 import users
@@ -145,21 +144,7 @@ SERVICE_CATALOG = [
{"region": "RegionOne",
"interface": "public",
"url": "http://public.neutron.example.com:9696/"}
]},
{"type": "ec2",
"name": "EC2 Service",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"interface": "admin",
"url": "http://admin.nova.example.com:8773/services/Admin"},
{"region": "RegionOne",
"interface": "public",
"url": "http://public.nova.example.com:8773/services/Cloud"},
{"region": "RegionOne",
"interface": "internal",
"url": "http://int.nova.example.com:8773/services/Cloud"}
]},
]}
]
@@ -175,7 +160,6 @@ def data(TEST):
TEST.tenants = utils.TestDataContainer()
TEST.role_assignments = utils.TestDataContainer()
TEST.roles = utils.TestDataContainer()
TEST.ec2 = utils.TestDataContainer()
TEST.identity_providers = utils.TestDataContainer()
TEST.idp_mappings = utils.TestDataContainer()
@@ -439,11 +423,6 @@ def data(TEST):
TEST.tokens.scoped_token = scoped_token
TEST.tokens.unscoped_token = unscoped_token
access_secret = ec2.EC2(ec2.CredentialsManager, {"access": "access",
"secret": "secret",
"tenant_id": tenant.id})
TEST.ec2.add(access_secret)
idp_dict_1 = {'id': 'idp_1',
'description': 'identity provider 1',
'enabled': True,

View File

@@ -0,0 +1,9 @@
---
upgrade:
- |
The following features are no longer supported. These have been disabled
unless EC2-API service is deployed, but EC2-API project was already
retired.
- Download EC2 credentials
- Recreate EC2 credentials