WIP: manager: offer to refresh the role summary cache (#83078) #174

Draft
ecazenave wants to merge 1 commits from wip/83078-role-summary-refresh-cache into main
8 changed files with 113 additions and 17 deletions

View File

@ -16,11 +16,12 @@
from django.core.management.base import BaseCommand
from authentic2.role_summary import write_roles_summary_cache
from authentic2.role_summary import RoleSummaryJob, write_roles_summary_cache
class Command(BaseCommand):
help = 'Build role summary cache'
def handle(self, *args, **options):
write_roles_summary_cache()
role_summary_job = RoleSummaryJob.new()
write_roles_summary_cache(role_summary_job.uuid)

View File

@ -38,8 +38,8 @@ from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role, Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.journal.views import JournalViewWithContext
from authentic2.forms.profile import modelform_factory
from authentic2.role_summary import get_roles_summary_cache
from authentic2.utils import crypto, hooks
from authentic2.role_summary import RoleSummaryJob, get_roles_summary_cache
from authentic2.utils import crypto, hooks, spooler
from authentic2.utils.misc import redirect
from . import forms, resources, tables, views
@ -761,6 +761,20 @@ class RolesJournal(views.SearchOUMixin, views.PermissionMixin, JournalViewWithCo
roles_journal = RolesJournal.as_view()
class RolesSummaryRefresh(views.PermissionMixin, views.TitleMixin, views.MediaMixin, TemplateView):
template_name = 'authentic2/manager/roles_summary_refresh.html'
permissions = ['a2_rbac.view_role']
title = _('Refresh roles summary')
def post(self, request, *args, **kwargs):
role_summary_job = RoleSummaryJob.new()
spooler.refresh_roles_summary_cache(role_summary_job.uuid)
return redirect(self.request, reverse('a2-manager-roles'))
roles_summary_refresh = RolesSummaryRefresh.as_view()
class UserOrRoleSelect2View(DetailView):
form_class = forms.ChooseUserOrRoleForm
model = Role

View File

@ -16,6 +16,7 @@
{% endif %}
<ul class="extra-actions-menu">
<li><a href="{% url "a2-manager-roles-journal" %}{% if multiple_ou and ou %}?search-ou={{ ou.id }}{% endif %}">{% trans "Journal" %}</a></li>
<li><a href="{% url 'a2-manager-roles-summary-refresh' %}" rel="popup">{% trans "Role summary refresh" %}</a></li>
<li><a download href="{% url 'a2-manager-roles-export' format="json" %}?{{ request.GET.urlencode }}">{% trans 'Export' %}</a></li>
{% if view.can_add %}
<li><a href="{% url 'a2-manager-roles-import' %}?{{ request.GET.urlencode }}" rel="popup">{% trans 'Import' %}</a></li>

View File

@ -0,0 +1,27 @@
{% extends "authentic2/manager/form.html" %}
{% load i18n gadjo %}
{% block page-title %}{% trans "Role summary refresh" %}{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'a2-manager-roles' %}">{% trans 'Roles' %}</a>
<a href="#">{{ object }}</a>
{% endblock %}
{% block appbar %}
<h2>{{ title }}</h2>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
<div class="form-inner-container">
<p>{% blocktrans %}Do you want to refresh roles summary ?{% endblocktrans %}</p>
<div class="buttons">
<button>{% trans "Confirm" %}</button>
<a class="cancel" href="{% url 'a2-manager-roles' %}">{% trans "Cancel" %}</a>
</div>
</div>
</form>
{% endblock %}

View File

@ -121,6 +121,11 @@ urlpatterns = required(
path('roles/add/', role_views.add, name='a2-manager-role-add'),
re_path(r'^roles/export/(?P<format>csv|json)/$', role_views.export, name='a2-manager-roles-export'),
path('roles/journal/', role_views.roles_journal, name='a2-manager-roles-journal'),
path(
'roles/summary-refresh/',
role_views.roles_summary_refresh,
name='a2-manager-roles-summary-refresh',
),
path('roles/<int:pk>/', role_views.members, name='a2-manager-role-members'),
re_path(
r'^roles/uuid:(?P<slug>[a-z0-9]+)/$', role_views.members, name='a2-manager-roles-by-uuid-detail'

View File

@ -17,9 +17,11 @@
import json
import os.path
import urllib
import uuid
import requests
from django.conf import settings
from django.core.files.storage import default_storage
from django.db import connection
from .a2_rbac.models import Role
@ -30,7 +32,48 @@ except ImportError: # fallback on python requests, no Publik signature
from requests.sessions import Session as Requests # pylint: disable=ungrouped-imports
def build_role_summary_cache():
def iter_services():
try:
services = settings.KNOWN_SERVICES
except AttributeError:
services = {}
for service_type, services in settings.KNOWN_SERVICES.items():
if service_type == 'authentic':
continue
yield services
def total():
service_slugs = []
for services in iter_services():
service_slugs.extend(services.keys())
return len(service_slugs)
class RoleSummaryJob:
def __init__(self, uuid):
self.uuid = uuid
self.base_path = os.path.join(default_storage.path('role_summary'), self.uuid)
self.progress_path = os.path.join(self.base_path, 'progress')
@classmethod
def new(cls):
job = cls(str(uuid.uuid4()))
os.makedirs(job.base_path)
return job
@property
def progress(self):
with open(self.progress_path) as f:
result = f.read()
return int(result) if result else 0
def set_progress(self, number):
with open(self.progress_path, 'w') as f:
f.write(str(number))
def build_role_summary_cache(job_uuid):
def _requests(url):
try:
resp = Requests().get(url=url, timeout=settings.REQUESTS_TIMEOUT)
@ -55,15 +98,11 @@ def build_role_summary_cache():
},
}
try:
services = settings.KNOWN_SERVICES
except AttributeError:
services = {}
progress = 0
job = RoleSummaryJob(job_uuid)
job.set_progress(progress)
data = {}
for service_type, services in settings.KNOWN_SERVICES.items():
if service_type == 'authentic':
continue
for services in iter_services():
for service_data in services.values():
url = urllib.parse.urljoin(service_data['url'], 'api/export-import/')
for type_object in _requests(url):
@ -93,7 +132,8 @@ def build_role_summary_cache():
data[role_uuid][match_key].append(type_object_copy)
if object not in data[role_uuid][match_key][-1]['hit']:
data[role_uuid][match_key][-1]['hit'].append(object)
progress += 1
job.set_progress(progress)
return data
@ -113,9 +153,9 @@ def get_roles_summary_cache():
return json.load(fd)
def write_roles_summary_cache():
def write_roles_summary_cache(uuid):
path = get_role_summary_cache_path()
if not path:
return
with open(path, 'w') as fd:
json.dump(build_role_summary_cache(), fd, indent=2)
json.dump(build_role_summary_cache(uuid), fd, indent=2)

View File

@ -91,3 +91,10 @@ def export_users(uuid, query):
from authentic2.manager.user_export import export_users_to_file
export_users_to_file(uuid, query)
@tenantspool
def refresh_roles_summary_cache(uuid):
from authentic2.role_summary import write_roles_summary_cache
write_roles_summary_cache(uuid)

View File

@ -114,7 +114,8 @@ def test_role_summary(db, simple_role, superuser, tmpdir, monkeypatch):
},
)
data = role_summary.build_role_summary_cache()
role_summary_job = role_summary.RoleSummaryJob.new()
data = role_summary.build_role_summary_cache(role_summary_job.uuid)
assert data == {
simple_role.uuid: {
'name': 'simple role',