Skip to content

Commit

Permalink
Merge pull request #68 from wklken/ft_string_contains
Browse files Browse the repository at this point in the history
feat(string_contains): add string_contains support
  • Loading branch information
wklken committed Jul 22, 2022
2 parents 15d3903 + a15b03b commit 19f3ef9
Show file tree
Hide file tree
Showing 12 changed files with 236 additions and 149 deletions.
2 changes: 1 addition & 1 deletion iam/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-

__version__ = "1.2.0"
__version__ = "1.2.1"
7 changes: 5 additions & 2 deletions iam/contrib/converter/queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@


import operator
from six.moves import reduce

from django.db.models import Q

from iam.eval.constants import KEYWORD_BK_IAM_PATH_FIELD_SUFFIX, OP
from iam.eval.expression import field_value_convert
from six.moves import reduce

from .base import Converter

Expand Down Expand Up @@ -84,6 +83,9 @@ def _ends_with(self, left, right):
def _not_ends_with(self, left, right):
return self._negative("{}__endswith", left, right)

def _string_contains(self, left, right):
return self._positive("{}__contains", left, right)

def _lt(self, left, right):
return self._positive("{}__lt", left, right)

Expand Down Expand Up @@ -139,6 +141,7 @@ def convert(self, data):
OP.NOT_STARTS_WITH: self._not_starts_with,
OP.ENDS_WITH: self._ends_with,
OP.NOT_ENDS_WITH: self._not_ends_with,
OP.STRING_CONTAINS: self._string_contains,
OP.LT: self._lt,
OP.LTE: self._lte,
OP.GT: self._gt,
Expand Down
9 changes: 6 additions & 3 deletions iam/contrib/converter/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@


import six

from iam.eval.constants import OP
from iam.eval.expression import field_value_convert

Expand Down Expand Up @@ -75,12 +74,12 @@ def _not_eq(self, left, right):
return self._negative("{} != {}", left, right)

def _in(self, left, right):
# TODO: right shuld be a list
# TODO: right should be a list
right = [self._to_str_present(r, True) for r in right]
return "{} IN ({})".format(left, ",".join([str(r) for r in right]))

def _not_in(self, left, right):
# TODO: right shuld be a list
# TODO: right should be a list
right = [self._to_str_present(r, True) for r in right]
return "{} NOT IN ({})".format(left, ",".join([str(r) for r in right]))

Expand All @@ -103,6 +102,9 @@ def _ends_with(self, left, right):
def _not_ends_with(self, left, right):
return self._negative("{} NOT LIKE '%{}'", left, right, False)

def _string_contains(self, left, right):
return self._positive("{} LIKE '%{}%'", left, right, False)

def _lt(self, left, right):
return self._positive("{} < {}", left, right)

Expand Down Expand Up @@ -145,6 +147,7 @@ def convert(self, data):
OP.NOT_STARTS_WITH: self._not_starts_with,
OP.ENDS_WITH: self._ends_with,
OP.NOT_ENDS_WITH: self._not_ends_with,
OP.STRING_CONTAINS: self._string_contains,
OP.LT: self._lt,
OP.LTE: self._lte,
OP.GT: self._gt,
Expand Down
7 changes: 5 additions & 2 deletions iam/eval/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class OP(object):
ENDS_WITH = "ends_with"
NOT_ENDS_WITH = "not_ends_with"

STRING_CONTAINS = "string_contains"

LT = "lt"
LTE = "lte"
GT = "gt"
Expand All @@ -47,12 +49,13 @@ class OP(object):
NOT_EQ,
IN,
NOT_IN,
CONTAINS,
NOT_CONTAINS,
# CONTAINS,
# NOT_CONTAINS,
STARTS_WITH,
NOT_STARTS_WITH,
ENDS_WITH,
NOT_ENDS_WITH,
STRING_CONTAINS,
ANY,
],
"numberic": [EQ, NOT_EQ, IN, NOT_IN, LT, LTE, GT, GTE],
Expand Down
197 changes: 82 additions & 115 deletions iam/eval/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def expr(self):
def calculate(self, left, right):
pass

