- Added permissions to the partner service configuration
- Corrected a nasty bug where dynamic boolean fields were not returned correctly
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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 ###
|
||||||
Reference in New Issue
Block a user