Назначение разрешений нескольким конечным точкам для пользователя и компании

код происхождения:

# add this class to make the code can be a reproducible example.In acutually environment all attribute could be True or False.
class Capabilities:
    def __init__(self) -> None:
        self.canEditCompany = True
        self.canDeleteCompany = True
        self.canShareCompany = True
        self.canAddFiscal = True
        self.canAddCategory = True
        self.canEditCategory = True
        self.canGetCategory = True
        self.canAddAccount = True
        self.canGetAccount = True
        self.canEditAccount = True
        self.canAddTransaction = True
        self.canAddScan = True
        self.canAddTax = True

def permission_control(user_email,company):
    capabilities = Capabilities()
    casbin_roles = []
    if capabilities.canEditCompany:
        casbin_roles.append((user_email, company, 'PUT'))
    if capabilities.canDeleteCompany:
        casbin_roles.append((user_email, company, 'Delete'))
    if capabilities.canShareCompany:
        casbin_roles.append((user_email, f'{company}/sharing', 'Post'))
    if capabilities.canAddFiscal:
        casbin_roles.append((user_email, f'{company}/fiscal_year', 'POST'))
    if capabilities.canAddCategory:
        casbin_roles.append((user_email, f'{company}/fiscals/*/categories', 'Post'))
    if capabilities.canEditCategory:
        casbin_roles.append((user_email, f'{company}/fiscals/*/categories/*', 'PUT'))
    if capabilities.canGetCategory:
        casbin_roles.append((user_email, f'{company}/fiscals/*/categories', 'GET'))
    if capabilities.canAddAccount:
        casbin_roles.append((user_email, f'{company}/fiscals/*/accounts', 'POST'))
    if capabilities.canGetAccount:
        casbin_roles.append((user_email, f'{company}/fiscals/*/accounts', 'GET'))
    if capabilities.canEditAccount:
        casbin_roles.append((user_email, f'{company}/fiscals/*/accounts/*', 'PUT'))
    if capabilities.canAddAccount:
        casbin_roles.append((user_email, f'{company}/fiscals/*/account', 'POST'))
    if capabilities.canAddTax:
        casbin_roles.append((user_email, f'{company}/tax', 'POST'))
    if capabilities.canAddScan:
        casbin_roles.append((user_email, f'{company}/scan', 'POST'))
    if capabilities.canAddTransaction:
        casbin_roles.append((user_email, f'{company}/accounts/*', 'POST'))
    return casbin_roles

моя попытка:

import functools
def permission_control(user_email,company):

    def partial(attribute_name):
        return functools.partial(getattr,capabilities,attribute_name)
        
    capabilities = Capabilities()
    permissions = ["canEditCompany","canDeleteCompany","canShareCompany","canAddFiscal","canAddCategory","canEditCategory","canGetCategory","canAddAccount","canGetAccount","canEditAccount","canAddAccount","canAddTax","canAddScan","canAddTransaction"]
    urls = [(company, 'PUT'),(company, 'Delete'),(f'{company}/sharing', 'Post'),(f'{company}/fiscal_year', 'POST'),(f'{company}/fiscals/*/categories', 'Post'),(f'{company}/fiscals/*/categories/*', 'PUT'),(f'{company}/fiscals/*/categories', 'GET'),(f'{company}/fiscals/*/accounts', 'POST'),(f'{company}/fiscals/*/accounts', 'GET'),(f'{company}/fiscals/*/accounts/*', 'PUT'),(f'{company}/fiscals/*/account', 'POST'),(f'{company}/tax', 'POST'),(f'{company}/scan', 'POST'),(f'{company}/accounts/*', 'POST')]
    return [((user_email,) + url) for permission,url in zip(permissions,urls) if partial(permission)()]

вопрос

  • Есть ли лучший, более питонический способ добиться этого?
  • Я думаю, что мой код может быть даже хуже, чем исходный код, потому что его трудно понять / поддерживать моим коллегам. Я прав?

2 ответа
2

Я мог бы сделать что-то вроде этого:

