- Added permissions to the partner service configuration

- Corrected a nasty bug where dynamic boolean fields were not returned correctly
This commit is contained in:
Josako
2025-04-11 21:47:41 +02:00
parent 35f0adef1b
commit 3eed546879
10 changed files with 96 additions and 25 deletions

View File

@@ -244,6 +244,7 @@ class PartnerService(db.Model):
# Dynamic configuration specific to this service - using JSONB like your other models # Dynamic configuration specific to this service - using JSONB like your other models
configuration = db.Column(db.JSON, nullable=True) configuration = db.Column(db.JSON, nullable=True)
permissions = db.Column(db.JSON, nullable=True)
# For services that need to track shared resources # For services that need to track shared resources
system_metadata = db.Column(db.JSON, nullable=True) system_metadata = db.Column(db.JSON, nullable=True)

View File

@@ -1,13 +1,25 @@
version: "1.0.0" version: "1.0.0"
name: "Management Service" name: "Management Service"
configuration: configuration:
admin_roles: billing_partner:
name: "Administrative Roles" name: "Billing Partner"
type: "enum" type: "boolean"
description: "Administrative Roles that can be assigned to the partner" description: "Billing of assigned Tenants is done through the partner."
required: true required: true
allowed_values: ["Partner Admin"] default: false
default: "Partner Admin" permissions:
can_create_tenant:
name: "Can Create Tenant"
type: "boolean"
description: "The Partner Admin can create new Tenants, linked to the partner"
required: true
default: false
can_assign_license:
name: "Can Assign License"
type: "boolean"
description: "The Partner Admin can assign licenses to Tenants, linked to the partner"
required: true
default: false
metadata: metadata:
author: "Josako" author: "Josako"
date_added: "2025-04-02" date_added: "2025-04-02"

View File

@@ -16,9 +16,5 @@ PARTNER_SERVICE_TYPES = {
"name": "Management Service", "name": "Management Service",
"description": "Partner managing customer instances", "description": "Partner managing customer instances",
}, },
"WHITE_LABEL_SERVICE": {
"name": "White Label Service",
"description": "Partner reselling under their own brand",
}
} }

View File

@@ -10,9 +10,19 @@
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% set disabled_fields = ['type'] %} {% set disabled_fields = ['type'] %}
{% set exclude_fields = [] %} {% set exclude_fields = [] %}
{% for field in form %} <!-- Render Static Fields -->
{% for field in form.get_static_fields() %}
{{ render_field(field, disabled_fields, exclude_fields) }} {{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %} {% endfor %}
<!-- Render Dynamic Fields -->
{% for collection_name, fields in form.get_dynamic_fields().items() %}
{% if fields|length > 0 %}
<h4 class="mt-4">{{ collection_name }}</h4>
{% endif %}
{% for field in fields %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
{% endfor %}
<button type="submit" class="btn btn-primary">Register Partner Service</button> <button type="submit" class="btn btn-primary">Register Partner Service</button>
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -2,7 +2,7 @@
{% if field.type == 'BooleanField' %} {% if field.type == 'BooleanField' %}
<div class="form-group"> <div class="form-group">
<div class="form-check form-switch"> <div class="form-check form-switch">
{{ field(class="form-check-input " + class, disabled=disabled) }} {{ field(class="form-check-input " + class, disabled=disabled, required=False) }}
{% if field.description %} {% if field.description %}
{{ field.label(class="form-check-label", {{ field.label(class="form-check-label",
**{'data-bs-toggle': 'tooltip', **{'data-bs-toggle': 'tooltip',

View File

@@ -17,14 +17,14 @@ class EditPartnerForm(FlaskForm):
tenant = StringField('Tenant', validators=[DataRequired()], render_kw={'readonly': True}) tenant = StringField('Tenant', validators=[DataRequired()], render_kw={'readonly': True})
code = StringField('Code', description="Referral Code", validators=[DataRequired()], render_kw={'readonly': True}) code = StringField('Code', description="Referral Code", validators=[DataRequired()], render_kw={'readonly': True})
logo_url = StringField('Logo URL', validators=[Optional(), Length(max=255)]) logo_url = StringField('Logo URL', validators=[Optional(), Length(max=255)])
active = BooleanField('Active', validators=[DataRequired()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)
class PartnerServiceForm(FlaskForm): class PartnerServiceForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)]) name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
type = SelectField('Partner Service Type', validators=[DataRequired()]) type = SelectField('Partner Service Type', validators=[DataRequired()])
active = BooleanField('Active', validators=[DataRequired()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json]) user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json]) system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
@@ -40,7 +40,7 @@ class EditPartnerServiceForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired(), Length(max=50)]) name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
type = StringField('Partner Service Type', validators=[DataRequired()], render_kw={'readonly': True}) type = StringField('Partner Service Type', validators=[DataRequired()], render_kw={'readonly': True})
active = BooleanField('Active', validators=[DataRequired()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json]) user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json]) system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])

