From f0cfc52bb1deeb2affa304ccb57840f169169167 Mon Sep 17 00:00:00 2001 From: Marica Antonacci Date: Thu, 2 Nov 2023 14:25:15 +0100 Subject: [PATCH] Fix/template access (#53) * Fix template visibility * Remove unused function * Manage missing visibility in metadata * Implement new metadata structure and validation --- app/__init__.py | 7 +- app/deployments/routes.py | 21 ++-- app/deployments/templates/deployments.html | 2 +- app/home/routes.py | 43 ++++---- app/home/templates/portfolio.html | 12 ++- app/lib/{ToscaInfo.py => tosca_info.py} | 46 +++++---- config/default.py | 2 +- config/infn-cloud.py | 2 - config/schemas/metadata_schema.json | 113 +++++++++++++++++++++ requirements.txt | 1 + 10 files changed, 193 insertions(+), 56 deletions(-) rename app/lib/{ToscaInfo.py => tosca_info.py} (90%) create mode 100644 config/schemas/metadata_schema.json diff --git a/app/__init__.py b/app/__init__.py index 15a1594ad..4780c5711 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -26,7 +26,7 @@ from flask_migrate import Migrate, upgrade from flask_caching import Cache from flask_redis import FlaskRedis -from app.lib.ToscaInfo import ToscaInfo +from app.lib.tosca_info import ToscaInfo from app.lib.Vault import Vault import logging @@ -48,6 +48,7 @@ app.secret_key = "30bb7cf2-1fef-4d26-83f0-8096b6dcc7a3" app.config.from_object('config.default') app.config.from_file('config.json', json.load) +app.config.from_file('../config/schemas/metadata_schema.json', json.load) if app.config.get("FEATURE_VAULT_INTEGRATION") == "yes": app.config.from_file('vault-config.json', json.load) @@ -87,7 +88,7 @@ def inject_settings(): s3_allowed_groups=app.config.get("S3_IAM_GROUPS") if app.config.get("S3_IAM_GROUPS") else [], enable_access_request=app.config.get("FEATURE_ACCESS_REQUEST") if app.config.get( 'FEATURE_ACCESS_REQUEST') else "no", - access_request_tag=app.config.get("ACCESS_REQUEST_TAG") + not_granted_access_tag=app.config.get("NOT_GRANTED_ACCESS_TAG") ) @@ -107,7 +108,7 @@ def inject_settings(): # initialize ToscaInfo tosca: ToscaInfo = ToscaInfo(redis_client, app.config.get("TOSCA_TEMPLATES_DIR"), - app.config.get("SETTINGS_DIR")) + app.config.get("SETTINGS_DIR"), app.config.get("METADATA_SCHEMA")) from app.errors.routes import errors_bp app.register_blueprint(errors_bp) diff --git a/app/deployments/routes.py b/app/deployments/routes.py index eb6cfc787..509a2b785 100644 --- a/app/deployments/routes.py +++ b/app/deployments/routes.py @@ -13,27 +13,26 @@ # limitations under the License. import copy - +import uuid as uuid_generator +import io +import os +import re +from urllib.parse import urlparse +import yaml from flask import Blueprint, session, render_template, flash, redirect, url_for, json, request +from packaging import version +from werkzeug.exceptions import Forbidden +from werkzeug.utils import secure_filename from app import app, iam_blueprint, tosca, vaultservice from app.lib import auth, utils, settings, dbhelpers, yourls from app.lib.ldap_user import LdapUserManager from app.models.Deployment import Deployment from app.providers import sla -from app.lib import ToscaInfo as tosca_helpers +from app.lib import tosca_info as tosca_helpers from app.lib import openstack as keystone from app.lib.orchestrator import Orchestrator from app.lib import s3 as s3 -from werkzeug.exceptions import Forbidden -from werkzeug.utils import secure_filename from app.swift.swift import Swift -from packaging import version -from urllib.parse import urlparse -import uuid as uuid_generator -import yaml -import io -import os -import re deployments_bp = Blueprint('deployments_bp', __name__, template_folder='templates', diff --git a/app/deployments/templates/deployments.html b/app/deployments/templates/deployments.html index 506617e40..a8c4c484b 100644 --- a/app/deployments/templates/deployments.html +++ b/app/deployments/templates/deployments.html @@ -88,7 +88,7 @@

My deployments

Edit Show template {% if enable_update_deployment == "yes" and deployment.updatable == 1 %} - Update + Add/Remove nodes {% endif %} {% if deployment.deployment_type == "CLOUD" %} diff --git a/app/home/routes.py b/app/home/routes.py index 31a259ddc..451b1dc05 100644 --- a/app/home/routes.py +++ b/app/home/routes.py @@ -18,7 +18,7 @@ from datetime import datetime from markupsafe import Markup from flask import Blueprint, json, render_template, request, redirect, url_for, session, make_response, flash -import json +import re, json app.jinja_env.filters['tojson_pretty'] = utils.to_pretty_json app.jinja_env.filters['extract_netinterface_ips'] = utils.extract_netinterface_ips @@ -85,7 +85,12 @@ def submit_settings(): if repo_url: dashboard_configuration_info['dashboard_configuration_url'] = repo_url if tag_or_branch: dashboard_configuration_info['dashboard_configuration_tag_or_branch'] = tag_or_branch - tosca.reload() + try: + tosca.reload() + except Exception as error: + app.logger.error(f"Error reloading configuration: {error}") + flash(f"Error reloading configuration: { type(error).__name__ }. Please check the logs.", "danger") + app.logger.debug("Configuration reloaded") now = datetime.now() @@ -118,25 +123,27 @@ def login(): return render_template(app.config.get('HOME_TEMPLATE')) -def is_template_locked(allowed_groups, user_groups): - # check intersection of user groups with user membership - if (allowed_groups is None or set(allowed_groups.split(',')) & set(user_groups)) != set() or allowed_groups == '*': - return False - else: - return True - - def set_template_access(tosca, user_groups, active_group): info = {} for k, v in tosca.items(): - allowed_groups = v.get("metadata").get("allowed_groups") - if not allowed_groups: - app.logger.error("Null - {}".format(k)) - access_locked = is_template_locked(allowed_groups, user_groups) - if (access_locked and ("visibility" not in v.get("metadata") or v["metadata"]["visibility"] == "public")) or ( - not access_locked and (active_group in allowed_groups.split(',') or allowed_groups == "*")): - v["metadata"]["access_locked"] = access_locked - info[k] = v + visibility = v.get("metadata").get("visibility") if "visibility" in v.get("metadata") else {"type": "public"} + + if visibility.get("type") != "public": + + regex = False if "groups_regex" not in visibility else True + + if regex: + access_locked = not re.match(visibility.get('groups_regex'), active_group) + else: + allowed_groups = visibility.get("groups") + access_locked = True if active_group not in allowed_groups else False + + if (visibility.get("type") == "private" and not access_locked) or visibility.get("type") == "protected": + v["metadata"]["access_locked"] = access_locked + info[k] = v + else: + info[k] = v + return info diff --git a/app/home/templates/portfolio.html b/app/home/templates/portfolio.html index f483e22e1..95456b3a6 100644 --- a/app/home/templates/portfolio.html +++ b/app/home/templates/portfolio.html @@ -24,6 +24,16 @@

On-demand Services:

+ {% if templates_info.items()|length == 0 %} +
+
+

No Available Services for Deployment

+

We're sorry, but no services are currently available for deployment.

+

This could be due to specific requirements or settings.

+

Please contact our support team for assistance."

+
+
+ {% endif %} {% for tosca_name, tosca in templates_info.items() %} {% if loop.index % 3 == 1 %} @@ -39,7 +49,7 @@
Card image cap {% if tosca['metadata']['access_locked'] %}
- {{ access_request_tag }} + {{ not_granted_access_tag }}
{% else %} {% if tosca['metadata']['tag'] is defined %} diff --git a/app/lib/ToscaInfo.py b/app/lib/tosca_info.py similarity index 90% rename from app/lib/ToscaInfo.py rename to app/lib/tosca_info.py index 93de738fe..997a506f4 100644 --- a/app/lib/ToscaInfo.py +++ b/app/lib/tosca_info.py @@ -12,19 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" - Class to load tosca templates at application start -""" + import json import os import io from fnmatch import fnmatch +import uuid import yaml +import jsonschema -class ToscaInfo(object): +class ToscaInfo: + """Class to load tosca templates and metadata at application start""" - def __init__(self, redis_client, tosca_dir=None, settings_dir=None): + def __init__(self, redis_client, tosca_dir, settings_dir, metadata_schema): """ Initialize the flask extension :param tosca_dir: the dir of the tosca templates @@ -34,6 +35,7 @@ def __init__(self, redis_client, tosca_dir=None, settings_dir=None): self.tosca_dir = tosca_dir + '/' self.tosca_params_dir = settings_dir + '/tosca-parameters' self.tosca_metadata_dir = settings_dir + '/tosca-metadata' + self.metadata_schema = metadata_schema tosca_info = {} tosca_gmetadata = {} @@ -41,10 +43,7 @@ def __init__(self, redis_client, tosca_dir=None, settings_dir=None): tosca_templates = self._loadtoscatemplates() tosca_info = self._extractalltoscainfo(tosca_templates) - - if os.path.isfile(self.tosca_metadata_dir + "/metadata.yml"): - with io.open(self.tosca_metadata_dir + "/metadata.yml") as stream: - tosca_gmetadata = yaml.full_load(stream) + tosca_gmetadata = self._loadmetadata() redis_client.set("tosca_templates", json.dumps(tosca_templates)) redis_client.set("tosca_gmetadata", json.dumps(tosca_gmetadata)) @@ -54,16 +53,24 @@ def __init__(self, redis_client, tosca_dir=None, settings_dir=None): def reload(self): tosca_templates = self._loadtoscatemplates() tosca_info = self._extractalltoscainfo(tosca_templates) - tosca_gmetadata = {} - - if os.path.isfile(self.tosca_metadata_dir + "/metadata.yml"): - with io.open(self.tosca_metadata_dir + "/metadata.yml") as stream: - tosca_gmetadata = yaml.full_load(stream) + tosca_gmetadata = self._loadmetadata() self.redis_client.set("tosca_templates", json.dumps(tosca_templates)) self.redis_client.set("tosca_gmetadata", json.dumps(tosca_gmetadata)) self.redis_client.set("tosca_info", json.dumps(tosca_info)) + def _loadmetadata(self): + if os.path.isfile(self.tosca_metadata_dir + "/metadata.yml"): + with io.open(self.tosca_metadata_dir + "/metadata.yml") as stream: + metadata = yaml.full_load(stream) + + # validate against schema + jsonschema.validate(metadata, self.metadata_schema, format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER) + #tosca_gmetadata = {service["id"]: {k: v for k, v in service.items() if k != "id"} for service in metadata['services']} + tosca_gmetadata = {str(uuid.uuid4()): service for service in metadata['services']} + return tosca_gmetadata + + def _loadtoscatemplates(self): toscatemplates = [] for path, subdirs, files in os.walk(self.tosca_dir): @@ -81,19 +88,20 @@ def _extractalltoscainfo(self, tosca_templates): with io.open(self.tosca_dir + tosca) as stream: template = yaml.full_load(stream) tosca_info[tosca] = self.extracttoscainfo(template, tosca) + #info = self.extracttoscainfo(template, tosca) + #tosca_info[info.get('id')] = info return tosca_info def extracttoscainfo(self, template, tosca): - + tosca_info = { "valid": True, "description": "TOSCA Template", "metadata": { "icon": "https://cdn4.iconfinder.com/data/icons/mosaicon-04/512/websettings-512.png", - "visibility": "public", - "allowed_groups": '*', - "require_ssh_key": True, - "template_type": "" + "visibility": { "type": "public" }, + "require_ssh_key": True, + "template_type": "" }, "enable_config_form": False, "inputs": {}, diff --git a/config/default.py b/config/default.py index 7a10a19c9..3f0b26151 100644 --- a/config/default.py +++ b/config/default.py @@ -46,7 +46,7 @@ FEATURE_S3CREDS_MENU = "no" FEATURE_ACCESS_REQUEST = "yes" -ACCESS_REQUEST_TAG = "LOCKED" +NOT_GRANTED_ACCESS_TAG = "LOCKED" ### VAULT INTEGRATION SETTINGS VAULT_ROLE = "orchestrator" diff --git a/config/infn-cloud.py b/config/infn-cloud.py index 09b3c7972..21e25ef8e 100644 --- a/config/infn-cloud.py +++ b/config/infn-cloud.py @@ -10,8 +10,6 @@ FEATURE_PORTS_REQUEST = "yes" FEATURE_ACCESS_REQUEST = "no" -ACCESS_REQUEST_TAG = "SYS-ADMIN ONLY" - ### Template Paths HOME_TEMPLATE = 'infn-cloud/home.html' FOOTER_TEMPLATE = 'infn-cloud/footer.html' diff --git a/config/schemas/metadata_schema.json b/config/schemas/metadata_schema.json new file mode 100644 index 000000000..4f8b00d40 --- /dev/null +++ b/config/schemas/metadata_schema.json @@ -0,0 +1,113 @@ +{ + "METADATA_SCHEMA": { + "$schema": "http://json-schema.org/draft/2020-12/schema#", + "type": "object", + "properties" : { + "services" : { + "type": "array", + "items": { + "$ref": "#/$defs/service" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": ["services"], + "additionalProperties": false, + "$defs": { + "service": { + "type": "object", + "properties": { + "description": { "type": "string" }, + "metadata": { "$ref": "#/$defs/metadata" }, + "templates": { + "type": "array", + "items": { "$ref": "#/$defs/template" } + } + }, + "required": ["description", "metadata", "templates"], + "additionalProperties": false + }, + "metadata": { + "type": "object", + "properties": { + "display_name": { "type": "string" }, + "icon": { "type": "string", "format": "uri" }, + "visibility": { + "$ref": "#/$defs/visibility" + }, + "authorization_required" : {"$ref": "#/$defs/authorization_required" } + }, + "required": ["display_name", "icon", "visibility"], + "additionalProperties": false + }, + "visibility": { + "type": "object", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string" }, + "groups": { "type": "array", "items": { "type": "string" } } + }, + "required": ["type", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { "type": "string" }, + "groups_regex": { "type": "string", "format": "regex" } + }, + "required": ["type", "groups_regex"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { "const": "public" } + }, + "required": ["type"] + } + ] + }, + "template": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "option": { "type": "string" }, + "description": { "type": "string" } + }, + "additionalProperties": false, + "if": + { + "type": "array", + "minItems": 2 + }, + "then": { + "required": ["name", "option"] + }, + "else": { + "required": ["name"] + } + }, + "authorization_required": { + "type": "object", + "properties": { + "pre_tasks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { "type": "string" }, + "args": { "type": "object" } + }, + "additionalProperties": false + } + } + } + } + } + } +} + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3ed9c7f54..b8cd655f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,3 +27,4 @@ boto3==1.21.27 python-ldap==3.4.0 randomcolor sshpubkeys +jsonschema[format]