class Capabilities:
    def __init__(self) -> None:
        self.canEditCompany = True
        self.canDeleteCompany = True
        self.canShareCompany = True
        self.canAddFiscal = True
        self.canAddCategory = True
        self.canEditCategory = True
        self.canGetCategory = True
        self.canAddAccount = True
        self.canGetAccount = True
        self.canEditAccount = True
        self.canAddTransaction = True
        self.canAddScan = True
        self.canAddTax = True

 actions_dict = {
    "canEditCompany":    lambda company: (company, 'PUT'),
    "canDeleteCompany":  lambda company: (company, 'Delete'),
    "canShareCompany":   lambda company: (f'{company}/sharing', 'Post'),
    "canAddFiscal":      lambda company: (f'{company}/fiscal_year', 'POST'),
    "canAddCategory":    lambda company: (f'{company}/fiscals/*/categories', 'Post'),
    "canEditCategory":   lambda company: (f'{company}/fiscals/*/categories/*', 'PUT'),
    "canGetCategory":    lambda company: (f'{company}/fiscals/*/categories', 'GET'),
    "canAddAccount":     lambda company: (f'{company}/fiscals/*/accounts', 'POST'),
    "canGetAccount":     lambda company: (f'{company}/fiscals/*/accounts', 'GET'),
    "canEditAccount":    lambda company: (f'{company}/fiscals/*/accounts/*', 'PUT'),
    "canAddAccount":     lambda company: (f'{company}/fiscals/*/account', 'POST'),
    "canAddTax":         lambda company: (f'{company}/tax', 'POST'),
    "canAddScan":        lambda company: (f'{company}/scan', 'POST'),
    "canAddTransaction": lambda company: (f'{company}/accounts/*', 'POST')
}


def permission_control(user_email, company):
    capabilities = Capabilities()
    return [(user_email, *action(company)) for capability, action in actions_dict.items() if getattr(capabilities, capability)]

Я согласен с тем, что ваш код был в некотором роде менее читабельным, но я думаю, что вы двигались в правильном направлении, убрав это ужасное дерево if, которое, на мой взгляд, определенно считается анти-шаблоном. Я думаю, что хранить ввод и результат вместе в словаре гораздо удобнее, чем хранить их в двух отдельных списках, как у вас, и используя lambda функций, мы можем создать dict в глобальном пространстве имен, чтобы его не нужно было создавать заново каждый раз при вызове функции, как ваш код делал с вашими двумя списками.


РЕДАКТИРОВАТЬ: Следуя предложению в комментариях, еще лучшим решением было бы использовать format-strings скорее, чем lambdas:

class Capabilities:
    def __init__(self) -> None:
        self.canEditCompany = True
        self.canDeleteCompany = True
        self.canShareCompany = True
        self.canAddFiscal = True
        self.canAddCategory = True
        self.canEditCategory = True
        self.canGetCategory = True
        self.canAddAccount = True
        self.canGetAccount = True
        self.canEditAccount = True
        self.canAddTransaction = True
        self.canAddScan = True
        self.canAddTax = True

actions_dict = {
    "canEditCompany":    ('{}', 'PUT'),
    "canDeleteCompany":  ('{}', 'Delete'),
    "canShareCompany":   ('{}/sharing', 'Post'),
    "canAddFiscal":      ('{}/fiscal_year', 'POST'),
    "canAddCategory":    ('{}/fiscals/*/categories', 'Post'),
    "canEditCategory":   ('{}/fiscals/*/categories/*', 'PUT'),
    "canGetCategory":    ('{}/fiscals/*/categories', 'GET'),
    "canAddAccount":     ('{}/fiscals/*/accounts', 'POST'),
    "canGetAccount":     ('{}/fiscals/*/accounts', 'GET'),
    "canEditAccount":    ('{}/fiscals/*/accounts/*', 'PUT'),
    "canAddAccount":     ('{}/fiscals/*/account', 'POST'),
    "canAddTax":         ('{}/tax', 'POST'),
    "canAddScan":        ('{}/scan', 'POST'),
    "canAddTransaction": ('{}/accounts/*', 'POST')
}


def permission_control(user_email, company):
    capabilities = Capabilities()

    return [
        (user_email, url.format(company), method)
        for capability, (url, method) in actions_dict.items() 
        if getattr(capabilities, capability)
    ]

ВТОРОЕ РЕДАКТИРОВАНИЕ: как было указано в комментариях, ответ со строками формата можно улучшить еще больше, предварительно привязав format метод форматных строк. Это немного более производительно и ограничивает струны одной целью.

Я также изменил структуру данных с str: tuple[str, str] словарь в список NamedTuples. Словарь на этом этапе терял смысл. NamedTuples не являются строго обязательными в отличие от кортежей, но я думаю, что их использование делает намерение кода более ясным и улучшает читаемость.

from typing import NamedTuple, Callable, Literal

class Capabilities:
    def __init__(self) -> None:
        self.canEditCompany = True
        self.canDeleteCompany = True
        self.canShareCompany = True
        self.canAddFiscal = True
        self.canAddCategory = True
        self.canEditCategory = True
        self.canGetCategory = True
        self.canAddAccount = True
        self.canGetAccount = True
        self.canEditAccount = True
        self.canAddTransaction = True
        self.canAddScan = True
        self.canAddTax = True


class CapabilityTuple(NamedTuple):
    capability: str
    url_factory: Callable[[str], str]

    # Any reason why some of these aren't all-uppercase in your question?
    # HTTP methods are usually all-uppercase
    method: Literal['PUT', 'Delete', 'POST', 'GET', 'Post'] 