View File

@@ -51,10 +51,12 @@ def edit_partner(partner_id):
partner = Partner.query.get_or_404(partner_id) # This will return a 404 if no partner is found partner = Partner.query.get_or_404(partner_id) # This will return a 404 if no partner is found
tenant = Tenant.query.get_or_404(partner.tenant_id) tenant = Tenant.query.get_or_404(partner.tenant_id)
form = EditPartnerForm(obj=partner) form = EditPartnerForm(obj=partner)
if request.method == 'GET': if request.method == 'GET':
form.tenant.data = tenant.name form.tenant.data = tenant.name
if form.validate_on_submit(): if form.validate_on_submit():
current_app.logger.debug(f"Form data for Partner: {form.data}")
# Populate the user with form data # Populate the user with form data
form.populate_obj(partner) form.populate_obj(partner)
update_logging_information(partner, dt.now(tz.utc)) update_logging_information(partner, dt.now(tz.utc))
@@ -127,10 +129,11 @@ def partner_service():
form = PartnerServiceForm() form = PartnerServiceForm()
if form.validate_on_submit(): if form.validate_on_submit():
partner_id = session.get('partner_id', None) partner = session.get('partner', None)
if not partner_id: if not partner:
flash('No partner has been selected. Set partner before adding services.', 'warning') flash('No partner has been selected. Set partner before adding services.', 'warning')
return redirect(prefixed_url_for('administration_bp.partners')) return redirect(prefixed_url_for('administration_bp.partners'))
partner_id = partner['id']
new_partner_service = PartnerService() new_partner_service = PartnerService()
form.populate_obj(new_partner_service) form.populate_obj(new_partner_service)
set_logging_information(new_partner_service, dt.now(tz.utc)) set_logging_information(new_partner_service, dt.now(tz.utc))
@@ -156,7 +159,8 @@ def partner_service():
@roles_accepted('Super User') @roles_accepted('Super User')
def edit_partner_service(partner_service_id): def edit_partner_service(partner_service_id):
partner_service = PartnerService.query.get_or_404(partner_service_id) partner_service = PartnerService.query.get_or_404(partner_service_id)
partner_id = session.get('partner_id', None) partner = session.get('partner', None)
partner_id = session['partner']['id']
form = EditPartnerServiceForm(obj=partner_service) form = EditPartnerServiceForm(obj=partner_service)
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type, partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
@@ -165,10 +169,21 @@ def edit_partner_service(partner_service_id):
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: " current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
f"{configuration_config}") f"{configuration_config}")
form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration) form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration)
permissions_config = partner_service_config.get('permissions')
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
f"{permissions_config}")
form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions)
if form.validate_on_submit(): if form.validate_on_submit():
current_app.logger.debug(f"Form returned: {form.data}")
raw_form_data = request.form.to_dict()
current_app.logger.debug(f"Raw form data: {raw_form_data}")
form.populate_obj(partner_service) form.populate_obj(partner_service)
partner_service.configuration = form.get_dynamic_data('configuration') partner_service.configuration = form.get_dynamic_data('configuration')
partner_service.permissions = form.get_dynamic_data('permissions')
current_app.logger.debug(f"Partner Service configuration: {partner_service.configuration}")
current_app.logger.debug(f"Partner Service permissions: {partner_service.permissions}")
# update partner relationship # update partner relationship
partner_service.partner_id = partner_id partner_service.partner_id = partner_id
@@ -201,10 +216,11 @@ def edit_partner_service(partner_service_id):
def partner_services(): def partner_services():
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int) per_page = request.args.get('per_page', 10, type=int)
partner_id = session.get('partner_id', None) partner = session.get('partner', None)
if not partner_id: if not partner:
flash('No partner has been selected. Set partner before adding services.', 'warning') flash('No partner has been selected. Set partner before adding services.', 'warning')
return redirect(prefixed_url_for('administration_bp.partners')) return redirect(prefixed_url_for('administration_bp.partners'))
partner_id = session['partner']['id']
query = PartnerService.query.filter(PartnerService.partner_id == partner_id) query = PartnerService.query.filter(PartnerService.partner_id == partner_id)

