diff options
Diffstat (limited to 'rpkid/rpki/gui/app')
-rw-r--r-- | rpkid/rpki/gui/app/forms.py | 67 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/migrations/0006_add_conf_acl.py | 168 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/migrations/0007_default_acls.py | 165 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/models.py | 14 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/app_base.html | 8 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/app_confirm_delete.html | 21 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/app_form.html | 4 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/resource_holder_list.html | 35 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/user_confirm_delete.html | 20 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/user_list.html | 43 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/urls.py | 4 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/views.py | 198 |
12 files changed, 623 insertions, 124 deletions
diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py index 1d354521..ae694568 100644 --- a/rpkid/rpki/gui/app/forms.py +++ b/rpkid/rpki/gui/app/forms.py @@ -108,32 +108,29 @@ class ImportClientForm(forms.Form): class UserCreateForm(forms.Form): - handle = forms.CharField(max_length=30, help_text='handle for new child') + username = forms.CharField(max_length=30) email = forms.CharField(max_length=30, help_text='email address for new user') password = forms.CharField(widget=forms.PasswordInput) password2 = forms.CharField(widget=forms.PasswordInput, label='Confirm Password') - parent = forms.ModelChoiceField(required=False, - queryset=models.Conf.objects.all(), - help_text='optionally make a child of') + resource_holders = forms.ModelMultipleChoiceField( + queryset=models.Conf.objects.all(), + help_text='allowed to manage these resource holders' - def clean_handle(self): - handle = self.cleaned_data.get('handle') - if (handle and models.Conf.objects.filter(handle=handle).exists() or - User.objects.filter(username=handle).exists()): + ) + + def clean_username(self): + username = self.cleaned_data.get('username') + if User.objects.filter(username=username).exists(): raise forms.ValidationError('user already exists') - return handle + return username def clean(self): p1 = self.cleaned_data.get('password') p2 = self.cleaned_data.get('password2') if p1 != p2: raise forms.ValidationError('passwords do not match') - handle = self.cleaned_data.get('handle') - parent = self.cleaned_data.get('parent') - if handle and parent and parent.children.filter(handle=handle).exists(): - raise forms.ValidationError('parent already has a child by that name') return self.cleaned_data @@ -143,7 +140,11 @@ class UserEditForm(forms.Form): pw = forms.CharField(widget=forms.PasswordInput, label='Password', required=False) pw2 = forms.CharField(widget=forms.PasswordInput, label='Confirm password', - required=False) + required=False) + resource_holders = forms.ModelMultipleChoiceField( + queryset=models.Conf.objects.all(), + help_text='allowed to manage these resource holders' + ) def clean(self): p1 = self.cleaned_data.get('pw') @@ -400,3 +401,41 @@ def ChildForm(instance): class Empty(forms.Form): """Stub form for views requiring confirmation.""" pass + + +class ResourceHolderForm(forms.Form): + """form for editing ACL on Conf objects.""" + users = forms.ModelMultipleChoiceField( + queryset=User.objects.all(), + help_text='users allowed to mange this resource holder' + ) + + +class ResourceHolderCreateForm(forms.Form): + """form for creating new resource holdres.""" + handle = forms.CharField(max_length=30) + parent = forms.ModelChoiceField( + required=False, + queryset=models.Conf.objects.all(), + help_text='optionally make the new resource holder a child of this resource holder' + ) + users = forms.ModelMultipleChoiceField( + required=False, + queryset=User.objects.all(), + help_text='users allowed to mange this resource holder' + ) + + def clean_handle(self): + handle = self.cleaned_data.get('handle') + if models.Conf.objects.filter(handle=handle).exists(): + raise forms.ValidationError( + 'a resource holder with that handle already exists' + ) + return handle + + def clean(self): + handle = self.cleaned_data.get('handle') + parent = self.cleaned_data.get('parent') + if handle and parent and parent.children.filter(handle=handle).exists(): + raise forms.ValidationError('parent already has a child by that name') + return self.cleaned_data diff --git a/rpkid/rpki/gui/app/migrations/0006_add_conf_acl.py b/rpkid/rpki/gui/app/migrations/0006_add_conf_acl.py new file mode 100644 index 00000000..88fe8171 --- /dev/null +++ b/rpkid/rpki/gui/app/migrations/0006_add_conf_acl.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'ConfACL' + db.create_table('app_confacl', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('conf', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['irdb.ResourceHolderCA'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + )) + db.send_create_signal('app', ['ConfACL']) + + # Adding unique constraint on 'ConfACL', fields ['user', 'conf'] + db.create_unique('app_confacl', ['user_id', 'conf_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'ConfACL', fields ['user', 'conf'] + db.delete_unique('app_confacl', ['user_id', 'conf_id']) + + # Deleting model 'ConfACL' + db.delete_table('app_confacl') + + + models = { + 'app.confacl': { + 'Meta': {'unique_together': "(('user', 'conf'),)", 'object_name': 'ConfACL'}, + 'conf': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['irdb.ResourceHolderCA']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'app.ghostbusterrequest': { + 'Meta': {'ordering': "('family_name', 'given_name')", 'object_name': 'GhostbusterRequest', '_ormbases': ['irdb.GhostbusterRequest']}, + 'additional_name': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}), + 'box': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'code': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'extended': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'family_name': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'full_name': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'ghostbusterrequest_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['irdb.GhostbusterRequest']", 'unique': 'True', 'primary_key': 'True'}), + 'given_name': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'honorific_prefix': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'honorific_suffix': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'organization': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'region': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'street': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'telephone': ('rpki.gui.app.models.TelephoneField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}) + }, + 'app.resourcecert': { + 'Meta': {'object_name': 'ResourceCert'}, + 'conf': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'certs'", 'to': "orm['irdb.ResourceHolderCA']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'not_after': ('django.db.models.fields.DateTimeField', [], {}), + 'not_before': ('django.db.models.fields.DateTimeField', [], {}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'certs'", 'null': 'True', 'to': "orm['irdb.Parent']"}), + 'uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'app.resourcerangeaddressv4': { + 'Meta': {'ordering': "('prefix_min',)", 'object_name': 'ResourceRangeAddressV4'}, + 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'address_ranges'", 'to': "orm['app.ResourceCert']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'prefix_max': ('rpki.gui.models.IPv4AddressField', [], {'db_index': 'True'}), + 'prefix_min': ('rpki.gui.models.IPv4AddressField', [], {'db_index': 'True'}) + }, + 'app.resourcerangeaddressv6': { + 'Meta': {'ordering': "('prefix_min',)", 'object_name': 'ResourceRangeAddressV6'}, + 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'address_ranges_v6'", 'to': "orm['app.ResourceCert']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'prefix_max': ('rpki.gui.models.IPv6AddressField', [], {'db_index': 'True'}), + 'prefix_min': ('rpki.gui.models.IPv6AddressField', [], {'db_index': 'True'}) + }, + 'app.resourcerangeas': { + 'Meta': {'ordering': "('min', 'max')", 'object_name': 'ResourceRangeAS'}, + 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'asn_ranges'", 'to': "orm['app.ResourceCert']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'max': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'min': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'app.timestamp': { + 'Meta': {'object_name': 'Timestamp'}, + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}), + 'ts': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'irdb.ghostbusterrequest': { + 'Meta': {'object_name': 'GhostbusterRequest'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ghostbuster_requests'", 'to': "orm['irdb.ResourceHolderCA']"}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ghostbuster_requests'", 'null': 'True', 'to': "orm['irdb.Parent']"}), + 'vcard': ('django.db.models.fields.TextField', [], {}) + }, + 'irdb.parent': { + 'Meta': {'unique_together': "(('issuer', 'handle'),)", 'object_name': 'Parent', '_ormbases': ['irdb.Turtle']}, + 'certificate': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}), + 'child_handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}), + 'handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}), + 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parents'", 'to': "orm['irdb.ResourceHolderCA']"}), + 'parent_handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}), + 'referral_authorization': ('rpki.irdb.models.SignedReferralField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'referrer': ('rpki.irdb.models.HandleField', [], {'max_length': '120', 'null': 'True', 'blank': 'True'}), + 'repository_type': ('rpki.irdb.models.EnumField', [], {}), + 'ta': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}), + 'turtle_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['irdb.Turtle']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'irdb.resourceholderca': { + 'Meta': {'object_name': 'ResourceHolderCA'}, + 'certificate': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}), + 'handle': ('rpki.irdb.models.HandleField', [], {'unique': 'True', 'max_length': '120'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_crl_update': ('rpki.irdb.models.SundialField', [], {}), + 'latest_crl': ('rpki.irdb.models.CRLField', [], {'default': 'None', 'blank': 'True'}), + 'next_crl_number': ('django.db.models.fields.BigIntegerField', [], {'default': '1'}), + 'next_crl_update': ('rpki.irdb.models.SundialField', [], {}), + 'next_serial': ('django.db.models.fields.BigIntegerField', [], {'default': '1'}), + 'private_key': ('rpki.irdb.models.RSAKeyField', [], {'default': 'None', 'blank': 'True'}) + }, + 'irdb.turtle': { + 'Meta': {'object_name': 'Turtle'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service_uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['app']
\ No newline at end of file diff --git a/rpkid/rpki/gui/app/migrations/0007_default_acls.py b/rpkid/rpki/gui/app/migrations/0007_default_acls.py new file mode 100644 index 00000000..40656d0f --- /dev/null +++ b/rpkid/rpki/gui/app/migrations/0007_default_acls.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from django.core.exceptions import ObjectDoesNotExist + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..." + for conf in orm['irdb.ResourceHolderCA'].objects.all(): + try: + user = orm['auth.User'].objects.get(username=conf.handle) + orm['app.ConfACL'].objects.create( + conf=conf, + user=user + ) + except ObjectDoesNotExist: + pass + + def backwards(self, orm): + "Write your backwards methods here." + orm['app.ConfACL'].objects.all().delete() + + models = { + 'app.confacl': { + 'Meta': {'unique_together': "(('user', 'conf'),)", 'object_name': 'ConfACL'}, + 'conf': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['irdb.ResourceHolderCA']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'app.ghostbusterrequest': { + 'Meta': {'ordering': "('family_name', 'given_name')", 'object_name': 'GhostbusterRequest', '_ormbases': ['irdb.GhostbusterRequest']}, + 'additional_name': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}), + 'box': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'code': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'extended': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'family_name': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'full_name': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'ghostbusterrequest_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['irdb.GhostbusterRequest']", 'unique': 'True', 'primary_key': 'True'}), + 'given_name': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'honorific_prefix': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'honorific_suffix': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'organization': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'region': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'street': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'telephone': ('rpki.gui.app.models.TelephoneField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}) + }, + 'app.resourcecert': { + 'Meta': {'object_name': 'ResourceCert'}, + 'conf': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'certs'", 'to': "orm['irdb.ResourceHolderCA']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'not_after': ('django.db.models.fields.DateTimeField', [], {}), + 'not_before': ('django.db.models.fields.DateTimeField', [], {}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'certs'", 'null': 'True', 'to': "orm['irdb.Parent']"}), + 'uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'app.resourcerangeaddressv4': { + 'Meta': {'ordering': "('prefix_min',)", 'object_name': 'ResourceRangeAddressV4'}, + 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'address_ranges'", 'to': "orm['app.ResourceCert']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'prefix_max': ('rpki.gui.models.IPv4AddressField', [], {'db_index': 'True'}), + 'prefix_min': ('rpki.gui.models.IPv4AddressField', [], {'db_index': 'True'}) + }, + 'app.resourcerangeaddressv6': { + 'Meta': {'ordering': "('prefix_min',)", 'object_name': 'ResourceRangeAddressV6'}, + 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'address_ranges_v6'", 'to': "orm['app.ResourceCert']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'prefix_max': ('rpki.gui.models.IPv6AddressField', [], {'db_index': 'True'}), + 'prefix_min': ('rpki.gui.models.IPv6AddressField', [], {'db_index': 'True'}) + }, + 'app.resourcerangeas': { + 'Meta': {'ordering': "('min', 'max')", 'object_name': 'ResourceRangeAS'}, + 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'asn_ranges'", 'to': "orm['app.ResourceCert']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'max': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'min': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'app.timestamp': { + 'Meta': {'object_name': 'Timestamp'}, + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}), + 'ts': ('django.db.models.fields.DateTimeField', [], {}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'irdb.ghostbusterrequest': { + 'Meta': {'object_name': 'GhostbusterRequest'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ghostbuster_requests'", 'to': "orm['irdb.ResourceHolderCA']"}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ghostbuster_requests'", 'null': 'True', 'to': "orm['irdb.Parent']"}), + 'vcard': ('django.db.models.fields.TextField', [], {}) + }, + 'irdb.parent': { + 'Meta': {'unique_together': "(('issuer', 'handle'),)", 'object_name': 'Parent', '_ormbases': ['irdb.Turtle']}, + 'certificate': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}), + 'child_handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}), + 'handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}), + 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parents'", 'to': "orm['irdb.ResourceHolderCA']"}), + 'parent_handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}), + 'referral_authorization': ('rpki.irdb.models.SignedReferralField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'referrer': ('rpki.irdb.models.HandleField', [], {'max_length': '120', 'null': 'True', 'blank': 'True'}), + 'repository_type': ('rpki.irdb.models.EnumField', [], {}), + 'ta': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}), + 'turtle_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['irdb.Turtle']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'irdb.resourceholderca': { + 'Meta': {'object_name': 'ResourceHolderCA'}, + 'certificate': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}), + 'handle': ('rpki.irdb.models.HandleField', [], {'unique': 'True', 'max_length': '120'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_crl_update': ('rpki.irdb.models.SundialField', [], {}), + 'latest_crl': ('rpki.irdb.models.CRLField', [], {'default': 'None', 'blank': 'True'}), + 'next_crl_number': ('django.db.models.fields.BigIntegerField', [], {'default': '1'}), + 'next_crl_update': ('rpki.irdb.models.SundialField', [], {}), + 'next_serial': ('django.db.models.fields.BigIntegerField', [], {'default': '1'}), + 'private_key': ('rpki.irdb.models.RSAKeyField', [], {'default': 'None', 'blank': 'True'}) + }, + 'irdb.turtle': { + 'Meta': {'object_name': 'Turtle'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service_uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['app'] + symmetrical = True diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py index fdb18b73..9c4c9890 100644 --- a/rpkid/rpki/gui/app/models.py +++ b/rpkid/rpki/gui/app/models.py @@ -16,6 +16,7 @@ __version__ = '$Id$' from django.db import models +from django.contrib.auth.models import User import rpki.resource_set import rpki.exceptions @@ -289,3 +290,16 @@ class RouteOriginV6(rpki.gui.routeview.models.RouteOriginV6): @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.route_detail', [str(self.pk)]) + + +class ConfACL(models.Model): + """Stores access control for which users are allowed to manage a given + resource handle. + + """ + + conf = models.ForeignKey(Conf) + user = models.ForeignKey(User) + + class Meta: + unique_together = (('user', 'conf')) diff --git a/rpkid/rpki/gui/app/templates/app/app_base.html b/rpkid/rpki/gui/app/templates/app/app_base.html index f7ec0927..2738e4da 100644 --- a/rpkid/rpki/gui/app/templates/app/app_base.html +++ b/rpkid/rpki/gui/app/templates/app/app_base.html @@ -13,10 +13,14 @@ <li><a href="{% url rpki.gui.app.views.route_view %}">routes</a></li> </ul> -{% if request.user.is_superuser %} <ul class='unstyled'> <li><a href="{% url rpki.gui.app.views.conf_list %}" title="select a different resource handle to manage">select identity</a></li> - <li><a href="{% url rpki.gui.app.views.user_list %}" title="manage users"><i class="icon-user"></i> users</a></li> +</ul> + +{% if request.user.is_superuser %} +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.user_list %}" title="manage users"><i class="icon-user"></i> web users</a></li> + <li><a href="{% url rpki.gui.app.views.resource_holder_list %}" title="manage resource holders"><i class="icon-user"></i> resource holders</a></li> <li><a href="{% url rpki.gui.app.views.client_list %}" title="manage repository clients">repository clients</a></li> </ul> {% endif %} diff --git a/rpkid/rpki/gui/app/templates/app/app_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/app_confirm_delete.html new file mode 100644 index 00000000..7c35a733 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/app_confirm_delete.html @@ -0,0 +1,21 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>{{ form_title }}</h1> +</div> + +<div class='alert alert-block'> + <h4>Warning!</h4> + <strong>Please confirm</strong> that you would like to delete this object. +</div> + +<form method='POST' action=""> + {% csrf_token %} + {{ form }} + <div class="form-actions"> + <input class='btn btn-danger' value='Delete' type='submit'> + <a class='btn' href="{{ cancel_url }}">Cancel</a> + </div> +</form> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/app_form.html b/rpkid/rpki/gui/app/templates/app/app_form.html index 4688d726..c2c58ae6 100644 --- a/rpkid/rpki/gui/app/templates/app/app_form.html +++ b/rpkid/rpki/gui/app/templates/app/app_form.html @@ -5,12 +5,12 @@ <h1>{{ form_title }}</h1> </div> -<form method='POST' action='{{ request.get_full_path }}' class="form-horizontal"> +<form method="POST" action="" class="form-horizontal"> {% csrf_token %} {% include "app/bootstrap_form.html" %} <div class="form-actions"> <input class='btn btn-primary' type='submit' value='Save'> - <a class='btn' href="{% url rpki.gui.app.views.dashboard %}">Cancel</a> + <a class='btn' href="{{ cancel_url }}">Cancel</a> </div> </form> {% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/resource_holder_list.html b/rpkid/rpki/gui/app/templates/app/resource_holder_list.html new file mode 100644 index 00000000..3d1c2366 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/resource_holder_list.html @@ -0,0 +1,35 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Resource Holders</h1> +</div> + +<p> +This page lists all of the resource holders that are currently managed by this server. +Note that this is distinct from the +<a href="{% url rpki.gui.app.views.user_list %}">list of web interface users</a>. +</p> + +<table class='table table-striped'> + <thead> + <tr> + <th>Handle</th> + <th>Action</th> + </tr> + </thead> + <tbody> + {% for conf in object_list %} + <tr> + <td>{{ conf.handle }}</td> + <td> + <a class='btn btn-small' href='{% url rpki.gui.app.views.resource_holder_edit conf.pk %}' title="Edit"><i class="icon-edit"></i></a> + <a class='btn btn-small' href='{% url rpki.gui.app.views.resource_holder_delete conf.pk %}' title="Delete"><i class="icon-trash"></i></a> + </td> + </tr> + {% endfor %} + </tbody> +</table> + +<a class="btn" href="{% url rpki.gui.app.views.resource_holder_create %}"><i class="icon-plus-sign"></i> Create</a> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html deleted file mode 100644 index 76c66775..00000000 --- a/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "app/app_base.html" %} - -{% block content %} -<div class='page-title'> - <h1>Delete User</h1> -</div> - -<div class='alert-message block-message warning'> - <p><strong>Please confirm</strong> that you would like to delete the following user account. - <h2>{{ object.handle }}</h2> - <div class='alert-actions'> - <form method='POST' action='{{ request.get_full_path }}'> - {% csrf_token %} - {{ form }} - <input class='btn danger' value='Delete' type='submit'> - <a class='btn' href='{% url rpki.gui.app.views.user_list %}'>Cancel</a> - </form> - </div> -</div> -{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/user_list.html b/rpkid/rpki/gui/app/templates/app/user_list.html index fed943f0..16ebf6c0 100644 --- a/rpkid/rpki/gui/app/templates/app/user_list.html +++ b/rpkid/rpki/gui/app/templates/app/user_list.html @@ -5,23 +5,32 @@ <h1>Users</h1> </div> -<table class='table table-striped table-condensed'> - <tr> - <th>Username</th> - <th>Email</th> - <th></th> - </tr> - {% for u in users %} - <tr> - <td>{{ u.0.handle }}</td> - <td>{{ u.1.email }}</td> - <td> - <a class='btn btn-small' href='{% url rpki.gui.app.views.user_edit u.0.pk %}' title="Edit"><i class="icon-edit"></i></a> - <a class='btn btn-small' href='{% url rpki.gui.app.views.user_delete u.0.pk %}' title="Delete"><i class="icon-trash"></i></a> - </td> - </tr> - {% endfor %} +<p> +This page lists all user accounts in the web interface. Note that this is distinct from the +<a href="{% url rpki.gui.app.views.resource_holder_list %}">list of resource holders</a>. +</p> + +<table class='table table-striped'> + <thead> + <tr> + <th>Username</th> + <th>Email</th> + <th></th> + </tr> + </thead> + <tbody> + {% for user in object_list %} + <tr> + <td>{{ user.username }}</td> + <td>{{ user.email }}</td> + <td> + <a class='btn btn-small' href='{% url rpki.gui.app.views.user_edit user.pk %}' title="Edit"><i class="icon-edit"></i></a> + <a class='btn btn-small' href='{% url rpki.gui.app.views.user_delete user.pk %}' title="Delete"><i class="icon-trash"></i></a> + </td> + </tr> + {% endfor %} + </tbody> </table> -<a class='btn' href="{% url rpki.gui.app.views.user_create %}" title="create a new locally hosted resource handle">Create</a> +<a class='btn' href="{% url rpki.gui.app.views.user_create %}" title="create a new locally hosted resource handle"><i class="icon-plus-sign"></i> Create</a> {% endblock content %} diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py index 93936ad4..81c56f4e 100644 --- a/rpkid/rpki/gui/app/urls.py +++ b/rpkid/rpki/gui/app/urls.py @@ -48,6 +48,10 @@ urlpatterns = patterns( (r'^repo/import$', views.repository_import), (r'^repo/(?P<pk>\d+)/$', views.repository_detail), (r'^repo/(?P<pk>\d+)/delete$', views.repository_delete), + (r'^resource_holder/$', views.resource_holder_list), + (r'^resource_holder/create$', views.resource_holder_create), + (r'^resource_holder/(?P<pk>\d+)/delete$', views.resource_holder_delete), + (r'^resource_holder/(?P<pk>\d+)/edit$', views.resource_holder_edit), (r'^roa/(?P<pk>\d+)/$', views.roa_detail), (r'^roa/create$', views.roa_create), (r'^roa/create_multi$', views.roa_create_multi), diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py index 0a393007..a82c7c1d 100644 --- a/rpkid/rpki/gui/app/views.py +++ b/rpkid/rpki/gui/app/views.py @@ -72,15 +72,13 @@ def handle_required(f): if request.user.is_superuser: conf = models.Conf.objects.all() else: - conf = models.Conf.objects.filter(handle=request.user.username) + conf = models.Conf.objects.filter(confacl__user=request.user) if conf.count() == 1: request.session['handle'] = conf[0] elif conf.count() == 0: return render(request, 'app/conf_empty.html', {}) else: - # Should reverse the view for this instead of hardcoding - # the URL. url = '%s?next=%s' % (reverse(conf_list), urlquote(request.get_full_path())) return http.HttpResponseRedirect(url) @@ -232,23 +230,29 @@ def dashboard(request): }) -@superuser_required +@login_required def conf_list(request, **kwargs): """Allow the user to select a handle.""" - return render(request, 'app/conf_list.html', - {'conf_list': models.Conf.objects.all()}) + next_url = request.GET.get('next', reverse(dashboard)) + return render(request, 'app/conf_list.html', { + 'conf_list': models.Conf.objects.filter(confacl__user=request.user), + 'next_url': next_url + }) -@superuser_required +@login_required def conf_select(request): """Change the handle for the current session.""" if not 'handle' in request.GET: - return http.HttpResponseRedirect('/myrpki/conf/select') + return redirect(conf_list) handle = request.GET['handle'] next_url = request.GET.get('next', reverse(dashboard)) - if next_url == '': - next_url = reverse(dashboard) - request.session['handle'] = get_object_or_404(models.Conf, handle=handle) + if request.user.is_superuser: + request.session['handle'] = get_object_or_404(models.Conf, handle=handle) + else: + request.session['handle'] = get_object_or_404( + models.Conf, confacl__user=request.user, handle=handle + ) return http.HttpResponseRedirect(next_url) @@ -996,86 +1000,68 @@ def client_export(request, pk): return serve_xml(str(xml), '%s.repo' % z.handle) +### Routines for managing resource handles serviced by this server + @superuser_required -def user_list(request): +def resource_holder_list(request): """Display a list of all the RPKI handles managed by this server.""" - # create a list of tuples of (Conf, User) - users = [] - for conf in models.Conf.objects.all(): - try: - u = User.objects.get(username=conf.handle) - except User.DoesNotExist: - u = None - users.append((conf, u)) - return render(request, 'app/user_list.html', {'users': users}) + return render(request, 'app/resource_holder_list.html', { + 'object_list': models.Conf.objects.all() + }) @superuser_required -def user_delete(request, pk): - conf = models.Conf.objects.get(pk=pk) - log = request.META['wsgi.errors'] +def resource_holder_edit(request, pk): + """Display a list of all the RPKI handles managed by this server.""" + conf = get_object_or_404(models.Conf, pk=pk) if request.method == 'POST': - form = forms.Empty(request.POST) + form = forms.ResourceHolderForm(request.POST, request.FILES) if form.is_valid(): - User.objects.filter(username=conf.handle).delete() - z = Zookeeper(handle=conf.handle, logstream=log) - z.delete_self() - z.synchronize() - return http.HttpResponseRedirect(reverse(user_list)) + models.ConfACL.objects.filter(conf=conf).delete() + for user in form.cleaned_data.get('users'): + models.ConfACL.objects.create(user=user, conf=conf) + return redirect(resource_holder_list) else: - form = forms.Empty() - return render(request, 'app/user_confirm_delete.html', - {'object': conf, 'form': form}) + users = [acl.user for acl in models.ConfACL.objects.filter(conf=conf).all()] + form = forms.ResourceHolderForm(initial={ + 'users': users + }) + return render(request, 'app/app_form.html', { + 'form_title': "Edit Resource Holder: " + conf.handle, + 'form': form, + 'cancel_url': reverse(resource_holder_list) + }) @superuser_required -def user_edit(request, pk): +def resource_holder_delete(request, pk): conf = get_object_or_404(models.Conf, pk=pk) - # in the old model, there may be users with a different name, so create a - # new user object if it is missing. - try: - user = User.objects.get(username=conf.handle) - except User.DoesNotExist: - user = User(username=conf.handle) - + log = request.META['wsgi.errors'] if request.method == 'POST': - form = forms.UserEditForm(request.POST) + form = forms.Empty(request.POST) if form.is_valid(): - pw = form.cleaned_data.get('pw') - if pw: - user.set_password(pw) - user.email = form.cleaned_data.get('email') - user.save() - return http.HttpResponseRedirect(reverse(user_list)) + z = Zookeeper(handle=conf.handle, logstream=log) + z.delete_self() + z.synchronize() + return redirect(resource_holder_list) else: - form = forms.UserEditForm(initial={'email': user.email}) - return render(request, 'app/app_form.html', { - 'object': user, + form = forms.Empty() + return render(request, 'app/app_confirm_delete.html', { + 'form_title': 'Delete Resource Holder: ' + conf.handle, 'form': form, - 'form_title': 'Edit User: ' + user.username, + 'cancel_url': reverse(resource_holder_list) }) -@handle_required -def user_create(request): - """ - Wizard mode to create a new locally hosted child. - - """ - if not request.user.is_superuser: - return http.HttpResponseForbidden() - +@superuser_required +def resource_holder_create(request): log = request.META['wsgi.errors'] if request.method == 'POST': - form = forms.UserCreateForm(request.POST, request.FILES) + form = forms.ResourceHolderCreateForm(request.POST, request.FILES) if form.is_valid(): handle = form.cleaned_data.get('handle') - pw = form.cleaned_data.get('password') - email = form.cleaned_data.get('email') parent = form.cleaned_data.get('parent') - User.objects.create_user(handle, email, pw) - zk_child = Zookeeper(handle=handle, logstream=log) identity_xml = zk_child.initialize() if parent: @@ -1094,14 +1080,88 @@ def user_create(request): zk_child.configure_repository(t.name) os.remove(t.name) zk_child.synchronize() + return redirect(resource_holder_list) + else: + form = forms.ResourceHolderCreateForm() + return render(request, 'app/app_form.html', { + 'form': form, + 'form_title': 'Create Resource Holder', + 'cancel_url': reverse(resource_holder_list) + }) - return http.HttpResponseRedirect(reverse(dashboard)) + +### views for managing user logins to the web interface + +@superuser_required +def user_create(request): + if request.method == 'POST': + form = forms.UserCreateForm(request.POST, request.FILES) + if form.is_valid(): + username = form.cleaned_data.get('username') + pw = form.cleaned_data.get('password') + email = form.cleaned_data.get('email') + user = User.objects.create_user(username, email, pw) + for conf in form.cleaned_data.get('resource_holders'): + models.ConfACL.objects.create(user=user, conf=conf) + return redirect(user_list) else: - conf = request.session['handle'] - form = forms.UserCreateForm(initial={'parent': conf}) + form = forms.UserCreateForm() return render(request, 'app/app_form.html', { 'form': form, 'form_title': 'Create User', 'cancel_url': reverse(user_list), }) + + +@superuser_required +def user_list(request): + """Display a list of all the RPKI handles managed by this server.""" + return render(request, 'app/user_list.html', { + 'object_list': User.objects.all() + }) + + +@superuser_required +def user_delete(request, pk): + user = get_object_or_404(User, pk=pk) + if request.method == 'POST': + form = forms.Empty(request.POST, request.FILES) + if form.is_valid(): + user.delete() + return redirect(user_list) + else: + form = forms.Empty() + return render(request, 'app/app_confirm_delete.html', { + 'form_title': 'Delete User: ' + user.username, + 'form': form, + 'cancel_url': reverse(user_list) + }) + + +@superuser_required +def user_edit(request, pk): + user = get_object_or_404(User, pk=pk) + if request.method == 'POST': + form = forms.UserEditForm(request.POST) + if form.is_valid(): + pw = form.cleaned_data.get('pw') + if pw: + user.set_password(pw) + user.email = form.cleaned_data.get('email') + user.save() + models.ConfACL.objects.filter(user=user).delete() + handles = form.cleaned_data.get('resource_holders') + for conf in handles: + models.ConfACL.objects.create(user=user, conf=conf) + return redirect(user_list) + else: + form = forms.UserEditForm(initial={ + 'email': user.email, + 'resource_holders': models.Conf.objects.filter(confacl__user=user).all() + }) + return render(request, 'app/app_form.html', { + 'form': form, + 'form_title': 'Edit User: ' + user.username, + 'cancel_url': reverse(user_list) + }) |