Skip to content

Commit

Permalink
Merge pull request #4689 from korycins/feature/bot_authentication
Browse files Browse the repository at this point in the history
Add logic to grant access to api through a token
  • Loading branch information
maarcingebala committed Sep 5, 2019
2 parents 854bcd3 + 38c9f21 commit 0ea50c9
Show file tree
Hide file tree
Showing 33 changed files with 866 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .isort.cfg

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add mutation to change the authenticated user's password - #4656 by @fowczarek
- Add an functionality to sort products by their "minimal variant price" - #4416 by @derenio
- New stripe gateway implementation based on Stripe PaymentIntents API - #4606 by @salwator
- Service (bot) accounts - backend support - #4689 by @korycins
- Change AddressValidationRules API - #4655 by @Kwaidan00
- Refactor account deletion mutations - #4668 by @fowczarek
- Upgraded django-prices from v1 to v2.1. Currency codes are now locked at 3 characters max by default for consistency. - #4639 by @NyanKiyoshi
Expand Down
75 changes: 75 additions & 0 deletions saleor/account/migrations/0033_serviceaccount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Generated by Django 2.2.4 on 2019-09-05 10:10

import django.contrib.postgres.fields.jsonb
import oauthlib.common
from django.db import migrations, models

import saleor.core.utils.json_serializer


class Migration(migrations.Migration):

dependencies = [
("auth", "0011_update_proxy_permissions"),
("account", "0032_remove_user_token"),
]

operations = [
migrations.CreateModel(
name="ServiceAccount",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"private_meta",
django.contrib.postgres.fields.jsonb.JSONField(
blank=True,
default=dict,
encoder=saleor.core.utils.json_serializer.CustomJsonEncoder,
null=True,
),
),
(
"meta",
django.contrib.postgres.fields.jsonb.JSONField(
blank=True,
default=dict,
encoder=saleor.core.utils.json_serializer.CustomJsonEncoder,
null=True,
),
),
("name", models.CharField(max_length=60)),
(
"auth_token",
models.CharField(
default=oauthlib.common.generate_token,
max_length=30,
unique=True,
),
),
("created", models.DateTimeField(auto_now_add=True)),
("is_active", models.BooleanField(default=True)),
(
"permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this service.",
related_name="service_set",
related_query_name="service",
to="auth.Permission",
verbose_name="service account permissions",
),
),
],
options={
"permissions": (("manage_service_accounts", "Manage service account"),)
},
)
]
57 changes: 56 additions & 1 deletion saleor/account/models.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
from typing import Set

from django.conf import settings
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
Permission,
PermissionsMixin,
)
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.db.models import Q, Value
from django.forms.models import model_to_dict
from django.utils import timezone
from django.utils.translation import pgettext_lazy
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country, CountryField
from oauthlib.common import generate_token
from phonenumber_field.modelfields import PhoneNumber, PhoneNumberField
from versatileimagefield.fields import VersatileImageField

Expand Down Expand Up @@ -182,6 +186,57 @@ def get_ajax_label(self):
return self.email


class ServiceAccount(ModelWithMetadata):
name = models.CharField(max_length=60)
auth_token = models.CharField(default=generate_token, unique=True, max_length=30)
created = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
permissions = models.ManyToManyField(
Permission,
verbose_name=_("service account permissions"),
blank=True,
help_text=_("Specific permissions for this service."),
related_name="service_set",
related_query_name="service",
)

class Meta:
permissions = (
(
"manage_service_accounts",
pgettext_lazy("Permission description", "Manage service account"),
),
)

def _get_permissions(self) -> Set[str]:
"""Return the permissions of the service."""
if not self.is_active:
return set()
perm_cache_name = "_service_perm_cache"
if not hasattr(self, perm_cache_name):
perms = self.permissions.all()
perms = perms.values_list("content_type__app_label", "codename").order_by()
setattr(self, perm_cache_name, {f"{ct}.{name}" for ct, name in perms})
return getattr(self, perm_cache_name)

def has_perms(self, perm_list):
"""Return True if the service has each of the specified permissions."""
if not self.is_active:
return False

wanted_perms = set(perm_list)
actual_perms = self._get_permissions()

return (wanted_perms & actual_perms) == wanted_perms

def has_perm(self, perm):
"""Return True if the service has the specified permission."""
if not self.is_active:
return False

return perm in self._get_permissions()