View File

@@ -1,7 +1,7 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField, from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
validators, ValidationError) validators, ValidationError)
from flask import current_app from flask import current_app, request
import json import json
from wtforms.fields.choices import SelectField from wtforms.fields.choices import SelectField
@@ -56,13 +56,14 @@ class DynamicFormBase(FlaskForm):
self.dynamic_fields = {} self.dynamic_fields = {}
# Store formdata for later use # Store formdata for later use
self.formdata = formdata self.formdata = formdata
self.raw_formdata = request.form.to_dict()
def _create_field_validators(self, field_def): def _create_field_validators(self, field_def):
"""Create validators based on field definition""" """Create validators based on field definition"""
validators_list = [] validators_list = []
# Required validator # Required validator
if field_def.get('required', False): if field_def.get('required', False) and field_def.get('type', None) != 'boolean':
validators_list.append(validators.DataRequired()) validators_list.append(validators.DataRequired())
else: else:
validators_list.append(validators.Optional()) validators_list.append(validators.Optional())
@@ -343,12 +344,16 @@ class DynamicFormBase(FlaskForm):
data = {} data = {}
if collection_name not in self.dynamic_fields: if collection_name not in self.dynamic_fields:
return data return data
prefix_length = len(collection_name) + 1 # +1 for the underscore prefix_length = len(collection_name) + 1 # +1 for the underscore
for full_field_name in self.dynamic_fields[collection_name]: for full_field_name in self.dynamic_fields[collection_name]:
original_field_name = full_field_name[prefix_length:] original_field_name = full_field_name[prefix_length:]
field = getattr(self, full_field_name) field = getattr(self, full_field_name)
# Parse JSON for special field types # Parse JSON for special field types
if isinstance(field, (TaggingFieldsField, TaggingFieldsFilterField, DynamicArgumentsField)) and field.data: if field.type == 'BooleanField':
data[original_field_name] = full_field_name in self.raw_formdata
current_app.logger.debug(f"Value for {original_field_name} is {data[original_field_name]}")
elif isinstance(field, (TaggingFieldsField, TaggingFieldsFilterField, DynamicArgumentsField)) and field.data:
try: try:
data[original_field_name] = json.loads(field.data) data[original_field_name] = json.loads(field.data)
except json.JSONDecodeError: except json.JSONDecodeError:

View File

@@ -106,7 +106,7 @@ class TenantProjectForm(FlaskForm):
services = SelectMultipleField('Allowed Services', choices=[], validators=[DataRequired()]) services = SelectMultipleField('Allowed Services', choices=[], validators=[DataRequired()])
unencrypted_api_key = StringField('Unencrypted API Key', validators=[DataRequired()], render_kw={'readonly': True}) unencrypted_api_key = StringField('Unencrypted API Key', validators=[DataRequired()], render_kw={'readonly': True})
visual_api_key = StringField('Visual API Key', validators=[DataRequired()], render_kw={'readonly': True}) visual_api_key = StringField('Visual API Key', validators=[DataRequired()], render_kw={'readonly': True})
active = BooleanField('Active', validators=[DataRequired()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)
responsible_email = EmailField('Responsible Email', validators=[Optional(), Email()]) responsible_email = EmailField('Responsible Email', validators=[Optional(), Email()])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -120,7 +120,7 @@ class EditTenantProjectForm(FlaskForm):
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
services = SelectMultipleField('Allowed Services', choices=[], validators=[DataRequired()]) services = SelectMultipleField('Allowed Services', choices=[], validators=[DataRequired()])
visual_api_key = StringField('Visual API Key', validators=[DataRequired()], render_kw={'readonly': True}) visual_api_key = StringField('Visual API Key', validators=[DataRequired()], render_kw={'readonly': True})
active = BooleanField('Active', validators=[DataRequired()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)
responsible_email = EmailField('Responsible Email', validators=[Optional(), Email()]) responsible_email = EmailField('Responsible Email', validators=[Optional(), Email()])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -0,0 +1,31 @@
"""Add Permissions to ParterService
Revision ID: cfee2c5bcd7a
Revises: 9ac89fc67661
Create Date: 2025-04-11 07:56:56.802824
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cfee2c5bcd7a'
down_revision = '9ac89fc67661'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('partner_service', schema=None) as batch_op:
batch_op.add_column(sa.Column('permissions', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('partner_service', schema=None) as batch_op:
batch_op.drop_column('permissions')
# ### end Alembic commands ###