def _eval_positive(self, attr, attr_is_array, value, value_is_array): # NOQA
def _eval_positive(self, object_attr, is_object_attr_array, policy_value): # NOQA
"""
positive:
- 1 hit: return True
Expand All @@ -121,65 +121,22 @@ def _eval_positive(self, attr, attr_is_array, value, value_is_array): # NOQA
op = eq => one of attr equals one of value
attr = 1; value = 1; True
attr = 1; value = [1, 2]; True
attr = [1, 2]; value = 2; True
attr = [1, 2]; value = [5, 1]; True
attr = [1, 2]; value = [3, 4]; False
"""
if self.op == OP.ANY:
return self.calculate(attr, value)

# 1. IN/NOT_IN value is a list, just only check attr
if self.op in (OP.IN,):
if attr_is_array:
for a in attr:
if self.calculate(a, value):
return True
return False

return self.calculate(attr, value)

# 2. CONTAINS/NOT_CONTAINS attr is a list, just check value
if self.op in (OP.CONTAINS,):
if value_is_array:
for v in value:
if self.calculate(attr, v):
return True
return False

return self.calculate(attr, value)

# 3. Others, check both attr and value
# 3.1 both not array, the most common situation
if not (value_is_array or attr_is_array):
return self.calculate(attr, value)

# 3.2 only value is array, the second common situation
if value_is_array and (not attr_is_array):
for v in value:
# return early if hit
if self.calculate(attr, v):
# if self.op == OP.ANY:
# return self.calculate(object_attr, policy_value)

# NOTE: here, the policyValue should not be array!
# It's single value (except: the NotIn op policyValue is an array)
if is_object_attr_array:
for a in object_attr:
if self.calculate(a, policy_value):
return True
return False

# 3.3 only attr value is array
if (not value_is_array) and attr_is_array:
for a in attr:
# return early if hit
if self.calculate(a, value):
return True
return False
return self.calculate(object_attr, policy_value)

# 4. both array
for a in attr:
for v in value:
# return early if hit
if self.calculate(a, v):
return True
return False

def _eval_negative(self, attr, attr_is_array, value, value_is_array): # NOQA
def _eval_negative(self, object_attr, is_object_attr_array, policy_value): # NOQA
"""
negative:
- 1 miss: return False
Expand All @@ -189,58 +146,18 @@ def _eval_negative(self, attr, attr_is_array, value, value_is_array): # NOQA
op = not_eq => all of attr should not_eq to all of the value
attr = 1; value = 2; True
attr = 1; value = [2]; True
attr = [1, 2]; value = [3, 4]; True
attr = [1, 2]; value = 3; True
attr = [1, 2]; value = [2, 3]; False
"""
# 1. IN/NOT_IN value is a list, just only check attr
if self.op in (OP.NOT_IN,):
if attr_is_array:
for a in attr:
if not self.calculate(a, value):
return False
return True

return self.calculate(attr, value)

# 2. CONTAINS/NOT_CONTAINS attr is a list, just check value
if self.op in (OP.NOT_CONTAINS,):
if value_is_array:
for v in value:
if not self.calculate(attr, v):
return False
return True

return self.calculate(attr, value)

# 3. Others, check both attr and value
# 3.1 both not array, the most common situation
if not (value_is_array or attr_is_array):
return self.calculate(attr, value)

# 3.2 only value is array, the second common situation
if value_is_array and (not attr_is_array):
for v in value:
if not self.calculate(attr, v):
# NOTE: here, the policyValue should not be array!
# It's single value (except: the NotIn op policyValue is an array)
if is_object_attr_array:
for a in object_attr:
if not self.calculate(a, policy_value):
return False
return True

# 3.3 only attr value is array
if (not value_is_array) and attr_is_array:
for a in attr:
if not self.calculate(a, value):
return False
return True

# 4. both array
for a in attr:
for v in value:
# return early if hit
if not self.calculate(a, v):
return False
return True
return self.calculate(object_attr, policy_value)

def eval(self, obj_set):
"""
Expand All @@ -251,19 +168,64 @@ def eval(self, obj_set):
if one of them is array, or both array
calculate each item in array
"""
attr = obj_set.get(self.field)
value = self.value
object_attr = obj_set.get(self.field)
policy_value = self.value

attr_is_array = isinstance(attr, (list, tuple))
value_is_array = isinstance(value, (list, tuple))
is_object_attr_array = isinstance(object_attr, (list, tuple))
is_policy_value_array = isinstance(policy_value, (list, tuple))