class CustomerNote(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
Expand Down
1 change: 1 addition & 0 deletions saleor/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
MODELS_PERMISSIONS = [
"account.manage_users",
"account.manage_staff",
"account.manage_service_accounts",
"account.impersonate_users",
"discount.manage_discounts",
"giftcard.manage_gift_card",
Expand Down
18 changes: 17 additions & 1 deletion saleor/graphql/account/filters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import django_filters
from django.db.models import Count, Sum

from ...account.models import User
from ...account.models import ServiceAccount, User
from ..core.filters import EnumFilter, ObjectTypeFilter
from ..core.types.common import DateRangeInput, IntRangeInput, PriceRangeInput
from ..utils import filter_by_query_param
Expand Down Expand Up @@ -69,6 +69,13 @@ def filter_search(qs, _, value):
return qs


def filter_service_account_search(qs, _, value):
search_fields = ("name",)
if value:
qs = filter_by_query_param(qs, value, search_fields)
return qs


class CustomerFilter(django_filters.FilterSet):
date_joined = ObjectTypeFilter(
input_class=DateRangeInput, method=filter_date_joined
Expand All @@ -95,6 +102,15 @@ class Meta:
]


class ServiceAccountFilter(django_filters.FilterSet):
search = django_filters.CharFilter(method=filter_service_account_search)
is_active = django_filters.BooleanFilter()

class Meta:
model = ServiceAccount
fields = ["search", "is_active"]


class StaffUserFilter(django_filters.FilterSet):
status = EnumFilter(input_class=StaffMemberStatus, method=filter_status)
search = django_filters.CharFilter(method=filter_search)
Expand Down
120 changes: 120 additions & 0 deletions saleor/graphql/account/mutations/service_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import graphene

from saleor.core.permissions import get_permissions

from ....account import models
from ...core.enums import PermissionEnum
from ...core.mutations import (
ClearMetaBaseMutation,
ModelDeleteMutation,
ModelMutation,
UpdateMetaBaseMutation,
)


class ServiceAccountInput(graphene.InputObjectType):
name = graphene.types.String(description="Name of the service account")
is_active = graphene.types.Boolean(
description="Determine if this service account should be enabled"
)
permissions = graphene.List(
PermissionEnum,
description="List of permission code names to assign to this service account.",
)


class ServiceAccountCreate(ModelMutation):
auth_token = graphene.types.String(
description="The newly created authentication token"
)

class Arguments:
input = ServiceAccountInput(
required=True,
description="Fields required to create a new service account.",
)

class Meta:
description = "Creates a new service account"
model = models.ServiceAccount
permissions = ("account.manage_service_accounts",)

@classmethod
def clean_input(cls, info, instance, data):
cleaned_input = super().clean_input(info, instance, data)
# clean and prepare permissions
if "permissions" in cleaned_input:
permissions = cleaned_input.pop("permissions")
cleaned_input["permissions"] = get_permissions(permissions)
return cleaned_input

@classmethod
def perform_mutation(cls, root, info, **data):
instance = cls.get_instance(info, **data)
data = data.get("input")
cleaned_input = cls.clean_input(info, instance, data)
instance = cls.construct_instance(instance, cleaned_input)
cls.clean_instance(instance)
cls.save(info, instance, cleaned_input)
cls._save_m2m(info, instance, cleaned_input)
response = cls.success_response(instance)
response.auth_token = instance.auth_token
return response

@classmethod
def success_response(cls, instance):
response = super().success_response(instance)
response.auth_token = instance.auth_token
return response


class ServiceAccountUpdate(ModelMutation):
class Arguments:
id = graphene.ID(
description="ID of a service account to update.", required=True
)
input = ServiceAccountInput(
required=True,
description="Fields required to update an existing service account.",
)

class Meta:
description = "Updates an existing service account"
model = models.ServiceAccount
permissions = ("account.manage_service_accounts",)

@classmethod
def clean_input(cls, info, instance, data):
cleaned_input = super().clean_input(info, instance, data)
# clean and prepare permissions
if "permissions" in cleaned_input:
cleaned_input["permissions"] = get_permissions(cleaned_input["permissions"])
return cleaned_input


class ServiceAccountDelete(ModelDeleteMutation):
class Arguments:
id = graphene.ID(
description="ID of a service account to delete.", required=True
)

class Meta:
description = "Deletes a service account"
model = models.ServiceAccount
permissions = ("account.manage_service_accounts",)


class ServiceAccountUpdatePrivateMeta(UpdateMetaBaseMutation):
class Meta:
description = "Updates private metadata for a service account."
permissions = ("account.manage_service_accounts",)
model = models.ServiceAccount
public = False


class ServiceAccountClearStoredPrivateMeta(ClearMetaBaseMutation):
class Meta:
description = "Clear stored metadata value."
model = models.ServiceAccount
permissions = ("account.manage_service_accounts",)
public = False
6 changes: 6 additions & 0 deletions saleor/graphql/account/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,19 @@ def resolve_staff_users(info, query):
return gql_optimizer.query(qs, info)


def resolve_service_accounts(info):
qs = models.ServiceAccount.objects.all()
return gql_optimizer.query(qs, info)


def resolve_address_validation_rules(
info,
country_code: str,
country_area: Optional[str],
city: Optional[str],
city_area: Optional[str],
):

params = {
"country_code": country_code,
"country_area": country_area,
Expand Down

0 comments on commit 0ea50c9

Please sign in to comment.