diff options
-rw-r--r-- | devel/management/commands/import_signatures.py | 104 | ||||
-rw-r--r-- | media/archweb.css | 12 | ||||
-rw-r--r-- | media/visualize.js | 130 | ||||
-rw-r--r-- | templates/visualize/index.html | 13 | ||||
-rw-r--r-- | visualize/urls.py | 1 | ||||
-rw-r--r-- | visualize/views.py | 48 |
6 files changed, 300 insertions, 8 deletions
diff --git a/devel/management/commands/import_signatures.py b/devel/management/commands/import_signatures.py new file mode 100644 index 00000000..8a4ce873 --- /dev/null +++ b/devel/management/commands/import_signatures.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +""" +import_signatures command + +Import signatures from a given GPG keyring. + +Usage: ./manage.py generate_keyring <keyring_path> +""" + +from datetime import datetime +import logging +import subprocess +import sys + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +from devel.models import PGPSignature + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s -> %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + stream=sys.stderr) +logger = logging.getLogger() + +class Command(BaseCommand): + args = "<keyring_path>" + help = "Import signatures from a given GPG keyring." + + def handle(self, *args, **options): + v = int(options.get('verbosity', None)) + if v == 0: + logger.level = logging.ERROR + elif v == 1: + logger.level = logging.INFO + elif v == 2: + logger.level = logging.DEBUG + + if len(args) < 1: + raise CommandError("keyring_path must be provided") + + import_signatures(args[0]) + +def parse_sigdata(data): + nodes = {} + edges = [] + current_pubkey = None + + # parse all of the output from our successful GPG command + logger.info("parsing command output") + for line in data.split('\n'): + parts = line.split(':') + if parts[0] == 'pub': + current_pubkey = parts[4] + nodes[current_pubkey] = None + if parts[0] == 'uid': + uid = parts[9] + # only set uid if this is the first one encountered + if nodes[current_pubkey] is None: + nodes[current_pubkey] = uid + if parts[0] == 'sig': + created = datetime.utcfromtimestamp(int(parts[5])) + expires = None + if parts[6]: + expires = datetime.utcfromtimestamp(int(parts[6])) + valid = parts[1] != '-' + edge = (parts[4], current_pubkey, created, expires, valid) + edges.append(edge) + + return nodes, edges + + +def import_signatures(keyring): + gpg_cmd = ["gpg", "--no-default-keyring", "--keyring", keyring, + "--list-sigs", "--with-colons", "--fixed-list-mode"] + logger.info("running command: %r", gpg_cmd) + proc = subprocess.Popen(gpg_cmd, stdout=subprocess.PIPE) + outdata, errdata = proc.communicate() + if proc.returncode != 0: + logger.error(errdata) + raise subprocess.CalledProcessError(proc.returncode, gpg_cmd) + + nodes, edges = parse_sigdata(outdata) + + # now prune the data down to what we actually want. + # prune edges not in nodes, remove duplicates, and self-sigs + pruned_edges = set(edge for edge in edges + if edge[0] in nodes and edge[0] != edge[1]) + + logger.info("creating or finding %d signatures", len(pruned_edges)) + created_ct = 0 + with transaction.commit_on_success(): + for edge in pruned_edges: + _, created = PGPSignature.objects.get_or_create( + signer=edge[0], signee=edge[1], + created=edge[2], expires=edge[3], + defaults={ 'valid': edge[4] }) + if created: + created_ct += 1 + + logger.info("created %d signatures", created_ct) + +# vim: set ts=4 sw=4 et: diff --git a/media/archweb.css b/media/archweb.css index bfb712f8..2cba2864 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -986,3 +986,15 @@ ul.signoff-list { font-size: 0.85em; line-height: 1em; } + +#visualize-keys svg { + width: 100%; +} + + #visualize-keys circle { + stroke-width: 1.5px; + } + + #visualize-keys line { + stroke: #888; + } diff --git a/media/visualize.js b/media/visualize.js index fa89a613..bd7b6679 100644 --- a/media/visualize.js +++ b/media/visualize.js @@ -19,7 +19,7 @@ */ function packages_treemap(chart_id, orderings, default_order) { - var jq_div = $(chart_id), + var jq_div = jQuery(chart_id), color = d3.scale.category20(); var key_func = function(d) { return d.key; }; var value_package_count = function(d) { return d.count; }, @@ -129,7 +129,7 @@ function packages_treemap(chart_id, orderings, default_order) { }); }; - $.each(orderings, function(k, v) { + jQuery.each(orderings, function(k, v) { make_group_button(k, v); }); @@ -140,7 +140,131 @@ function packages_treemap(chart_id, orderings, default_order) { .data(treemap.size([jq_div.width(), jq_div.height()]), key_func) .call(cell); }; - $(window).resize(function() { + jQuery(window).resize(function() { + if (resize_timeout) { + clearTimeout(resize_timeout); + } + resize_timeout = setTimeout(real_resize, 200); + }); +} + +function developer_keys(chart_id, data_url) { + var jq_div = jQuery(chart_id), + r = 10; + + var force = d3.layout.force() + .gravity(0.1) + .charge(-200) + .linkStrength(0.2) + .size([jq_div.width(), jq_div.height()]); + + var svg = d3.select(chart_id) + .append("svg"); + + d3.json(data_url, function(json) { + var fill = d3.scale.category20(); + + var index_for_key = function(key) { + var i; + key = key.slice(-8); + for (i = 0; i < json.nodes.length; i++) { + var node_key = json.nodes[i].key; + if (node_key && node_key.slice(-8) === key) { + return i; + } + } + }; + + /* filter edges to only include those that we have two nodes for */ + var edges = jQuery.grep(json.edges, function(d, i) { + d.source = index_for_key(d.signer); + d.target = index_for_key(d.signee); + return d.source >= 0 && d.target >= 0; + }); + + jQuery.map(json.nodes, function(d, i) { d.master_sigs = 0; }); + jQuery.map(edges, function(d, i) { + if (json.nodes[d.source].group === "master") { + json.nodes[d.target].master_sigs += 1; + } + }); + jQuery.map(json.nodes, function(d, i) { + if (d.group === "dev" || d.group === "tu") { + d.approved = d.master_sigs >= 3; + } else { + d.approved = null; + } + }); + + var link = svg.selectAll("line") + .data(edges) + .enter() + .append("line"); + + var node = svg.selectAll("circle") + .data(json.nodes) + .enter().append("circle") + .attr("r", function(d) { + switch (d.group) { + case "master": + return r * 1.6 - 0.75; + case "cacert": + return r * 1.4 - 0.75; + case "dev": + case "tu": + default: + return r - 0.75; + } + }) + .style("fill", function(d) { return fill(d.group); }) + .style("stroke", function(d) { + if (d.approved === null) { + return d3.rgb(fill(d.group)).darker(); + } else if (d.approved) { + return "green"; + } else { + return "red"; + } + }) + .call(force.drag); + node.append("title").text(function(d) { return d.name; }); + + var distance = function(d, i) { + /* place a long line between all master keys and other keys. + * however, other connected clusters should be close together. */ + if (d.source.group === "master" || d.target.group === "master") { + return 200; + } else { + return 50; + } + }; + + var tick = function() { + var offset = r * 2, + w = jq_div.width(), + h = jq_div.height(); + node.attr("cx", function(d) { return (d.x = Math.max(offset, Math.min(w - offset, d.x))); }) + .attr("cy", function(d) { return (d.y = Math.max(offset, Math.min(h - offset, d.y))); }); + + link.attr("x1", function(d) { return d.source.x; }) + .attr("y1", function(d) { return d.source.y; }) + .attr("x2", function(d) { return d.target.x; }) + .attr("y2", function(d) { return d.target.y; }); + }; + + force.nodes(json.nodes) + .links(edges) + .linkDistance(distance) + .on("tick", tick) + .start(); + }); + + var resize_timeout = null; + var real_resize = function() { + resize_timeout = null; + force.size([jq_div.width(), jq_div.height()]); + }; + jQuery(window).resize(function() { if (resize_timeout) { clearTimeout(resize_timeout); } diff --git a/templates/visualize/index.html b/templates/visualize/index.html index cc4593d2..1854eb58 100644 --- a/templates/visualize/index.html +++ b/templates/visualize/index.html @@ -4,10 +4,7 @@ {% block content %} <div class="box"> - - <h2>Visualizations of Packaging Data</h2> - - <h3>Package Treemap</h3> + <h2>Visualization of Package Data</h2> <div class="visualize-buttons"> <div> @@ -25,9 +22,16 @@ </div> <div id="visualize-archrepo" class="visualize-chart"></div> </div> +{% comment %} +<div class="box"> + <h2>Visualization of PGP Master and Signing Keys</h2> + <div id="visualize-keys" class="visualize-chart"></div> +</div> +{% endcomment %} {% load cdn %}{% jquery %} <script type="text/javascript" src="/media/d3.min.js"></script> +<script type="text/javascript" src="/media/d3.geom.min.js"></script> <script type="text/javascript" src="/media/d3.layout.min.js"></script> <script type="text/javascript" src="/media/archweb.js"></script> <script type="text/javascript" src="/media/visualize.js"></script> @@ -38,6 +42,7 @@ $(document).ready(function() { "arch": { url: "{% url visualize-byarch %}", color_attr: "arch" }, }; packages_treemap("#visualize-archrepo", orderings, "repo"); + /*developer_keys("#visualize-keys", "{% url visualize-pgp_keys %}");*/ }); </script> {% endblock %} diff --git a/visualize/urls.py b/visualize/urls.py index 57ee0626..02d83bec 100644 --- a/visualize/urls.py +++ b/visualize/urls.py @@ -4,6 +4,7 @@ urlpatterns = patterns('visualize.views', (r'^$', 'index', {}, 'visualize-index'), (r'^by_arch/$', 'by_arch', {}, 'visualize-byarch'), (r'^by_repo/$', 'by_repo', {}, 'visualize-byrepo'), + (r'^pgp_keys/$', 'pgp_keys', {}, 'visualize-pgp_keys'), ) # vim: set ts=4 sw=4 et: diff --git a/visualize/views.py b/visualize/views.py index f2b1d63b..be6057b2 100644 --- a/visualize/views.py +++ b/visualize/views.py @@ -1,10 +1,14 @@ -from django.db.models import Count, Sum +from datetime import datetime + +from django.contrib.auth.models import User +from django.db.models import Count, Sum, Q from django.http import HttpResponse from django.utils import simplejson from django.views.decorators.cache import cache_page from django.views.generic.simple import direct_to_template from main.models import Package, Arch, Repo +from devel.models import MasterKey, PGPSignature def index(request): return direct_to_template(request, 'visualize/index.html', {}) @@ -66,4 +70,46 @@ def by_repo(request): to_json = simplejson.dumps(data['by_repo'], ensure_ascii=False) return HttpResponse(to_json, mimetype='application/json') + +@cache_page(1800) +def pgp_keys(request): + node_list = [] + + users = User.objects.filter(is_active=True).select_related('userprofile') + node_list.extend({ + 'name': dev.get_full_name(), + 'key': dev.userprofile.pgp_key, + 'group': 'dev' + } for dev in users.filter(groups__name='Developers')) + node_list.extend({ + 'name': tu.get_full_name(), + 'key': tu.userprofile.pgp_key, + 'group': 'tu' + } for tu in users.filter(groups__name='Trusted Users').exclude( + groups__name='Developers')) + + master_keys = MasterKey.objects.select_related('owner').filter( + revoked__isnull=True) + node_list.extend({ + 'name': 'Master Key (%s)' % key.owner.get_full_name(), + 'key': key.pgp_key, + 'group': 'master' + } for key in master_keys) + + node_list.append({ + 'name': 'CA Cert Signing Authority', + 'key': 'A31D4F81EF4EBD07B456FA04D2BB0D0165D0FD58', + 'group': 'cacert', + }) + + not_expired = Q(expires__gt=datetime.now) | Q(expires__isnull=True) + signatures = PGPSignature.objects.filter(not_expired, valid=True) + edge_list = [{ 'signee': sig.signee, 'signer': sig.signer } + for sig in signatures] + + data = { 'nodes': node_list, 'edges': edge_list } + + to_json = simplejson.dumps(data, ensure_ascii=False) + return HttpResponse(to_json, mimetype='application/json') + # vim: set ts=4 sw=4 et: |