diff --git a/Access/__init__.py b/Access/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/Access/admin.py b/Access/admin.py
new file mode 100644
index 00000000..846f6b40
--- /dev/null
+++ b/Access/admin.py
@@ -0,0 +1 @@
+# Register your models here.
diff --git a/Access/apps.py b/Access/apps.py
new file mode 100644
index 00000000..0276054c
--- /dev/null
+++ b/Access/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AccessConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "Access"
diff --git a/Access/decorators.py b/Access/decorators.py
new file mode 100644
index 00000000..8a33efa2
--- /dev/null
+++ b/Access/decorators.py
@@ -0,0 +1,37 @@
+from django.core.exceptions import PermissionDenied
+
+
+def user_admin_or_ops(function):
+ def wrap(request, *args, **kwargs):
+ if request.user.is_superuser or request.user.user.is_ops:
+ return function(request, *args, **kwargs)
+ else:
+ raise PermissionDenied
+
+ wrap.__doc__ = function.__doc__
+ wrap.__name__ = function.__name__
+ return wrap
+
+
+def authentication_classes(authentication_classes):
+ def decorator(func):
+ func.authentication_classes = authentication_classes
+ return func
+
+ return decorator
+
+
+def user_with_permission(permissions_list):
+ def user_with_permission_decorator(function):
+ def wrap(request, *args, **kwargs):
+ if hasattr(request.user, "user"):
+ permission_labels = [
+ permission.label for permission in request.user.user.permissions
+ ]
+ if len(set(permissions_list).intersection(permission_labels)) > 0:
+ return function(request, *args, **kwargs)
+ raise PermissionDenied
+
+ return wrap
+
+ return user_with_permission_decorator
diff --git a/Access/helpers.py b/Access/helpers.py
new file mode 100644
index 00000000..91a11a58
--- /dev/null
+++ b/Access/helpers.py
@@ -0,0 +1,30 @@
+from os.path import dirname, basename, isfile, join
+import glob
+import re
+import logging
+
+logger = logging.getLogger(__name__)
+available_accesses = []
+cached_accesses = []
+
+
+def getAvailableAccessModules():
+ global available_accesses
+ if len(available_accesses) > 0:
+ return available_accesses
+ available_accesses = [access for access in getAccessModules() if access.available]
+ return available_accesses
+
+
+def getAccessModules():
+ global cached_accesses
+ if len(cached_accesses) > 0:
+ return cached_accesses
+ access_modules_dirs = glob.glob(join(dirname(__file__), "access_modules", "*"))
+ for each_dir in access_modules_dirs:
+ if re.search(r"/(base_|__pycache__)", each_dir):
+ access_modules_dirs.remove(each_dir)
+ access_modules_dirs.sort()
+ cached_accesses = \
+ [globals()[basename(f)].access.get_object() for f in access_modules_dirs if not isfile(f)]
+ return cached_accesses
diff --git a/Access/migrations/0001_initial.py b/Access/migrations/0001_initial.py
new file mode 100644
index 00000000..87478b42
--- /dev/null
+++ b/Access/migrations/0001_initial.py
@@ -0,0 +1,500 @@
+# Generated by Django 4.1.3 on 2022-12-19 10:42
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="AccessV2",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("access_tag", models.CharField(max_length=255)),
+ ("access_label", models.JSONField(default=dict)),
+ ("is_auto_approved", models.BooleanField(default=False)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="gitAcces",
+ fields=[
+ ("requested_on", models.DateTimeField(auto_now_add=True)),
+ ("updated_on", models.DateTimeField(auto_now=True)),
+ ("requestInfo", models.JSONField(default=dict)),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("Pending", "pending"),
+ ("Approved", "approved"),
+ ("Declined", "declined"),
+ ("Processing", "processing"),
+ ("Revoked", "revoked"),
+ ],
+ default="Pending",
+ max_length=255,
+ ),
+ ),
+ ("requestDateUTC", models.CharField(max_length=255)),
+ (
+ "RevokeDateUTC",
+ models.CharField(blank=True, max_length=255, null=True),
+ ),
+ (
+ "requestId",
+ models.CharField(max_length=255, primary_key=True, serialize=False),
+ ),
+ ("approver", models.CharField(blank=True, max_length=255)),
+ ("requester", models.CharField(max_length=255)),
+ ("reason", models.TextField(blank=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="GroupV2",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("group_id", models.CharField(max_length=255, unique=True)),
+ ("requested_on", models.DateTimeField(auto_now_add=True)),
+ ("updated_on", models.DateTimeField(auto_now=True)),
+ ("name", models.CharField(max_length=128)),
+ ("description", models.TextField()),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("Pending", "pending"),
+ ("Approved", "approved"),
+ ("Declined", "declined"),
+ ("Deprecated", "deprecated"),
+ ],
+ default="Pending",
+ max_length=255,
+ ),
+ ),
+ ("decline_reason", models.TextField(blank=True, null=True)),
+ ("needsAccessApprove", models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Permission",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("label", models.CharField(max_length=255, unique=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Role",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("label", models.CharField(max_length=255, unique=True)),
+ ("permission", models.ManyToManyField(to="Access.permission")),
+ ],
+ ),
+ migrations.CreateModel(
+ name="SshPublicKey",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("key", models.TextField()),
+ (
+ "status",
+ models.CharField(
+ choices=[("Active", "active"), ("Revoked", "revoked")],
+ default="Active",
+ max_length=100,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="User",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "gitusername",
+ models.CharField(blank=True, max_length=255, null=True),
+ ),
+ ("name", models.CharField(max_length=255, null=True)),
+ ("email", models.EmailField(max_length=254, null=True)),
+ ("phone", models.IntegerField(blank=True, null=True)),
+ ("is_bot", models.BooleanField(default=False)),
+ (
+ "bot_type",
+ models.CharField(
+ choices=[("None", "none"), ("Github", "github")],
+ default="None",
+ max_length=100,
+ ),
+ ),
+ ("alerts_enabled", models.BooleanField(default=False)),
+ ("is_manager", models.BooleanField(default=False)),
+ ("is_ops", models.BooleanField(default=False)),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ ("ssh_pub_key", models.TextField(blank=True, null=True)),
+ ("avatar", models.TextField(blank=True, null=True)),
+ (
+ "state",
+ models.CharField(
+ choices=[
+ ("1", "active"),
+ ("2", "offboarding"),
+ ("3", "offboarded"),
+ ],
+ default=1,
+ max_length=255,
+ ),
+ ),
+ ("offbaord_date", models.DateTimeField(blank=True, null=True)),
+ (
+ "revoker",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="user_revoker",
+ to="Access.user",
+ ),
+ ),
+ ("role", models.ManyToManyField(blank=True, to="Access.role")),
+ (
+ "ssh_public_key",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="user",
+ to="Access.sshpublickey",
+ ),
+ ),
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="user",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="UserAccessMapping",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("request_id", models.CharField(max_length=255, unique=True)),
+ ("requested_on", models.DateTimeField(auto_now_add=True)),
+ ("approved_on", models.DateTimeField(blank=True, null=True)),
+ ("updated_on", models.DateTimeField(auto_now=True)),
+ ("request_reason", models.TextField()),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("Pending", "pending"),
+ ("SecondaryPending", "secondarypending"),
+ ("Processing", "processing"),
+ ("Approved", "approved"),
+ ("GrantFailed", "grantfailed"),
+ ("Declined", "declined"),
+ ("Offboarding", "offboarding"),
+ ("ProcessingRevoke", "processingrevoke"),
+ ("RevokeFailed", "revokefailed"),
+ ("Revoked", "revoked"),
+ ],
+ default="Pending",
+ max_length=100,
+ ),
+ ),
+ ("decline_reason", models.TextField(blank=True, null=True)),
+ (
+ "access_type",
+ models.CharField(
+ choices=[("Individual", "individual"), ("Group", "group")],
+ default="Individual",
+ max_length=255,
+ ),
+ ),
+ (
+ "access",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ to="Access.accessv2",
+ ),
+ ),
+ (
+ "approver_1",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="approver_1",
+ to="Access.user",
+ ),
+ ),
+ (
+ "approver_2",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="approver_2",
+ to="Access.user",
+ ),
+ ),
+ (
+ "revoker",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="user_access_revoker",
+ to="Access.user",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT, to="Access.user"
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="MembershipV2",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("membership_id", models.CharField(max_length=255, unique=True)),
+ ("is_owner", models.BooleanField(default=False)),
+ ("requested_on", models.DateTimeField(auto_now_add=True)),
+ ("updated_on", models.DateTimeField(auto_now=True)),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("Pending", "pending"),
+ ("Approved", "approved"),
+ ("Declined", "declined"),
+ ("Revoked", "revoked"),
+ ],
+ default="Pending",
+ max_length=255,
+ ),
+ ),
+ ("reason", models.TextField(blank=True, null=True)),
+ ("decline_reason", models.TextField(blank=True, null=True)),
+ (
+ "approver",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="membership_approver",
+ to="Access.user",
+ ),
+ ),
+ (
+ "group",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="membership_group",
+ to="Access.groupv2",
+ ),
+ ),
+ (
+ "requested_by",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="membership_requester",
+ to="Access.user",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="membership_user",
+ to="Access.user",
+ ),
+ ),
+ ],
+ ),
+ migrations.AddField(
+ model_name="groupv2",
+ name="approver",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="group_approver",
+ to="Access.user",
+ ),
+ ),
+ migrations.AddField(
+ model_name="groupv2",
+ name="requester",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="group_requester",
+ to="Access.user",
+ ),
+ ),
+ migrations.CreateModel(
+ name="GroupAccessMapping",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("request_id", models.CharField(max_length=255, unique=True)),
+ ("requested_on", models.DateTimeField(auto_now_add=True)),
+ ("updated_on", models.DateTimeField(auto_now=True)),
+ ("request_reason", models.TextField()),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("Pending", "pending"),
+ ("SecondaryPending", "secondarypending"),
+ ("Approved", "approved"),
+ ("Declined", "declined"),
+ ("Revoked", "revoked"),
+ ("Inactive", "inactive"),
+ ],
+ default="Pending",
+ max_length=100,
+ ),
+ ),
+ ("decline_reason", models.TextField(blank=True, null=True)),
+ (
+ "access",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ to="Access.accessv2",
+ ),
+ ),
+ (
+ "approver_1",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="g_approver_1",
+ to="Access.user",
+ ),
+ ),
+ (
+ "approver_2",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="g_approver_2",
+ to="Access.user",
+ ),
+ ),
+ (
+ "group",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT, to="Access.groupv2"
+ ),
+ ),
+ (
+ "requested_by",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="g_requester",
+ to="Access.user",
+ ),
+ ),
+ (
+ "revoker",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="group_access_revoker",
+ to="Access.user",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/Access/migrations/0002_delete_gitacces_remove_user_is_manager_and_more.py b/Access/migrations/0002_delete_gitacces_remove_user_is_manager_and_more.py
new file mode 100644
index 00000000..2f6ac423
--- /dev/null
+++ b/Access/migrations/0002_delete_gitacces_remove_user_is_manager_and_more.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.1.3 on 2022-12-26 08:51
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("Access", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name="gitAcces",
+ ),
+ migrations.RemoveField(
+ model_name="user",
+ name="is_manager",
+ ),
+ migrations.RemoveField(
+ model_name="user",
+ name="is_ops",
+ ),
+ ]
diff --git a/Access/migrations/0003_user_is_manager_user_is_ops.py b/Access/migrations/0003_user_is_manager_user_is_ops.py
new file mode 100644
index 00000000..31647fd7
--- /dev/null
+++ b/Access/migrations/0003_user_is_manager_user_is_ops.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.1.3 on 2022-12-26 12:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("Access", "0002_delete_gitacces_remove_user_is_manager_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="user",
+ name="is_manager",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="user",
+ name="is_ops",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/Access/migrations/__init__.py b/Access/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/Access/models.py b/Access/models.py
new file mode 100644
index 00000000..128c3366
--- /dev/null
+++ b/Access/models.py
@@ -0,0 +1,426 @@
+from BrowserStackAutomation.settings import USER_STATUS_CHOICES
+from django.contrib.auth.models import User as user
+from django.db import models
+
+
+class Permission(models.Model):
+ """
+ Permission to perform actions on enigma
+ """
+
+ label = models.CharField(max_length=255, null=False, blank=False, unique=True)
+
+ def __str__(self):
+ return "%s" % (self.label)
+
+
+class Role(models.Model):
+ """
+ User role to attach permissions to perform actions on enigma; one user can have multiple roles
+ Role is a group of permissions which can be associated with a group of users
+ """
+
+ label = models.CharField(max_length=255, null=False, blank=False, unique=True)
+ permission = models.ManyToManyField(Permission)
+
+ def __str__(self):
+ return "%s" % (self.label)
+
+
+class SshPublicKey(models.Model):
+ """
+ SSH Public keys for users
+ """
+
+ key = models.TextField(null=False, blank=False)
+
+ STATUS_CHOICES = (("Active", "active"), ("Revoked", "revoked"))
+ status = models.CharField(
+ max_length=100,
+ null=False,
+ blank=False,
+ choices=STATUS_CHOICES,
+ default="Active",
+ )
+
+ def __str__(self):
+ return str(self.key)
+
+
+# Create your models here.
+class User(models.Model):
+ """
+ Represents an user belonging to the organistaion
+ """
+
+ user = models.OneToOneField(
+ user, null=False, blank=False, on_delete=models.CASCADE, related_name="user"
+ )
+ gitusername = models.CharField(max_length=255, null=True, blank=True)
+ name = models.CharField(max_length=255, null=True, blank=False)
+
+ email = models.EmailField(null=True, blank=False)
+ phone = models.IntegerField(null=True, blank=True)
+
+ is_bot = models.BooleanField(null=False, blank=False, default=False)
+ BOT_TYPES = (
+ ("None", "none"),
+ ("Github", "github"),
+ )
+ bot_type = models.CharField(
+ max_length=100, null=False, blank=False, choices=BOT_TYPES, default="None"
+ )
+
+ alerts_enabled = models.BooleanField(null=False, blank=False, default=False)
+
+ is_manager = models.BooleanField(null=False, blank=False, default=False)
+ is_ops = models.BooleanField(null=False, blank=False, default=False)
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ # ssh_pub_key will be deprecated. Use ssh_public_key field
+ ssh_pub_key = models.TextField(null=True, blank=True)
+ ssh_public_key = models.ForeignKey(
+ "SshPublicKey",
+ related_name="user",
+ blank=True,
+ null=True,
+ on_delete=models.PROTECT,
+ )
+
+ avatar = models.TextField(null=True, blank=True)
+
+ state = models.CharField(
+ max_length=255, null=False, blank=False, choices=USER_STATUS_CHOICES, default=1
+ )
+ role = models.ManyToManyField(Role, blank=True)
+
+ offbaord_date = models.DateTimeField(null=True, blank=True)
+ revoker = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="user_revoker",
+ on_delete=models.PROTECT,
+ )
+
+ @property
+ def permissions(self):
+ user_roles = self.role.all()
+ user_permissions = [
+ permission for role in user_roles for permission in role.permission.all()
+ ]
+ return user_permissions
+
+ def has_permission(self, permission_label):
+ all_permission_labels = [permission.label for permission in self.permissions]
+ return permission_label in all_permission_labels
+
+ def current_state(self):
+ return dict(USER_STATUS_CHOICES).get(self.state)
+
+ def change_state(self, final_state):
+ user_states = dict(USER_STATUS_CHOICES)
+ state_key = self.state
+ for key in user_states:
+ if user_states[key] == final_state:
+ state_key = key
+ self.state = state_key
+ self.save()
+
+ def __str__(self):
+ return "%s" % (self.user)
+
+
+class MembershipV2(models.Model):
+ """
+ Membership of user in a GroupV2
+ """
+
+ membership_id = models.CharField(
+ max_length=255, null=False, blank=False, unique=True
+ )
+
+ user = models.ForeignKey(
+ "User",
+ null=False,
+ blank=False,
+ related_name="membership_user",
+ on_delete=models.PROTECT,
+ )
+ group = models.ForeignKey(
+ "GroupV2",
+ null=False,
+ blank=False,
+ related_name="membership_group",
+ on_delete=models.PROTECT,
+ )
+ is_owner = models.BooleanField(null=False, blank=False, default=False)
+
+ requested_by = models.ForeignKey(
+ User,
+ null=False,
+ blank=False,
+ related_name="membership_requester",
+ on_delete=models.PROTECT,
+ )
+ requested_on = models.DateTimeField(auto_now_add=True)
+ updated_on = models.DateTimeField(auto_now=True)
+
+ STATUS = (
+ ("Pending", "pending"),
+ ("Approved", "approved"),
+ ("Declined", "declined"),
+ ("Revoked", "revoked"),
+ )
+ status = models.CharField(
+ max_length=255, null=False, blank=False, choices=STATUS, default="Pending"
+ )
+
+ reason = models.TextField(null=True, blank=True)
+
+ approver = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="membership_approver",
+ on_delete=models.PROTECT,
+ )
+ decline_reason = models.TextField(null=True, blank=True)
+
+ def __str__(self):
+ return self.group.name + "-" + self.user.email + "-" + self.status
+
+
+class GroupV2(models.Model):
+ """
+ Model for Enigma Groups redefined.
+ """
+
+ group_id = models.CharField(max_length=255, null=False, blank=False, unique=True)
+ requested_on = models.DateTimeField(auto_now_add=True)
+ updated_on = models.DateTimeField(auto_now=True)
+
+ name = models.CharField(max_length=128, null=False, blank=False)
+ description = models.TextField(null=False, blank=False)
+
+ requester = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="group_requester",
+ on_delete=models.PROTECT,
+ )
+
+ STATUS = (
+ ("Pending", "pending"),
+ ("Approved", "approved"),
+ ("Declined", "declined"),
+ ("Deprecated", "deprecated"),
+ )
+ status = models.CharField(
+ max_length=255, null=False, blank=False, choices=STATUS, default="Pending"
+ )
+
+ approver = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="group_approver",
+ on_delete=models.PROTECT,
+ )
+ decline_reason = models.TextField(null=True, blank=True)
+ needsAccessApprove = models.BooleanField(null=False, blank=False, default=True)
+
+ def __str__(self):
+ return self.name
+
+class UserAccessMapping(models.Model):
+ """
+ Model to map access to user. Requests are broken down
+ into mappings which are sent for approval.
+ """
+
+ request_id = models.CharField(max_length=255, null=False, blank=False, unique=True)
+
+ requested_on = models.DateTimeField(auto_now_add=True)
+ approved_on = models.DateTimeField(null=True, blank=True)
+ updated_on = models.DateTimeField(auto_now=True)
+
+ user = models.ForeignKey("User", null=False, blank=False, on_delete=models.PROTECT)
+
+ request_reason = models.TextField(null=False, blank=False)
+
+ approver_1 = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="approver_1",
+ on_delete=models.PROTECT,
+ )
+ approver_2 = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="approver_2",
+ on_delete=models.PROTECT,
+ )
+
+ access = models.ForeignKey(
+ "AccessV2", null=False, blank=False, on_delete=models.PROTECT
+ )
+
+ STATUS_CHOICES = (
+ ("Pending", "pending"),
+ ("SecondaryPending", "secondarypending"),
+ ("Processing", "processing"),
+ ("Approved", "approved"),
+ ("GrantFailed", "grantfailed"),
+ ("Declined", "declined"),
+ ("Offboarding", "offboarding"),
+ ("ProcessingRevoke", "processingrevoke"),
+ ("RevokeFailed", "revokefailed"),
+ ("Revoked", "revoked"),
+ )
+ status = models.CharField(
+ max_length=100,
+ null=False,
+ blank=False,
+ choices=STATUS_CHOICES,
+ default="Pending",
+ )
+
+ decline_reason = models.TextField(null=True, blank=True)
+
+ TYPE_CHOICES = (("Individual", "individual"), ("Group", "group"))
+ access_type = models.CharField(
+ max_length=255,
+ null=False,
+ blank=False,
+ choices=TYPE_CHOICES,
+ default="Individual",
+ )
+ revoker = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="user_access_revoker",
+ on_delete=models.PROTECT,
+ )
+
+ def __str__(self):
+ return self.request_id
+
+ # Wrote the override version of save method in order to update the
+ # "approved_on" field whenever the request is marked "Approved"
+ def save(self, *args, **kwargs):
+ super(UserAccessMapping, self).save(*args, **kwargs)
+ # Consider only the first cycle of approval
+ if self.status.lower() == "approved" and self.approved_on in [None, ""]:
+ self.approved_on = self.updated_on
+ super(UserAccessMapping, self).save(*args, **kwargs)
+
+
+class GroupAccessMapping(models.Model):
+ """
+ Model to map access to group. Requests are broken down
+ into mappings which are sent for approval.
+ """
+
+ request_id = models.CharField(max_length=255, null=False, blank=False, unique=True)
+
+ requested_on = models.DateTimeField(auto_now_add=True)
+ updated_on = models.DateTimeField(auto_now=True)
+
+ group = models.ForeignKey(
+ "GroupV2", null=False, blank=False, on_delete=models.PROTECT
+ )
+
+ requested_by = models.ForeignKey(
+ "User",
+ null=True,
+ blank=False,
+ related_name="g_requester",
+ on_delete=models.PROTECT,
+ )
+
+ request_reason = models.TextField(null=False, blank=False)
+
+ approver_1 = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="g_approver_1",
+ on_delete=models.PROTECT,
+ )
+ approver_2 = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="g_approver_2",
+ on_delete=models.PROTECT,
+ )
+
+ access = models.ForeignKey(
+ "AccessV2", null=False, blank=False, on_delete=models.PROTECT
+ )
+
+ STATUS_CHOICES = (
+ ("Pending", "pending"),
+ ("SecondaryPending", "secondarypending"),
+ ("Approved", "approved"),
+ ("Declined", "declined"),
+ ("Revoked", "revoked"),
+ ("Inactive", "inactive"),
+ )
+ status = models.CharField(
+ max_length=100,
+ null=False,
+ blank=False,
+ choices=STATUS_CHOICES,
+ default="Pending",
+ )
+
+ decline_reason = models.TextField(null=True, blank=True)
+
+ revoker = models.ForeignKey(
+ "User",
+ null=True,
+ blank=True,
+ related_name="group_access_revoker",
+ on_delete=models.PROTECT,
+ )
+
+ def __str__(self):
+ return self.request_id
+
+
+class AccessV2(models.Model):
+ access_tag = models.CharField(max_length=255)
+ access_label = models.JSONField(default=dict)
+ is_auto_approved = models.BooleanField(null=False, default=False)
+
+ def __str__(self):
+ try:
+ if self.access_tag == "aws":
+ label = self.access_label["data"]
+ return "{}: Team- {} | Access: {} | Level: {} | Service: {} | Resource: {}".format(
+ self.access_tag,
+ label["team"],
+ label["awsAccessType"],
+ label["levelAccessType"],
+ label["awsService"],
+ label["awsResource"],
+ )
+ if self.access_tag == "other":
+ return self.access_tag + " - " + self.access_label["request"]
+ details_arr = []
+ for data in list(self.access_label.values()):
+ try:
+ details_arr.append(data.decode("utf-8"))
+ except Exception:
+ details_arr.append(data)
+ return self.access_tag + " - " + ", ".join(details_arr)
+ except Exception:
+ return self.access_tag
diff --git a/Access/tests.py b/Access/tests.py
new file mode 100644
index 00000000..a39b155a
--- /dev/null
+++ b/Access/tests.py
@@ -0,0 +1 @@
+# Create your tests here.
diff --git a/Access/tests/test_access_helpers.py b/Access/tests/test_access_helpers.py
new file mode 100644
index 00000000..2ef9760c
--- /dev/null
+++ b/Access/tests/test_access_helpers.py
@@ -0,0 +1,58 @@
+import pytest
+from Access import helpers
+from Access.helpers import getAvailableAccessModules, getAccessModules
+
+
+class MockAccessModule:
+ def __init__(self, name=""):
+ self.name = name
+ self.available = True
+
+
+@pytest.mark.parametrize(
+ "testName, available_accesses, expectedModuleList",
+ [
+ (
+ "available_accesses has values",
+ [MockAccessModule(name="name1"), MockAccessModule(name="name2")],
+ [MockAccessModule(name="name1"), MockAccessModule(name="name2")],
+ ),
+ (
+ "available_accesses has no values",
+ [],
+ [MockAccessModule(name="name3"), MockAccessModule(name="name4")],
+ ),
+ ],
+)
+def test_getAvailableAccessModules(
+ mocker, testName, available_accesses, expectedModuleList
+):
+ mocker.patch("Access.helpers.getAccessModules", return_value=expectedModuleList)
+
+ helpers.available_accesses = available_accesses
+ modules = getAvailableAccessModules()
+ assert len(modules) == len(expectedModuleList)
+ for i in range(len(modules)):
+ assert modules[i].name == expectedModuleList[i].name
+
+
+@pytest.mark.parametrize(
+ "testName, cached_accesses, expectedModuleList",
+ [
+ (
+ "cached_accesses has values",
+ [MockAccessModule(name="name1"), MockAccessModule(name="name2")],
+ [MockAccessModule(name="name1"), MockAccessModule(name="name2")],
+ ),
+ ],
+)
+def test_getAccessModules(mocker, testName, cached_accesses, expectedModuleList):
+ mocker.patch(
+ "glob.glob", return_value=["dir1", "dir2"] + ["base_somedir", "__pycache__"]
+ )
+ mocker.patch("os.path.isfile", return_value=False)
+ helpers.cached_accesses = cached_accesses
+ modules = getAccessModules()
+ assert len(modules) == len(expectedModuleList)
+ for i in range(len(modules)):
+ assert modules[i].name == expectedModuleList[i].name
diff --git a/Access/tests/test_access_views_helper.py b/Access/tests/test_access_views_helper.py
new file mode 100644
index 00000000..451fde8b
--- /dev/null
+++ b/Access/tests/test_access_views_helper.py
@@ -0,0 +1,217 @@
+from Access.views_helper import generateUserMappings, executeGroupAccess
+from Access import models
+import pytest
+from Access import views_helper
+from bootprocess import general
+import logging
+
+
+class MockAuthUser:
+ def __init__(self, username="", user=""):
+ self.user = user
+ self.username = username
+
+
+class MockRequest:
+ def __init__(self, username=""):
+ self.user = MockAuthUser(username)
+
+
+def test_generateUserMappings(mocker):
+ class MemberShipObj:
+ def __init__(self, membership_id="", reason=""):
+ self.membership_id = membership_id
+ self.reason = reason
+
+ class AuthUser:
+ def __init__(self, username):
+ self.username = username
+ self.user = username
+
+ class User:
+ def __init__(self, user=""):
+ self.user = AuthUser(username="")
+
+ class MockAccess:
+ def __init__(self):
+ self.access_tag = ""
+
+ class MockGroupAccessMapping:
+ def __init__(self, access="", approver_1="", approver_2="", request_reason=""):
+ self.access = access
+ self.approver_1 = approver_1
+ self.approver_2 = approver_2
+ self.request_reason = request_reason
+ self.access = MockAccess()
+
+ class MockUserAccessMapping:
+ def __init__(
+ self,
+ request_id="",
+ user="",
+ access="",
+ approver_1="",
+ approver_2="",
+ request_reason="",
+ access_type="",
+ status="",
+ ):
+ self.request_id = request_id
+ self.user = user
+ self.access = access
+ self.approver_1 = approver_1
+ self.approver_2 = approver_2
+ self.request_reason = request_reason
+ self.access_type = access_type
+ self.status = status
+
+ def __len__(self):
+ return 1
+
+ def values_list(*args, **kwargs):
+ return [""]
+
+ mock_objects = mocker.MagicMock(name="objects")
+ mock_objects.values_list = values_list
+ mocker.patch(
+ "Access.models.GroupAccessMapping.objects.filter",
+ return_value=[MockGroupAccessMapping()],
+ )
+ groupAccessMappingSpy = mocker.spy(models.GroupAccessMapping.objects, "filter")
+
+ mocker.patch(
+ "Access.models.UserAccessMapping.objects.filter",
+ return_value=mock_objects,
+ side_effect=mocker.patch(
+ "Access.models.UserAccessMapping.objects.filter", return_value=mock_objects
+ ),
+ )
+ userAccessMappingFilterSpy = mocker.spy(models.UserAccessMapping.objects, "filter")
+
+ mocker.patch(
+ "Access.models.UserAccessMapping.objects.create",
+ return_value=MockUserAccessMapping(
+ request_id="request_id1",
+ user="user1",
+ access="access2",
+ approver_1="approver_1",
+ approver_2="approver_2",
+ request_reason="reason123",
+ access_type="access_type",
+ status="status1",
+ ),
+ )
+ userAccessMappingCreateSpy = mocker.spy(models.UserAccessMapping.objects, "create")
+
+ usermappingList = generateUserMappings(
+ User(user="username1"),
+ MemberShipObj(),
+ MemberShipObj(membership_id="1", reason="reason"),
+ )
+
+ assert usermappingList[0].request_id == "request_id1"
+ assert groupAccessMappingSpy.call_count == 1
+ assert userAccessMappingFilterSpy.call_count == 2
+ assert userAccessMappingCreateSpy.call_count == 1
+
+
+@pytest.mark.parametrize(
+ "testName, userstate, requestid, expectedStatus, expected_decline_reason",
+ [
+ (
+ "User is Active",
+ "active",
+ "other",
+ "Declined",
+ "Auto decline for 'Other Access'. Please replace this with correct access.",
+ ),
+ ("User is Active", "inactive", "", "Declined", "User is not active"),
+ ],
+)
+def test_executeGroupAccess(
+ mocker, testName, userstate, requestid, expectedStatus, expected_decline_reason
+):
+ userMock = mocker.MagicMock()
+ userMock.username = "username"
+ userMock.current_state.return_value = userstate
+
+ mappingObj = mocker.MagicMock()
+ mappingObj.access.access_tag = "tagname"
+ mappingObj.user = userMock
+ mappingObj.approver_1.user = userMock
+ mappingObj.request_id = requestid
+ executeGroupAccess([mappingObj])
+ assert mappingObj.status == expectedStatus
+ assert mappingObj.decline_reason == expected_decline_reason
+
+
+@pytest.mark.parametrize(
+ """testName, userstate, requestid,user_state,
+ approvalResponse ,expectedStatus, emailSesCallCount,""",
+ [
+ (
+ "User is Active, request is not other, user.state not 1",
+ "active",
+ "reqid",
+ "2",
+ None,
+ "Declined",
+ 0,
+ ),
+ (
+ "User is Active, request is not other, user.state is 1, Approval Returns true",
+ "active",
+ "reqid",
+ "1",
+ True,
+ "Approved",
+ 0,
+ ),
+ (
+ "User is Active, request is not other, user.state is 1, Approval Fails",
+ "active",
+ "reqid",
+ "1",
+ [False, "Failure Message"],
+ "GrantFailed",
+ 1,
+ ),
+ ],
+)
+def test_executeGroupAccess_run_access_grant(
+ mocker,
+ testName,
+ userstate,
+ approvalResponse,
+ requestid,
+ user_state,
+ expectedStatus,
+ emailSesCallCount,
+):
+ views_helper.logger = logging.getLogger(__name__)
+ mockAccessModule = mocker.MagicMock()
+ mockAccessModule.tag.return_value = "tagname"
+ mockAccessModule.approve.return_value = approvalResponse
+ mockAccessModule.access_mark_revoke_permission.return_value = "destination"
+
+ userMock = mocker.MagicMock()
+ userMock.username = "username"
+ userMock.current_state.return_value = userstate
+ userMock.state = user_state
+
+ mappingObj = mocker.MagicMock()
+ mappingObj.access.access_tag = "tagname"
+ mappingObj.user = userMock
+ mappingObj.approver_1.user = userMock
+ mappingObj.request_id = requestid
+ mappingObj.status = ""
+ mappingObj.decline_reason = ""
+
+ mocker.patch("bootprocess.general.emailSES", return_value="")
+ emailSES_Spy = mocker.spy(general, "emailSES")
+
+ views_helper.all_access_modules = [mockAccessModule]
+
+ executeGroupAccess([mappingObj])
+ assert mappingObj.status == expectedStatus
+ assert emailSES_Spy.call_count == emailSesCallCount
diff --git a/Access/views.py b/Access/views.py
new file mode 100644
index 00000000..55950503
--- /dev/null
+++ b/Access/views.py
@@ -0,0 +1,61 @@
+from django.contrib.auth.decorators import login_required
+import logging
+from . import helpers as helper
+from .decorators import user_admin_or_ops, authentication_classes, user_with_permission
+from rest_framework.authentication import TokenAuthentication, BasicAuthentication
+from rest_framework.decorators import api_view
+
+logger = logging.getLogger(__name__)
+
+# Create your views here.
+all_access_modules = helper.getAvailableAccessModules()
+
+
+@login_required
+def showAccessHistory(request):
+ return False
+
+
+@login_required
+@user_admin_or_ops
+def pendingFailure(request):
+ return False
+
+
+@login_required
+@user_admin_or_ops
+def pendingRevoke(request):
+ return False
+
+
+@login_required
+def updateUserInfo(request):
+ return False
+
+
+@login_required
+def createNewGroup(request):
+ return False
+
+
+@api_view(["GET"])
+@login_required
+@user_with_permission(["VIEW_USER_ACCESS_LIST"])
+@authentication_classes((TokenAuthentication, BasicAuthentication))
+def allUserAccessList(request, load_ui=True):
+ return False
+
+
+@login_required
+def allUsersList(request):
+ return False
+
+
+@login_required
+def requestAccess(request):
+ return False
+
+
+@login_required
+def groupRequestAccess(request):
+ return False
diff --git a/Access/views_helper.py b/Access/views_helper.py
new file mode 100644
index 00000000..a4b7e756
--- /dev/null
+++ b/Access/views_helper.py
@@ -0,0 +1,202 @@
+from .models import UserAccessMapping, GroupAccessMapping
+import datetime
+import traceback
+import logging
+from . import helpers as helper
+from bootprocess import general
+
+logger = logging.getLogger(__name__)
+all_access_modules = helper.getAvailableAccessModules()
+
+
+def generateUserMappings(user, group, membershipObj):
+ groupMappings = GroupAccessMapping.objects.filter(group=group, status="Approved")
+
+ userMappingsList = []
+ for groupMapping in groupMappings:
+ access = groupMapping.access
+ approver_1 = groupMapping.approver_1
+ approver_2 = groupMapping.approver_2
+ membership_id = membershipObj.membership_id
+ base_datetime_prefix = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
+ reason = (
+ "Added to group for request "
+ + membership_id
+ + " - "
+ + membershipObj.reason
+ + " - "
+ + groupMapping.request_reason
+ )
+ request_id = (
+ user.user.username + "-" + access.access_tag + "-" + base_datetime_prefix
+ )
+ similar_id_mappings = list(
+ UserAccessMapping.objects.filter(
+ request_id__icontains=request_id
+ ).values_list("request_id", flat=True)
+ )
+ idx = 0
+ while request_id + "_" + str(idx) in similar_id_mappings:
+ idx += 1
+ request_id = request_id + "_" + str(idx)
+ if not len(
+ UserAccessMapping.objects.filter(
+ user=user, access=access, status="Approved"
+ )
+ ):
+ userMappingObj = UserAccessMapping.objects.create(
+ request_id=request_id,
+ user=user,
+ access=access,
+ approver_1=approver_1,
+ approver_2=approver_2,
+ request_reason=reason,
+ access_type="Group",
+ status="Processing",
+ )
+ userMappingsList.append(userMappingObj)
+ return userMappingsList
+
+
+def executeGroupAccess(userMappingsList):
+ for mappingObj in userMappingsList:
+
+ accessType = mappingObj.access.access_tag
+ user = mappingObj.user
+ approver = mappingObj.approver_1.user.username
+ if user.current_state() == "active":
+ if "other" in mappingObj.request_id:
+ decline_group_other_access(mappingObj)
+ else:
+ run_access_grant(
+ mappingObj.request_id, mappingObj, accessType, user, approver
+ )
+ logger.debug(
+ "Successful group access grant for " + mappingObj.request_id
+ )
+ else:
+ mappingObj.status = "Declined"
+ mappingObj.decline_reason = "User is not active"
+ mappingObj.save()
+ logger.debug(
+ "Skipping group access grant for user "
+ + user.user.username
+ + " as user is not active"
+ )
+
+
+def decline_group_other_access(access_mapping):
+ user = access_mapping.user
+ access_mapping.status = "Declined"
+ access_mapping.decline_reason = (
+ "Auto decline for 'Other Access'. Please replace this with correct access."
+ )
+ access_mapping.save()
+ logger.debug(
+ "Skipping group access grant for user "
+ + user.user.username
+ + " for request_id "
+ + access_mapping.request_id
+ + " as it is 'Other Access'"
+ )
+
+
+def run_access_grant(requestId, requestObject, accessType, user, approver):
+ message = ""
+ if not requestObject.user.state == "1":
+ requestObject.status = "Declined"
+ requestObject.save()
+ logger.debug(
+ {
+ "requestId": requestId,
+ "status": "Declined",
+ "by": approver,
+ "response": message,
+ }
+ )
+ return False
+ for each_access_module in all_access_modules:
+ if accessType == each_access_module.tag():
+ try:
+ response = each_access_module.approve(
+ user,
+ [requestObject.access.access_label],
+ approver,
+ requestId,
+ is_group=False,
+ )
+ if type(response) is bool:
+ approve_success = response
+ else:
+ approve_success = response[0]
+ message = str(response[1])
+ except Exception:
+ logger.exception(
+ "Error while running approval module: " + str(traceback.format_exc())
+ )
+ approve_success = False
+ message = str(traceback.format_exc())
+ if approve_success:
+ requestObject.status = "Approved"
+ requestObject.save()
+ logger.debug(
+ {
+ "requestId": requestId,
+ "status": "Approved",
+ "by": approver,
+ "response": message,
+ }
+ )
+ else:
+ requestObject.status = "GrantFailed"
+ requestObject.save()
+ logger.debug(
+ {
+ "requestId": requestId,
+ "status": "GrantFailed",
+ "by": approver,
+ "response": message,
+ }
+ )
+ try:
+ destination = [
+ each_access_module.access_mark_revoke_permission(accessType)
+ ]
+ subject = str("Access Grant Failed - ") + accessType.upper()
+ body = (
+ "Request by "
+ + user.email
+ + " having Request ID = "
+ + requestId
+ + " is GrantFailed. Please debug and rerun the grant.
"
+ )
+ body = body + "Failure Reason - " + message
+ body = (
+ body
+ + "
View all failed grants"
+ )
+ logger.debug(
+ "Sending Grant Failed email - "
+ + str(destination)
+ + " - "
+ + subject
+ + " - "
+ + body
+ )
+ general.emailSES(destination, subject, body)
+ except Exception:
+ logger.debug(
+ "Grant Failed - Error while sending email - "
+ + requestId
+ + "-"
+ + str(str(traceback.format_exc()))
+ )
+
+ # For generic modules, approve method will send an email on "Access granted",
+ # additional email of "Access approved" is not needed
+ return True
+ return False
diff --git a/BrowserStackAutomation/settings.py b/BrowserStackAutomation/settings.py
index 8c6a4e54..20651043 100644
--- a/BrowserStackAutomation/settings.py
+++ b/BrowserStackAutomation/settings.py
@@ -24,7 +24,7 @@
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = ""
+SECRET_KEY = "abc"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@@ -42,8 +42,8 @@
"django.contrib.messages",
"django.contrib.staticfiles",
"bootprocess.apps.BootprocessConfig",
- 'social_django',
- 'Access',
+ "social_django",
+ "Access",
]
MIDDLEWARE = [
@@ -54,41 +54,38 @@
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
- 'social_django.middleware.SocialAuthExceptionMiddleware',
+ "social_django.middleware.SocialAuthExceptionMiddleware",
]
AUTHENTICATION_BACKENDS = (
- 'social_core.backends.google.GoogleOAuth2',
- 'django.contrib.auth.backends.ModelBackend',
+ "social_core.backends.google.GoogleOAuth2",
+ "django.contrib.auth.backends.ModelBackend",
)
SOCIAL_AUTH_PIPELINE = (
- 'social_core.pipeline.social_auth.social_details',
- 'social_core.pipeline.social_auth.social_uid',
- 'social_core.pipeline.social_auth.auth_allowed',
- 'social_core.pipeline.social_auth.social_user',
- 'social_core.pipeline.user.get_username',
- 'social_core.pipeline.user.create_user',
- 'social_core.pipeline.social_auth.associate_user',
- 'social_core.pipeline.social_auth.load_extra_data',
- 'social_core.pipeline.user.user_details',
- 'social_core.pipeline.debug.debug',
+ "social_core.pipeline.social_auth.social_details",
+ "social_core.pipeline.social_auth.social_uid",
+ "social_core.pipeline.social_auth.auth_allowed",
+ "social_core.pipeline.social_auth.social_user",
+ "social_core.pipeline.user.get_username",
+ "social_core.pipeline.user.create_user",
+ "social_core.pipeline.social_auth.associate_user",
+ "social_core.pipeline.social_auth.load_extra_data",
+ "social_core.pipeline.user.user_details",
+ "social_core.pipeline.debug.debug",
)
SOCIAL_AUTH_DISCONNECT_PIPELINE = (
-# Verifies that the social association can be disconnected from the current
-# user (ensure that the user login mechanism is not compromised by this
-# disconnection).
-#'social.pipeline.disconnect.allowed_to_disconnect',
-
-# Collects the social associations to disconnect.
-'social_core.pipeline.disconnect.get_entries',
-
-# Revoke any access_token when possible.
-'social_core.pipeline.disconnect.revoke_tokens',
-
-# Removes the social associations.
-'social_core.pipeline.disconnect.disconnect',
+ # Verifies that the social association can be disconnected from the current
+ # user (ensure that the user login mechanism is not compromised by this
+ # disconnection).
+ # 'social.pipeline.disconnect.allowed_to_disconnect',
+ # Collects the social associations to disconnect.
+ "social_core.pipeline.disconnect.get_entries",
+ # Revoke any access_token when possible.
+ "social_core.pipeline.disconnect.revoke_tokens",
+ # Removes the social associations.
+ "social_core.pipeline.disconnect.disconnect",
)
ROOT_URLCONF = "BrowserStackAutomation.urls"
@@ -160,7 +157,7 @@
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_URL = "static/"
-STATIC_ROOT = os.path.join(BASE_DIR, 'public/')
+STATIC_ROOT = os.path.join(BASE_DIR, "public/")
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static/"),
]
@@ -170,18 +167,20 @@
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
-with open('config.json') as data_file:
+with open("config.json") as data_file:
data = json.load(data_file)
-SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = data['googleapi']['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY']
-SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET= data['googleapi']['SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET']
-SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS = data['googleapi']['SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS']
+SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = data["googleapi"]["SOCIAL_AUTH_GOOGLE_OAUTH2_KEY"]
+SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = data["googleapi"]["SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET"]
+SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS = data["googleapi"][
+ "SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS"
+]
USER_STATUS_CHOICES = [
- ('1', 'active'),
- ('2', 'offboarding'),
- ('3', 'offboarded'),
+ ("1", "active"),
+ ("2", "offboarding"),
+ ("3", "offboarded"),
]
-DEFAULT_ACCESS_GROUP = 'default_access_group'
\ No newline at end of file
+DEFAULT_ACCESS_GROUP = "default_access_group"
diff --git a/BrowserStackAutomation/urls.py b/BrowserStackAutomation/urls.py
index 1a86fb12..856449d3 100644
--- a/BrowserStackAutomation/urls.py
+++ b/BrowserStackAutomation/urls.py
@@ -17,23 +17,31 @@
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import re_path, include
-from Access.views import showAccessHistory, pendingFailure, pendingRevoke, updateUserInfo, createNewGroup, allUserAccessList, allUsersList, requestAccess, groupRequestAccess
-from django.conf.urls.static import static
+from Access.views import (
+ showAccessHistory,
+ pendingFailure,
+ pendingRevoke,
+ updateUserInfo,
+ createNewGroup,
+ allUserAccessList,
+ allUsersList,
+ requestAccess,
+ groupRequestAccess,
+)
urlpatterns = [
- re_path(r'^admin/', admin.site.urls),
- re_path(r'^$',dashboard, name='dashboard'),
- re_path(r'^login/$', auth_views.LoginView.as_view(), name='login'),
- re_path(r'^logout/$', logout_view, name='logout'),
- re_path(r'^oauth/', include('social_django.urls', namespace='social')),
-
- re_path(r'^access/showAccessHistory$', showAccessHistory, name='showAccessHistory'),
- re_path(r'^resolve/pendingFailure',pendingFailure, name='pendingFailure'),
- re_path(r'^resolve/pendingRevoke',pendingRevoke, name='pendingRevoke'),
- re_path(r'^user/updateUserInfo/',updateUserInfo,name='updateUserInfo'),
- re_path(r'^group/create$', createNewGroup, name='createNewGroup'),
- re_path(r'^access/userAccesses$', allUserAccessList, name='allUserAccessList'),
- re_path(r'^access/usersList$', allUsersList, name='allUsersList'),
- re_path(r'^access/requestAccess$', requestAccess, name='requestAccess'),
- re_path(r'^group/requestAccess$', groupRequestAccess, name='groupRequestAccess'),
+ re_path(r"^admin/", admin.site.urls),
+ re_path(r"^$", dashboard, name="dashboard"),
+ re_path(r"^login/$", auth_views.LoginView.as_view(), name="login"),
+ re_path(r"^logout/$", logout_view, name="logout"),
+ re_path(r"^oauth/", include("social_django.urls", namespace="social")),
+ re_path(r"^access/showAccessHistory$", showAccessHistory, name="showAccessHistory"),
+ re_path(r"^resolve/pendingFailure", pendingFailure, name="pendingFailure"),
+ re_path(r"^resolve/pendingRevoke", pendingRevoke, name="pendingRevoke"),
+ re_path(r"^user/updateUserInfo/", updateUserInfo, name="updateUserInfo"),
+ re_path(r"^group/create$", createNewGroup, name="createNewGroup"),
+ re_path(r"^access/userAccesses$", allUserAccessList, name="allUserAccessList"),
+ re_path(r"^access/usersList$", allUsersList, name="allUsersList"),
+ re_path(r"^access/requestAccess$", requestAccess, name="requestAccess"),
+ re_path(r"^group/requestAccess$", groupRequestAccess, name="groupRequestAccess"),
]
diff --git a/bootprocess/admin.py b/bootprocess/admin.py
index 260fa08f..050fc7be 100644
--- a/bootprocess/admin.py
+++ b/bootprocess/admin.py
@@ -2,4 +2,4 @@
from Access.models import User
-admin.site.register(User)
\ No newline at end of file
+admin.site.register(User)
diff --git a/bootprocess/general.py b/bootprocess/general.py
index d7fe9d84..4ad3303e 100644
--- a/bootprocess/general.py
+++ b/bootprocess/general.py
@@ -1,6 +1,3 @@
-from os import environ
-import boto3
-
-def emailSES(destination, subject,body):
- print("Email Sent!!!")
+def emailSES(destination, subject, body):
+ print("Email Sent!!!")
return True
diff --git a/bootprocess/models.py b/bootprocess/models.py
index 71a83623..6b202199 100644
--- a/bootprocess/models.py
+++ b/bootprocess/models.py
@@ -1,3 +1 @@
-from django.db import models
-
# Create your models here.
diff --git a/bootprocess/tests.py b/bootprocess/tests.py
index 7ce503c2..a39b155a 100644
--- a/bootprocess/tests.py
+++ b/bootprocess/tests.py
@@ -1,3 +1 @@
-from django.test import TestCase
-
# Create your tests here.
diff --git a/bootprocess/tests/test_views_helper.py b/bootprocess/tests/test_views_helper.py
index 21d8de67..41ec9d10 100644
--- a/bootprocess/tests/test_views_helper.py
+++ b/bootprocess/tests/test_views_helper.py
@@ -1,54 +1,61 @@
import pytest
from bootprocess import views_helper
-from django.http import HttpRequest, QueryDict
from Access import models
-import Access.helpers as helpers
-from Access.models import User as access_user, MembershipV2
-import json
import threading
from BrowserStackAutomation.settings import DEFAULT_ACCESS_GROUP
class MockAuthUser:
- def __init__(self,username="", user=""):
+ def __init__(self, username="", user=""):
self.user = user
self.username = username
+
class MockRequest:
- def __init__(self, username = ""):
- self.user=MockAuthUser(username)
-
-
-@pytest.mark.parametrize("testName, userIsInDefaultAccessGroup, gitcount, dashboardCount, sshMachineCount, groupCount",[
- #user is not part of default group and has respective count of git repo, dashboard, ssh machines and group accesses
- # ("UserInDefaultGroup", True, 10, 20, 30, 40),
- ("UserInDefaultGroup", False, 10, 20, 30, 40),
-
-])
-def test_getDashboardData(monkeypatch, testName, userIsInDefaultAccessGroup, gitcount, dashboardCount, sshMachineCount, groupCount):
-
+ def __init__(self, username=""):
+ self.user = MockAuthUser(username)
+
+
+@pytest.mark.parametrize(
+ "testName, userIsInDefaultAccessGroup, gitcount, dashboardCount, sshMachineCount, groupCount",
+ [
+ # user is not part of default group and has respective count of git repo,
+ # dashboard, ssh machines and group accesses
+ ("UserInDefaultGroup", True, 10, 20, 30, 40),
+ ("UserInDefaultGroup", False, 10, 20, 30, 40),
+ ],
+)
+def test_getDashboardData(
+ monkeypatch,
+ testName,
+ userIsInDefaultAccessGroup,
+ gitcount,
+ dashboardCount,
+ sshMachineCount,
+ groupCount,
+):
class MockUserModelobj:
- def __init__(self,user="", gitusername="", name = ""):
+ def __init__(self, user="", gitusername="", name=""):
self.user = user
self.gitusername = gitusername
self.name = name
- def get (self,user__username=""):
+ def get(self, user__username=""):
self.user = user__username
return MockUserModelobj(user="username", gitusername="username")
class MockGitAccessModelobj:
- def __init__(self, requestInfo = {"":""}):
+ def __init__(self, requestInfo={"": ""}):
self.user = ""
self.requestInfo = requestInfo
- def filter (self,requester="",status=""):
+ def filter(self, requester="", status=""):
if status == "Approved":
return [MockGitAccessModelobj()]
return MockGitAccessModelobj()
class MockGroupV2:
- def filter (self,name = "", status = ""):
+ def filter(self, name="", status=""):
if status == "Approved":
return [MockGroupV2()]
return MockGitAccessModelobj()
@@ -61,70 +68,78 @@ def filter(self, user="", status="", access__access_tag=""):
dashboard.append(i)
return dashboard
elif access__access_tag == "github_access":
- gitRepo=[]
+ gitRepo = []
for i in range(gitcount):
gitRepo.append(i)
return gitRepo
elif access__access_tag == "ssh":
- ssh=[]
+ ssh = []
for i in range(sshMachineCount):
ssh.append(i)
return ssh
else:
- group=[]
+ group = []
for i in range(groupCount):
group.append(i)
return group
+
class Group:
def __init__(self):
- self.name= ""
+ self.name = ""
class MockMembershipV2:
- def __init__(self,is_owner = False, approver = "", status = "", user = ""):
+ def __init__(self, is_owner=False, approver="", status="", user=""):
self.is_owner = is_owner
self.approver = approver
self.status = status
self.user = user
self.group = Group()
- def filter (self,user = "", status = "", group = "", is_owner = False):
+
+ def filter(self, user="", status="", group="", is_owner=False):
if is_owner:
return [MockMembershipV2()]
return MockMembershipV2()
- def only (self,filter):
+
+ def only(self, filter):
return MockMembershipV2()
def create(*args, **kwargs):
return MockMembershipV2()
+
def save(self):
return ""
+
def __str__(self):
- if userIsInDefaultAccessGroup :
+ if userIsInDefaultAccessGroup:
return DEFAULT_ACCESS_GROUP
else:
return ""
+
def __len__(self):
return groupCount
+
class MockThread:
def start(self):
return True
def mockgenerateUserMappings(*args, **kwargs):
return []
+
views_helper.generateUserMappings = mockgenerateUserMappings
+
def mock_Thread(*args, **kwargs):
- return MockThread()
+ return MockThread()
monkeypatch.setattr(threading, "Thread", mock_Thread)
models.MembershipV2.objects = MockMembershipV2()
models.User.objects = MockUserModelobj()
models.UserAccessMapping.objects = MockUserAccessMapping()
- models.gitAcces.objects = MockGitAccessModelobj()
models.GroupV2.objects = MockGroupV2()
request = MockRequest(username="username1")
context = views_helper.getDashboardData(request)
- assert context["regions"] == ['eu-central-1']
+ assert context["regions"] == ["eu-central-1"]
assert context["gitCount"] == gitcount
assert context["dashboardCount"] == dashboardCount
assert context["sshMachineCount"] == sshMachineCount
- assert context["groupCount"] == groupCount
\ No newline at end of file
+ assert context["groupCount"] == groupCount
diff --git a/bootprocess/views.py b/bootprocess/views.py
index f0e62880..e8e25a82 100644
--- a/bootprocess/views.py
+++ b/bootprocess/views.py
@@ -2,26 +2,22 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
import logging
-from Access.models import User as access_user, MembershipV2, GroupV2, gitAcces, UserAccessMapping
-from BrowserStackAutomation.settings import DEFAULT_ACCESS_GROUP
-from Access.views import executeGroupAccess, generateUserMappings
-import threading
-import json
-import datetime
from .views_helper import getDashboardData
logger = logging.getLogger(__name__)
# Create your views here.
+
+
@login_required
def logout_view(request):
"""Logout View runs when logout url call"""
logout(request)
logger.debug("User: {0} is logging out".format(request.user.username))
- return render(request,"registration/login.html")
+ return render(request, "registration/login.html")
@login_required
def dashboard(request):
"""Loaded dashboard"""
context = getDashboardData(request)
- return render(request,"BSOps/dashboard.html",context)
\ No newline at end of file
+ return render(request, "BSOps/dashboard.html", context)
diff --git a/bootprocess/views_helper.py b/bootprocess/views_helper.py
index e92400cc..a3c1e5ec 100644
--- a/bootprocess/views_helper.py
+++ b/bootprocess/views_helper.py
@@ -1,5 +1,10 @@
import logging
-from Access.models import User as access_user, MembershipV2, GroupV2, gitAcces, UserAccessMapping
+from Access.models import (
+ User as access_user,
+ MembershipV2,
+ GroupV2,
+ UserAccessMapping,
+)
from BrowserStackAutomation.settings import DEFAULT_ACCESS_GROUP
from Access.views_helper import executeGroupAccess, generateUserMappings
import threading
@@ -8,34 +13,65 @@
logger = logging.getLogger(__name__)
+
def getDashboardData(request):
- user=access_user.objects.get(user__username=request.user)
+ user = access_user.objects.get(user__username=request.user)
# Add users to DEFAULT_ACCESS_GROUP
- user_membership=str(MembershipV2.objects.filter(user=user).filter(status="Approved"))
+ user_membership = str(
+ MembershipV2.objects.filter(user=user).filter(status="Approved")
+ )
if DEFAULT_ACCESS_GROUP not in user_membership:
- group_objects = GroupV2.objects.filter(name=DEFAULT_ACCESS_GROUP).filter(status='Approved')
+ group_objects = GroupV2.objects.filter(name=DEFAULT_ACCESS_GROUP).filter(
+ status="Approved"
+ )
if len(group_objects) > 0:
group = group_objects[0]
- group_members = MembershipV2.objects.filter(group=group).filter(status="Approved").only('user')
- group_owner = [member for member in group_members.filter(is_owner = True)]
- membership_id = user.name+'-'+str(group)+'-membership-'+datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
- member = MembershipV2.objects.create(group=group,user=user,reason='New joiner added to BrowserStack defaut access group.',membership_id=membership_id,requested_by=group_owner[0].user)
+ group_members = (
+ MembershipV2.objects.filter(group=group)
+ .filter(status="Approved")
+ .only("user")
+ )
+ group_owner = [member for member in group_members.filter(is_owner=True)]
+ membership_id = (
+ user.name
+ + "-"
+ + str(group)
+ + "-membership-"
+ + datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
+ )
+ member = MembershipV2.objects.create(
+ group=group,
+ user=user,
+ reason="New joiner added to BrowserStack defaut access group.",
+ membership_id=membership_id,
+ requested_by=group_owner[0].user,
+ )
member.approver = group_owner[0].user
- member.status = 'Approved'
+ member.status = "Approved"
user_mappings_list = generateUserMappings(user, group, member)
member.save()
group_name = member.group.name
- access_accept_thread = threading.Thread(target=executeGroupAccess, args=(request, group_name, user_mappings_list))
+ access_accept_thread = threading.Thread(
+ target=executeGroupAccess,
+ args=(request, group_name, user_mappings_list),
+ )
access_accept_thread.start()
- logger.debug("Process has been started for the Approval of request - " + membership_id + " - Approver=" + request.user.username)
+ logger.debug(
+ "Process has been started for the Approval of request - "
+ + membership_id
+ + " - Approver="
+ + request.user.username
+ )
if not user.gitusername:
- logger.debug("Redirecting User to Fill out git username and his public key for ssh access.")
+ logger.debug(
+ "Redirecting User to Fill out git username and his public key for ssh access."
+ )
# return redirect('updateUserInfo')
- with open('instanceTypes.json') as data_file:
+ with open("instanceTypes.json") as data_file:
data = json.load(data_file)
ec2_regions = list(data.keys())
@@ -47,25 +83,31 @@ def getDashboardData(request):
sshMachineCount = 0
groupCount = 0
- #TODO: Is this code even used?
- gitRepos = gitAcces.objects.filter(requester=str(request.user)).filter(status='Approved')
- for repos in gitRepos:
- if 'selectedRepoList' in repos.requestInfo:
- gitCount += len(repos.requestInfo['selectedRepoList'])
- for re in repos.requestInfo['selectedRepoList']:
- dataList.append({'name':re,'accessType':"GIT",'accessLevel':repos.requestInfo['gitAccessLevel'][0],'type':'Personal'})
- else:
- gitCount += 1
-
- dashboardCount = len(UserAccessMapping.objects.filter(user=request.user.user, status="Approved", access__access_tag="other"))
- sshMachineCount = len(UserAccessMapping.objects.filter(user=request.user.user, status="Approved", access__access_tag="ssh"))
- gitCount = len(UserAccessMapping.objects.filter(user=request.user.user, status="Approved", access__access_tag="github_access"))
- groupCount = len(MembershipV2.objects.filter(user=request.user.user, status="Approved"))
+ dashboardCount = len(
+ UserAccessMapping.objects.filter(
+ user=request.user.user, status="Approved", access__access_tag="other"
+ )
+ )
+ sshMachineCount = len(
+ UserAccessMapping.objects.filter(
+ user=request.user.user, status="Approved", access__access_tag="ssh"
+ )
+ )
+ gitCount = len(
+ UserAccessMapping.objects.filter(
+ user=request.user.user,
+ status="Approved",
+ access__access_tag="github_access",
+ )
+ )
+ groupCount = len(
+ MembershipV2.objects.filter(user=request.user.user, status="Approved")
+ )
- context['regions'] = ec2_regions
- context['gitCount'] = gitCount
- context['dashboardCount'] = dashboardCount
- context['sshMachineCount'] = sshMachineCount
- context['groupCount'] = groupCount
+ context["regions"] = ec2_regions
+ context["gitCount"] = gitCount
+ context["dashboardCount"] = dashboardCount
+ context["sshMachineCount"] = sshMachineCount
+ context["groupCount"] = groupCount
- return context
\ No newline at end of file
+ return context
diff --git a/instanceTypes.json b/instanceTypes.json
new file mode 100644
index 00000000..7721b839
--- /dev/null
+++ b/instanceTypes.json
@@ -0,0 +1,38 @@
+{
+ "eu-central-1": {
+ "instanceTypes": [
+ {
+ "type": "t3.nano",
+ "dispName": "General -- CPU: 1 -- RAM: 0.5 -- $0.0067/hr"
+ },
+ {
+ "type": "t3.micro",
+ "dispName": "General -- CPU: 1 -- RAM: 1 -- $0.0134/hr"
+ },
+ {
+ "type": "t3.small",
+ "dispName": "General -- CPU: 1 -- RAM: 2 -- $0.0268/hr"
+ },
+ {
+ "type": "t3.medium",
+ "dispName": "General -- CPU: 2 -- RAM: 4 -- $0.0536/hr"
+ },
+ {
+ "type": "t3.large",
+ "dispName": "General -- CPU: 2 -- RAM: 8 -- $0.1072/hr"
+ },
+ {
+ "type": "m5.large",
+ "dispName": "EBS-Optimized -- CPU: 2 -- RAM: 8 -- $0.1150/hr"
+ },
+ {
+ "type": "c5.large",
+ "dispName": "Compute -- CPU: 2 -- RAM: 4 -- $0.0970/hr"
+ },
+ {
+ "type": "c5.xlarge",
+ "dispName": "Compute -- CPU: 4 -- RAM: 8 -- $0.1940/hr"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/templates/BSOps/dashboard.html b/templates/BSOps/dashboard.html
new file mode 100644
index 00000000..d78d005f
--- /dev/null
+++ b/templates/BSOps/dashboard.html
@@ -0,0 +1,193 @@
+{% extends 'global_layout.html' %}
+{% load static %}
+
+{% block content_body %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+