# positive and negative operator
# == 命中一个即返回
# != 需要全部遍历完, 确认全部不等于才返回?
if self.op.startswith("not_"):
return self._eval_negative(attr, attr_is_array, value, value_is_array)
else:
return self._eval_positive(attr, attr_is_array, value, value_is_array)
# any
if self.op == OP.ANY:
return True

# if you add new operator, please read this first: https://github.com/TencentBlueKing/bk-iam-saas/issues/1293
# valid the attr and value first
if self.op in (OP.IN, OP.NOT_IN):
# a in b, a not_in b
# b should be an array, while a can be a single or an array
# so we should make the in expression b always be an array
if not is_policy_value_array:
return False

if self.op == OP.IN:
return self._eval_positive(object_attr, is_object_attr_array, policy_value)
else:
return self._eval_negative(object_attr, is_object_attr_array, policy_value)

if self.op in (OP.CONTAINS, OP.NOT_CONTAINS):
# a contains b, a not_contains b
# a should be an array, b should be a single value
# so, we should make the contains expression b always be a single string,
# while a can be a single value or an array
if not is_object_attr_array or is_policy_value_array:
return False
return self.calculate(object_attr, policy_value)

if self.op in (
OP.EQ,
OP.NOT_EQ,
OP.LT,
OP.LTE,
OP.GT,
OP.GTE,
OP.STARTS_WITH,
OP.NOT_STARTS_WITH,
OP.ENDS_WITH,
OP.NOT_ENDS_WITH,
OP.STRING_CONTAINS,
):
# a starts_with b, a not_starts_with, a ends_with b, a not_ends_with b
# b should be a single value, while a can be a single value or an array
if is_policy_value_array:
return False

# positive and negative operator
# == 命中一个即返回
# != 需要全部遍历完, 确认全部不等于才返回?
if self.op.startswith("not_"):
return self._eval_negative(object_attr, is_object_attr_array, policy_value)
else:
return self._eval_positive(object_attr, is_object_attr_array, policy_value)


class EqualOperator(BinaryOperator):
Expand Down Expand Up @@ -318,7 +280,6 @@ def calculate(self, left, right):

class StartsWithOperator(BinaryOperator):
def __init__(self, field, value):
# TODO: value should be string?
super(StartsWithOperator, self).__init__(OP.STARTS_WITH, field, value)

def calculate(self, left, right):
Expand All @@ -327,7 +288,6 @@ def calculate(self, left, right):

class NotStartsWithOperator(BinaryOperator):
def __init__(self, field, value):
# TODO: value should be string?
super(NotStartsWithOperator, self).__init__(OP.NOT_STARTS_WITH, field, value)

def calculate(self, left, right):
Expand All @@ -336,7 +296,6 @@ def calculate(self, left, right):

class EndsWithOperator(BinaryOperator):
def __init__(self, field, value):
# TODO: value should be string?
super(EndsWithOperator, self).__init__(OP.ENDS_WITH, field, value)

def calculate(self, left, right):
Expand All @@ -345,13 +304,20 @@ def calculate(self, left, right):

class NotEndsWithOperator(BinaryOperator):
def __init__(self, field, value):
# TODO: value should be string?
super(NotEndsWithOperator, self).__init__(OP.NOT_ENDS_WITH, field, value)

def calculate(self, left, right):
return not left.endswith(right)


class StringContainsOperator(BinaryOperator):
def __init__(self, field, value):
super(StringContainsOperator, self).__init__(OP.STRING_CONTAINS, field, value)

def calculate(self, left, right):
return right in left


class LTOperator(BinaryOperator):
def __init__(self, field, value):
# TODO: field / value should be numberic
Expand Down Expand Up @@ -407,6 +373,7 @@ def calculate(self, left, right):
OP.NOT_STARTS_WITH: NotStartsWithOperator,
OP.ENDS_WITH: EndsWithOperator,
OP.NOT_ENDS_WITH: NotEndsWithOperator,
OP.STRING_CONTAINS: StringContainsOperator,
OP.LT: LTOperator,
OP.LTE: LTEOperator,
OP.GT: GTOperator,
Expand Down
Loading

0 comments on commit 19f3ef9

Please sign in to comment.