diff --git a/TODO.md b/TODO.md index 971aaade6e..b79a20251c 100644 --- a/TODO.md +++ b/TODO.md @@ -27,5 +27,6 @@ TWEAKS/ASSORTED * uniqueness checks for playbook paths? * allow multiple playbook execution types per project, different --tag choices, different --limit choices (maybe just free form in the job for now?) * permissions infrastructure about who can kick off what kind of jobs -* it would be nice if POSTs to subcollections used the permissions of the regular collection POST rules and then called the PUT code. +* it would be nice if POSTs to subcollections used the permissions of the regular collection POST rules and then called the attach code. * root API discovery resource at /api and /api/v1 +* audit/test read only fields like creation_date diff --git a/lib/main/base_views.py b/lib/main/base_views.py index b8d3ac6bb5..9d19d20b15 100644 --- a/lib/main/base_views.py +++ b/lib/main/base_views.py @@ -40,11 +40,14 @@ class BaseList(generics.ListCreateAPIView): return True if request.method == 'POST': if self.__class__.model in [ User ]: - # Django user gets special handling since it's not our class - # org admins are allowed to create users - return self.request.user.is_superuser or (self.request.user.admin_of_organizations.count() > 0) + ok = self.request.user.is_superuser or (self.request.user.admin_of_organizations.count() > 0) + if not ok: + raise PermissionDenied() + return True else: - return self.__class__.model.can_user_add(request.user, self.request.DATA) + if not self.__class__.model.can_user_add(request.user, self.request.DATA): + raise PermissionDenied() + return True raise exceptions.NotImplementedError def get_queryset(self): @@ -112,7 +115,7 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView): obj = self.model.objects.get(pk=kwargs['pk']) if not request.user.is_superuser and not self.delete_permissions_check(request, obj): raise PermissionDenied() - if isinstance(obj, CommonModel): + if isinstance(obj, PrimordialModel): obj.name = "_deleted_%s_%s" % (str(datetime.time()), obj.name) obj.active = False obj.save() @@ -125,7 +128,7 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView): return HttpResponse(status=204) def delete_permissions_check(self, request, obj): - if isinstance(obj, CommonModel): + if isinstance(obj, PrimordialModel): return self.__class__.model.can_user_delete(request.user, obj) elif isinstance(obj, User): return UserHelper.can_user_delete(request.user, obj) diff --git a/lib/main/migrations/0009_changes.py b/lib/main/migrations/0009_changes.py new file mode 100644 index 0000000000..e7a3a77897 --- /dev/null +++ b/lib/main/migrations/0009_changes.py @@ -0,0 +1,270 @@ +# -*- 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): + # Removing unique constraint on 'Group', fields ['name'] + db.delete_unique(u'main_group', ['name']) + + # Removing unique constraint on 'Host', fields ['name'] + db.delete_unique(u'main_host', ['name']) + + + # Changing field 'Inventory.organization' + db.alter_column(u'main_inventory', 'organization_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['main.Organization'])) + + # Changing field 'Host.inventory' + db.alter_column(u'main_host', 'inventory_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['main.Inventory'])) + + # Changing field 'Group.inventory' + db.alter_column(u'main_group', 'inventory_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['main.Inventory'])) + + # Changing field 'VariableData.group' + db.alter_column(u'main_variabledata', 'group_id', self.gf('django.db.models.fields.related.ForeignKey')(on_delete=models.SET_NULL, to=orm['main.Group'], null=True)) + + # Changing field 'VariableData.host' + db.alter_column(u'main_variabledata', 'host_id', self.gf('django.db.models.fields.related.ForeignKey')(on_delete=models.SET_NULL, to=orm['main.Host'], null=True)) + + def backwards(self, orm): + + # Changing field 'Inventory.organization' + db.alter_column(u'main_inventory', 'organization_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, on_delete=models.SET_NULL, to=orm['main.Organization'])) + # Adding unique constraint on 'Host', fields ['name'] + db.create_unique(u'main_host', ['name']) + + + # Changing field 'Host.inventory' + db.alter_column(u'main_host', 'inventory_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, on_delete=models.SET_NULL, to=orm['main.Inventory'])) + # Adding unique constraint on 'Group', fields ['name'] + db.create_unique(u'main_group', ['name']) + + + # Changing field 'Group.inventory' + db.alter_column(u'main_group', 'inventory_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, on_delete=models.SET_NULL, to=orm['main.Inventory'])) + + # Changing field 'VariableData.group' + db.alter_column(u'main_variabledata', 'group_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['main.Group'])) + + # Changing field 'VariableData.host' + db.alter_column(u'main_variabledata', 'host_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['main.Host'])) + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'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': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'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'}), + u'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'}) + }, + 'main.audittrail': { + 'Meta': {'object_name': 'AuditTrail'}, + 'comment': ('django.db.models.fields.TextField', [], {}), + 'delta': ('django.db.models.fields.TextField', [], {}), + 'detail': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'resource_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Tag']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}) + }, + 'main.credential': { + 'Meta': {'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'credential_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['main.Project']", 'blank': 'True', 'null': 'True'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '4096', 'blank': 'True'}), + 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'ssh_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'credential_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Team']", 'blank': 'True', 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': 'True'}) + }, + 'main.group': { + 'Meta': {'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'group_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'group_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + 'main.host': { + 'Meta': {'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + 'main.inventory': { + 'Meta': {'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'inventory_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'inventory_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + 'main.launchjob': { + 'Meta': {'object_name': 'LaunchJob'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'launchjob_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'launchjob\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Inventory']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['main.Project']", 'blank': 'True', 'null': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'launchjob_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': 'True'}) + }, + 'main.launchjobstatus': { + 'Meta': {'object_name': 'LaunchJobStatus'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'launchjobstatus_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'launchjobstatus\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'launch_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'launch_job_statuses'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.LaunchJob']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'result_data': ('django.db.models.fields.TextField', [], {}), + 'status': ('django.db.models.fields.IntegerField', [], {}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'launchjobstatus_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + 'main.organization': { + 'Meta': {'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organization_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['main.Project']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organization_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'permission_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'permission_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + u'main.project': { + 'Meta': {'object_name': 'Project'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'project_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'project\', \'app_label\': u\'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'default_playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'projects'", 'blank': 'True', 'to': "orm['main.Inventory']"}), + 'local_repository': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'project_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + }, + 'main.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}) + }, + 'main.team': { + 'Meta': {'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'team_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organizations': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'teams'", 'symmetrical': 'False', 'to': "orm['main.Organization']"}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['main.Project']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'team_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.variabledata': { + 'Meta': {'object_name': 'VariableData'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'variabledata_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'variabledata\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'creation_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'data': ('django.db.models.fields.TextField', [], {}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variable_data'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Group']", 'blank': 'True', 'null': 'True'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variable_data'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Host']", 'blank': 'True', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'variabledata_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}) + } + } + + complete_apps = ['main'] \ No newline at end of file diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index 96a146bac3..eb9c15cb72 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -124,15 +124,16 @@ class UserHelper(object): return matching_orgs -class CommonModel(models.Model): +class PrimordialModel(models.Model): ''' common model for all object types that have these standard fields + must use a subclass CommonModel or CommonModelNameNotUnique though + as this lacks a name field. ''' class Meta: abstract = True - name = models.CharField(max_length=512, unique=True) description = models.TextField(blank=True, default='') created_by = models.ForeignKey('auth.User', on_delete=SET_NULL, null=True, related_name='%s(class)s_created') # not blank=False on purpose for admin! creation_date = models.DateField(auto_now_add=True) @@ -173,6 +174,22 @@ class CommonModel(models.Model): def can_user_unattach(cls, user, obj, sub_obj, relationship): return cls.can_user_administrate(user, obj) +class CommonModel(PrimordialModel): + ''' a base model where the name is unique ''' + + class Meta: + abstract = True + + name = models.CharField(max_length=512, unique=True) + +class CommonModelNameNotUnique(PrimordialModel): + ''' a base model where the name is not unique ''' + + class Meta: + abstract = True + + name = models.CharField(max_length=512, unique=False) + class Tag(models.Model): ''' any type of object can be given a search tag @@ -268,7 +285,7 @@ class Inventory(CommonModel): app_label = 'main' verbose_name_plural = _('inventories') - organization = models.ForeignKey(Organization, null=True, on_delete=SET_NULL, related_name='inventories') + organization = models.ForeignKey(Organization, null=False, related_name='inventories') def get_absolute_url(self): import lib.urls @@ -297,10 +314,34 @@ class Inventory(CommonModel): result = (by_org_admin + by_team_permission + by_user_permission) return result > 0 + @classmethod + def _has_any_inventory_permission_types(cls, user, allowed): + ''' + rather than checking for a permission on a specific inventory, return whether we have + permissions on any inventory. This is primarily used to decide if the user can create + host or group objects + ''' + + if user.is_superuser: + return True + by_org_admin = user.organizations.filter( + admins__in = [ user ] + ).count() + by_team_permission = Permission.objects.filter( + team__in = user.teams.all(), + permission_type__in = allowed + ).count() + by_user_permission = user.permissions.filter( + permission_type__in = allowed + ).count() + + result = (by_org_admin + by_team_permission + by_user_permission) + return result > 0 + @classmethod def can_user_add(cls, user, data): if not 'organization' in data: - return False + return True if user.is_superuser: return True if not user.is_superuser: @@ -322,7 +363,7 @@ class Inventory(CommonModel): return cls._has_permission_types(user, obj, PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN) -class Host(CommonModel): +class Host(CommonModelNameNotUnique): ''' A managed node ''' @@ -330,12 +371,26 @@ class Host(CommonModel): class Meta: app_label = 'main' - inventory = models.ForeignKey('Inventory', null=True, on_delete=SET_NULL, related_name='hosts') + inventory = models.ForeignKey('Inventory', null=False, related_name='hosts') def __unicode__(self): return self.name + + @classmethod + def can_user_add(cls, user, data): + print "DEBUG: can_user_add called for HOST: %s" % data + if not 'inventory' in data: + print 'DEBUG: missing inventory!' + return False + inventory = Inventory.objects.get(pk=data['inventory']) + return Inventory._has_permission_types(user, inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE) + + def get_absolute_url(self): + import lib.urls + return reverse(lib.urls.views_HostsDetail, args=(self.pk,)) + +class Group(CommonModelNameNotUnique): -class Group(CommonModel): ''' A group of managed nodes. May belong to multiple groups ''' @@ -343,13 +398,20 @@ class Group(CommonModel): class Meta: app_label = 'main' - inventory = models.ForeignKey('Inventory', null=True, on_delete=SET_NULL, related_name='groups') + inventory = models.ForeignKey('Inventory', null=False, related_name='groups') parents = models.ManyToManyField('self', symmetrical=False, related_name='children', blank=True) hosts = models.ManyToManyField('Host', related_name='groups', blank=True) def __unicode__(self): return self.name + @classmethod + def can_user_add(cls, user, data): + if not 'inventory' in data: + return False + inventory = Inventory.objects.get(pk=data['inventory']) + return Inventory._has_permission_types(user, inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE) + # FIXME: audit nullables # FIXME: audit cascades @@ -362,8 +424,8 @@ class VariableData(CommonModel): app_label = 'main' verbose_name_plural = _('variable data') - host = models.ForeignKey('Host', null=True, default=None, blank=True, on_delete=CASCADE, related_name='variable_data') - group = models.ForeignKey('Group', null=True, default=None, blank=True, on_delete=CASCADE, related_name='variable_data') + host = models.ForeignKey('Host', null=True, default=None, blank=True, on_delete=SET_NULL, related_name='variable_data') + group = models.ForeignKey('Group', null=True, default=None, blank=True, on_delete=SET_NULL, related_name='variable_data') data = models.TextField() # FIXME: JsonField def __unicode__(self): diff --git a/lib/main/serializers.py b/lib/main/serializers.py index 3b6af2e094..3c60e0212a 100644 --- a/lib/main/serializers.py +++ b/lib/main/serializers.py @@ -90,9 +90,36 @@ class InventorySerializer(BaseSerializer): fields = ('url', 'id', 'name', 'description', 'creation_date', 'organization') def get_related(self, obj): - # FIXME: add related resources: inventories + # FIXME: add related resources: hosts, groups return dict() +class HostSerializer(BaseSerializer): + + # add the URL and related resources + url = serializers.CharField(source='get_absolute_url', read_only=True) + related = serializers.SerializerMethodField('get_related') + + class Meta: + model = Host + fields = ('url', 'id', 'name', 'description', 'creation_date', 'inventory') + + def get_related(self, obj): + # FIXME: add related resources + return dict() + +class GroupSerializer(BaseSerializer): + + # add the URL and related resources + url = serializers.CharField(source='get_absolute_url', read_only=True) + related = serializers.SerializerMethodField('get_related') + + class Meta: + model = Host + fields = ('url', 'id', 'name', 'description', 'creation_date', 'inventory') + + def get_related(self, obj): + # FIXME: add related resources + return dict() class TeamSerializer(BaseSerializer): @@ -126,7 +153,6 @@ class UserSerializer(BaseSerializer): admin_of_organizations = reverse(lib.urls.views_UsersAdminOrganizationsList, args=(obj.pk,)), ) - def get_absolute_url_override(self, obj): import lib.urls return reverse(lib.urls.views_UsersDetail, args=(obj.pk,)) diff --git a/lib/main/tests/inventory.py b/lib/main/tests/inventory.py index 36d233db6d..b52302a206 100644 --- a/lib/main/tests/inventory.py +++ b/lib/main/tests/inventory.py @@ -108,7 +108,7 @@ class InventoryTest(BaseTest): # an org admin of any org can create inventory, if it is one of his organizations # the organization parameter is required! new_inv_incomplete = dict(name='inventory-d', description='baz') - data = self.post(inventories, data=new_inv_incomplete, expect=403, auth=self.get_normal_credentials()) + data = self.post(inventories, data=new_inv_incomplete, expect=400, auth=self.get_normal_credentials()) new_inv_not_my_org = dict(name='inventory-d', description='baz', organization=3) data = self.post(inventories, data=new_inv_not_my_org, expect=403, auth=self.get_normal_credentials()) @@ -119,13 +119,38 @@ class InventoryTest(BaseTest): new_inv_denied = dict(name='inventory-e', description='glorp', organization=1) data = self.post(inventories, data=new_inv_denied, expect=403, auth=self.get_other_credentials()) - # a super user can add hosts + # a super user can add hosts (but inventory ID is required) + inv = Inventory.objects.create( + name = 'test inventory', + organization = self.organizations[0] + ) + invalid = dict(name='asdf0.example.com') + new_host_a = dict(name='asdf0.example.com', inventory=inv.pk) + new_host_b = dict(name='asdf1.example.com', inventory=inv.pk) + new_host_c = dict(name='asdf2.example.com', inventory=inv.pk) + new_host_d = dict(name='asdf3.example.com', inventory=inv.pk) + # FIXME: should raise 400 not 201, look into required fields in rest_framework + print hosts + data0 = self.post(hosts, data=invalid, expect=400, auth=self.get_super_credentials()) + data0 = self.post(hosts, data=new_host_a, expect=201, auth=self.get_super_credentials()) - # an org admin can add groups + # an org admin can add hosts + data1 = self.post(hosts, data=new_host_a, expect=201, auth=self.get_normal_credentials()) # a normal user cannot add hosts + data2 = self.post(hosts, data=new_host_b, expect=403, auth=self.get_nobody_credentials()) - # a normal user with inventory edit permissions can create hosts + # a normal user with inventory edit permissions (on any inventory) can create hosts + edit_perm = Permission.objects.create( + user = self.other_django_user, + inventory = Inventory.objects.get(pk=1), + permission_type = PERM_INVENTORY_EDIT + ) + data3 = self.post(hosts, data=new_host_c, expect=201, auth=self.get_other_credentials()) + + # hostnames must be unique -- posting a duplicate just returns the previous + data4 = self.post(hosts, data=new_host_c, expect=200, auth=self.get_other_credentials()) + self.assertEqual(data1['id'], data4['id']) # a super user can add groups @@ -134,6 +159,8 @@ class InventoryTest(BaseTest): # a normal user cannot create groups # a normal user with inventory edit permissions can create groups + + # group names must be unique for each inventory record # a super user can associate hosts with inventories diff --git a/lib/main/views.py b/lib/main/views.py index 04f1c64f52..1554087699 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -282,3 +282,37 @@ class InventoryDetail(BaseDetail): serializer_class = InventorySerializer permission_classes = (CustomRbac,) +class HostsList(BaseList): + + model = Host + serializer_class = HostSerializer + permission_classes = (CustomRbac,) + + def _get_queryset(self): + ''' + I can see hosts when: + I'm a superuser, + or an organization admin of an inventory they are in + or when I have allowing read permissions via a user or team on an inventory they are in + ''' + base = Host.objects + if self.request.user.is_superuser: + return base.all() + admin_of = base.filter(inventory__organization__admins__in = [ self.request.user ]).distinct() + has_user_perms = base.filter( + inventory__permissions__user__in = [ self.request.user ], + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + has_team_perms = base.filter( + inventory__permissions__team__in = self.request.user.teams.all(), + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + return admin_of | has_user_perms | has_team_perms + +class HostsDetail(BaseDetail): + + model = Host + serializer_class = HostSerializer + permission_classes = (CustomRbac,) + + diff --git a/lib/urls.py b/lib/urls.py index 261abb6d00..ed1ba3aaf2 100644 --- a/lib/urls.py +++ b/lib/urls.py @@ -50,6 +50,8 @@ views_InventoryDetail = views.InventoryDetail.as_view() # group service # host service +views_HostsList = views.HostsList.as_view() +views_HostsDetail = views.HostsDetail.as_view() # inventory variable service @@ -92,9 +94,11 @@ urlpatterns = patterns('', url(r'^api/v1/inventories/$', views_InventoryList), url(r'^api/v1/inventories/(?P[0-9]+)/$', views_InventoryDetail), - # group service - # host service + url(r'^api/v1/hosts/$', views_HostsList), + url(r'^api/v1/hosts/(?P[0-9]+)/$', views_HostsDetail), + + # group service # inventory variable service