actions_list = [
    CapabilityTuple(capability, url.format, method) for  capability, url, method in (
        ("canEditCompany",    '{}',                        'PUT'),
        ("canDeleteCompany",  '{}',                        'Delete'),
        ("canShareCompany",   '{}/sharing',                'Post'),
        ("canAddFiscal",      '{}/fiscal_year',            'POST'),
        ("canAddCategory",    '{}/fiscals/*/categories',   'Post'),
        ("canEditCategory",   '{}/fiscals/*/categories/*', 'PUT'),
        ("canGetCategory",    '{}/fiscals/*/categories',   'GET'),
        ("canAddAccount",     '{}/fiscals/*/accounts',     'POST'),
        ("canGetAccount",     '{}/fiscals/*/accounts',     'GET'),
        ("canEditAccount",    '{}/fiscals/*/accounts/*',   'PUT'),
        ("canAddAccount",     '{}/fiscals/*/account',      'POST'),
        ("canAddTax",         '{}/tax',                    'POST'),
        ("canAddScan",        '{}/scan',                   'POST'),
        ("canAddTransaction", '{}/accounts/*',             'POST')
    )
]


def permission_control(user_email, company):
    capabilities = Capabilities()

    return [
        (user_email, url_factory(company), method)
        for capability, url_factory, method in actions_list
        if getattr(capabilities, capability)
    ]

  • 1

    Это полезный ответ. Одна альтернатива для рассмотрения: строки формата, которые все еще можно определить заранее, а не лямбда-выражения, которые немного тяжелее с точки зрения синтаксиса.

    — FMc

  • О, отличная идея! Я включил это в свой ответ — это делает его намного чище

    — Алекс Вэйгуд

  • 1

    да точно. Повышение производительности будет очень незначительным, но, что более важно, строка ограничена только одной целью.

    — Райндериен

  • 1

    Честно говоря, это потому, что я не знал о новом (иш) NamedTuple определение стиля наследования. Это приятно — спасибо, что показали это.

    — Райндериен

  • 1

    Да, мне это нравится гораздо больше — позволяет использовать подсказки типов, а также упрощает расширение класса и добавление ваших собственных методов — например, github.com/willmcgugan/textual/blob/main/src/textual/…. Мне всегда кажется странным, что он находится в модуле набора текста, а не в коллекциях.

    — Алекс Вэйгуд

Я бы немного потупил: без лямбд, без партиалов, без форматирования (у вас есть только суффиксы и ничего больше). Сделайте свои методы HTTP заглавными для единообразия, а также вы можете использовать генератор вместо понимания списка. Для тебя Capabilities вы можете принимать логические kwargs. Предварительно зарегистрируйте все возможные возможности в постоянном словаре, примерно похожем на словарь Алекса. actions_dict но с более сильным набором текста.

Также обратите внимание permission_control это существительное, в котором имена методов должны быть глаголами, т.е. control_permissions или, как я предложил, .evaluate()

Предложенный

from dataclasses import dataclass
from typing import Tuple, Iterable

CapTuple = Tuple[
    str,  # email
    str,  # URL path
    str,  # method
]


@dataclass(frozen=True)
class Capability:
    name: str
    method: str
    path: str


CAPABILITIES = {
    name: Capability(name, method, path)
    for name, method, path in (
        ("canEditCompany",    'PUT',    ''),
        ("canDeleteCompany",  'DELETE', ''),
        ("canShareCompany",   'POST',   '/sharing'),
        ("canAddFiscal",      'POST',   '/fiscal_year'),
        ("canAddCategory",    'POST',   '/fiscals/*/categories'),
        ("canEditCategory",   'PUT',    '/fiscals/*/categories/*'),
        ("canGetCategory",    'GET',    '/fiscals/*/categories'),
        ("canAddAccount",     'POST',   '/fiscals/*/accounts'),
        ("canGetAccount",     'GET',    '/fiscals/*/accounts'),
        ("canEditAccount",    'PUT',    '/fiscals/*/accounts/*'),
        ("canAddAccount",     'POST',   '/fiscals/*/account'),
        ("canAddTax",         'POST',   '/tax'),
        ("canAddScan",        'POST',   '/scan'),
        ("canAddTransaction", 'POST',   '/accounts/*'),
    )
}


class Capabilities:
    def __init__(self, **kwargs: bool):
        self.allowed: Tuple[Capability] = tuple(
            CAPABILITIES[name]
            for name, allowed in kwargs.items()
            if allowed
        )

    def evaluate(self, user_email: str, company: str) -> Iterable[CapTuple]:
        for cap in self.allowed:
            yield user_email, company + cap.path, cap.method


def test() -> None:
    caps = Capabilities(canAddAccount=True, canEditAccount=True, canDeleteCompany=False)
    for user_email, path, method in caps.evaluate('me@here.com', 'here_inc'):
        print(user_email, path, method)


if __name__ == '__main__':
    test()

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *