Skip to content

Commit

Permalink
Merge pull request #4766 from korycins/fix/service_account_with_many_…
Browse files Browse the repository at this point in the history
…tokens

ServiceAccount should be able to have multiple tokens
  • Loading branch information
maarcingebala committed Sep 26, 2019
2 parents 73c1b26 + 8f5f3e0 commit 3742319
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 47 deletions.
52 changes: 52 additions & 0 deletions saleor/account/migrations/0034_service_account_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import django.db.models.deletion
import oauthlib.common
from django.db import migrations, models


def move_existing_token(apps, schema_editor):
ServiceAccount = apps.get_model("account", "ServiceAccount")
for service_account in ServiceAccount.objects.iterator():
service_account.tokens.create(
name="Default", auth_token=service_account.auth_token
)


class Migration(migrations.Migration):

dependencies = [("account", "0033_serviceaccount")]

operations = [
migrations.CreateModel(
name="ServiceAccountToken",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(blank=True, default="", max_length=128)),
(
"auth_token",
models.CharField(
default=oauthlib.common.generate_token,
max_length=30,
unique=True,
),
),
(
"service_account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tokens",
to="account.ServiceAccount",
),
),
],
),
migrations.RunPython(move_existing_token),
migrations.RemoveField(model_name="serviceaccount", name="auth_token"),
]
9 changes: 8 additions & 1 deletion saleor/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ def get_ajax_label(self):

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(
Expand Down Expand Up @@ -237,6 +236,14 @@ def has_perm(self, perm):
return perm in self._get_permissions()


class ServiceAccountToken(models.Model):
service_account = models.ForeignKey(
ServiceAccount, on_delete=models.CASCADE, related_name="tokens"
)
name = models.CharField(blank=True, default="", max_length=128)
auth_token = models.CharField(default=generate_token, unique=True, max_length=30)


class CustomerNote(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
Expand Down
71 changes: 48 additions & 23 deletions saleor/graphql/account/mutations/service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,38 @@


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"
name = graphene.String(description="Name of the service account.")
is_active = graphene.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):
class ServiceAccountTokenInput(graphene.InputObjectType):
name = graphene.String(description="Name of the token.", required=False)
service_account = graphene.ID(description="ID of service account.", required=True)


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

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

class Meta:
description = "Creates a new service account"
model = models.ServiceAccount
description = "Creates a new token."
model = models.ServiceAccountToken
permissions = ("account.manage_service_accounts",)
error_type_class = AccountError
error_type_field = "account_errors"

@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)
Expand All @@ -64,11 +59,41 @@ def perform_mutation(cls, root, info, **data):
response.auth_token = instance.auth_token
return response


class ServiceAccountTokenDelete(ModelDeleteMutation):
class Arguments:
id = graphene.ID(description="ID of an auth token to delete.", required=True)

class Meta:
description = "Deletes an authentication token assigned to service account."
model = models.ServiceAccountToken
permissions = ("account.manage_service_accounts",)
error_type_class = AccountError
error_type_field = "account_errors"


class ServiceAccountCreate(ModelMutation):
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",)
error_type_class = AccountError
error_type_field = "account_errors"

@classmethod
def success_response(cls, instance):
response = super().success_response(instance)
response.auth_token = instance.auth_token
return response
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


class ServiceAccountUpdate(ModelMutation):
Expand Down
5 changes: 5 additions & 0 deletions saleor/graphql/account/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
ServiceAccountClearStoredPrivateMeta,
ServiceAccountCreate,
ServiceAccountDelete,
ServiceAccountTokenCreate,
ServiceAccountTokenDelete,
ServiceAccountUpdate,
ServiceAccountUpdatePrivateMeta,
)
Expand Down Expand Up @@ -228,5 +230,8 @@ class AccountMutations(graphene.ObjectType):
ServiceAccountClearStoredPrivateMeta.Field()
)

service_account_token_create = ServiceAccountTokenCreate.Field()
service_account_token_delete = ServiceAccountTokenDelete.Field()

# Staff deprecated mutation
password_reset = PasswordReset.Field()
35 changes: 31 additions & 4 deletions saleor/graphql/account/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,22 @@ def resolve_order_line(root: models.CustomerEvent, info):
return None


class ServiceAccountToken(CountableDjangoObjectType):
name = graphene.String(description="Name of the authenticated token.")
auth_token = graphene.String(description="Last 4 characters of the token.")

class Meta:
description = "Represents token data."
model = models.ServiceAccountToken
interfaces = [relay.Node]
permissions = ("account.manage_service_accounts",)
only_fields = ["name", "auth_token"]

@staticmethod
def resolve_auth_token(root: models.ServiceAccount, _info, **_kwargs):
return root.auth_token[-4:]


class ServiceAccount(MetadataObjectType, CountableDjangoObjectType):
permissions = graphene.List(
PermissionDisplay, description="List of the service's permissions."
Expand All @@ -165,14 +181,24 @@ class ServiceAccount(MetadataObjectType, CountableDjangoObjectType):
description="Determine if service account will be set active or not."
)
name = graphene.String(description="Name of the service account.")
auth_token = graphene.String(description="Last 4 characters of the token")

tokens = graphene.List(
ServiceAccountToken, description="Last 4 characters of the tokens"
)

class Meta:
description = "Represents service account data."
interfaces = [relay.Node]
model = models.ServiceAccount
permissions = ("account.manage_service_accounts",)
only_fields = ["name" "permissions", "created", "is_active", "auth_token", "id"]
only_fields = [
"name" "permissions",
"created",
"is_active",
"tokens",
"id",
"tokens",
]

@staticmethod
def resolve_permissions(root: models.ServiceAccount, _info, **_kwargs):
Expand All @@ -182,8 +208,9 @@ def resolve_permissions(root: models.ServiceAccount, _info, **_kwargs):
return format_permissions_for_display(permissions)

@staticmethod
def resolve_auth_token(root: models.ServiceAccount, _info, **_kwargs):
return root.auth_token[-4:]
@gql_optimizer.resolver_hints(prefetch_related="tokens")
def resolve_tokens(root: models.ServiceAccount, _info, **_kwargs):
return root.tokens.all()

@staticmethod
def resolve_meta(root, info):
Expand Down
2 changes: 1 addition & 1 deletion saleor/graphql/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def middleware(request):


def get_service_account(auth_token) -> Optional[ServiceAccount]:
qs = ServiceAccount.objects.filter(auth_token=auth_token, is_active=True)
qs = ServiceAccount.objects.filter(tokens__auth_token=auth_token, is_active=True)
return qs.first()


Expand Down
29 changes: 27 additions & 2 deletions saleor/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2198,6 +2198,8 @@ type Mutations {
serviceAccountDelete(id: ID!): ServiceAccountDelete
serviceAccountUpdatePrivateMetadata(id: ID!, input: MetaInput!): ServiceAccountUpdatePrivateMeta
serviceAccountClearStoredPrivateMetadata(id: ID!, input: MetaPath!): ServiceAccountClearStoredPrivateMeta
serviceAccountTokenCreate(input: ServiceAccountTokenInput!): ServiceAccountTokenCreate
serviceAccountTokenDelete(id: ID!): ServiceAccountTokenDelete
passwordReset(email: String!): PasswordReset
}

Expand Down Expand Up @@ -3446,9 +3448,9 @@ input SeoInput {

type ServiceAccount implements Node {
id: ID!
authToken: String
created: DateTime
isActive: Boolean
tokens: [ServiceAccountToken]
privateMeta: [MetaStore]!
meta: [MetaStore]!
permissions: [PermissionDisplay]
Expand All @@ -3474,7 +3476,6 @@ type ServiceAccountCountableEdge {

type ServiceAccountCreate {
errors: [Error!]
authToken: String
accountErrors: [AccountError!]
serviceAccount: ServiceAccount
}
Expand All @@ -3496,6 +3497,30 @@ input ServiceAccountInput {
permissions: [PermissionEnum]
}

type ServiceAccountToken implements Node {
name: String
authToken: String
id: ID!
}

type ServiceAccountTokenCreate {
errors: [Error!]
authToken: String
accountErrors: [AccountError!]
serviceAccountToken: ServiceAccountToken
}

type ServiceAccountTokenDelete {
errors: [Error!]
accountErrors: [AccountError!]
serviceAccountToken: ServiceAccountToken
}

input ServiceAccountTokenInput {
name: String
serviceAccount: ID!
}

type ServiceAccountUpdate {
errors: [Error!]
accountErrors: [AccountError!]
Expand Down
14 changes: 12 additions & 2 deletions tests/api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.test.client import MULTIPART_CONTENT, Client
from graphql_jwt.shortcuts import get_token

from saleor.account.models import User
from saleor.account.models import ServiceAccount, User

from .utils import assert_no_permission

Expand All @@ -28,7 +28,8 @@ def __init__(self, *args, **kwargs):
if not user.is_anonymous:
self.token = get_token(user)
elif service_account:
self.service_token = service_account.auth_token
token = service_account.tokens.first()
self.service_token = token.auth_token if token else None
super().__init__(*args, **kwargs)

def _base_environ(self, **request):
Expand Down Expand Up @@ -153,3 +154,12 @@ def user_list_not_active(user_list):
users = User.objects.filter(pk__in=[user.pk for user in user_list])
users.update(is_active=False)
return users


@pytest.fixture
def service_account(db):
service_account = ServiceAccount.objects.create(
name="Sample service account", is_active=True
)
service_account.tokens.create(name="Default")
return service_account

0 comments on commit 3742319

Please sign in to comment.