Skip to content

Commit

Permalink
Update userName to be case insensitive with UPPER
Browse files Browse the repository at this point in the history
This commit updates the SQL transpiler to produce SQL that matches on
userName in a case insensitive manner. This brings the behavior into
compliance with the RFC:
https://datatracker.ietf.org/doc/html/rfc7643#section-4.1.1

For example, prior to this commit a query like the following would not
find users by the username "Bjenson". After this commit, such users
would be returned.

    userName eq "bjenson"

The comparisons are now done by first passing the field and the value
through the SQL function `UPPER`.

These changes only effect the SQL transpiler as the Django Q transpiler
is already case insensitive.

BREAKING CHANGE: This allows queries that did not match rows before to
match rows now!
  • Loading branch information
logston committed Aug 20, 2022
1 parent b3d5819 commit 1302f69
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 24 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
CHANGE LOG
==========
0.4.0
-----
- Update userName to be case insensitive. #31

BREAKING CHANGE: This allows queries that did not match rows before to
match rows now!


0.3.9
-----
Expand Down
47 changes: 23 additions & 24 deletions src/scim2_filter_parser/transpilers/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,14 @@ class Transpiler(ast.NodeTransformer):
'ge': '>=',
'lt': '<',
'le': '<=',

# These are not valid SCIM comparison ops. They exist to make case
# insensitive logic simpler.
'ieq': 'ILIKE',
'ine': 'NOT ILIKE',
'ico': 'ILIKE',
'isw': 'ILIKE',
'iew': 'ILIKE',
}

matching_op_by_scim_op = {
'co': ('%', '%'),
'sw': ('', '%'),
'ew': ('%', ''),
'ico': ('%', '%'),
'isw': ('', '%'),
'iew': ('%', ''),
}
}


def __init__(self, attr_map, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -123,33 +113,38 @@ def visit_AttrExpr(self, node):
if isinstance(node.attr_path.attr_name, scim2ast.Filter):
full, partial = self.visit_PartialAttrExpr(node.attr_path.attr_name)
if full and partial:
value = self.visit_AttrExprValue(node.value, node.comp_value)
value = self.visit_AttrExprValue(node)
return f'({full} AND {partial} {value})'
elif full:
return full
elif partial:
value = self.visit_AttrExprValue(node.value, node.comp_value)
value = self.visit_AttrExprValue(node)
return f'{partial} {value}'
else:
return None
else:
# Case-insensitivity only needs to be checked in this branch
# because userName is currently the only attribute that can be case
# insensitive and userName can not be a nested part of a complex query (eg.
# emails.type in emails[type eq "Primary"]...).
# https://datatracker.ietf.org/doc/html/rfc7643#section-4.1.1
attr = self.visit(node.attr_path)
if attr is None:
return None

node_value = node.value
value = self.visit_AttrExprValue(node)

if node.case_insensitive:
node_value = 'i' + node_value
return f'UPPER({attr}) {value}'

value = self.visit_AttrExprValue(node_value, node.comp_value)
return f'{attr} {value}'

def visit_AttrExprValue(self, node_value, node_comp_value):
op_sql = self.lookup_op(node_value)
def visit_AttrExprValue(self, node):
op_sql = self.lookup_op(node.value)

item_id = self.get_next_id()

if not node_comp_value:
if not node.comp_value:
self.params[item_id] = None
return op_sql

Expand All @@ -158,15 +153,19 @@ def visit_AttrExprValue(self, node_value, node_comp_value):
# prep item_id to be a str replacement placeholder
item_id_placeholder = '{' + item_id + '}'

if node_value.lower() in self.matching_op_by_scim_op.keys():
if node.value.lower() in self.matching_op_by_scim_op.keys():
# Add appropriate % signs to values in LIKE clause
prefix, suffix = self.lookup_like_matching(node_value)
value = prefix + self.visit(node_comp_value) + suffix
prefix, suffix = self.lookup_like_matching(node.value)
value = prefix + self.visit(node.comp_value) + suffix

else:
value = self.visit(node_comp_value)
value = self.visit(node.comp_value)

self.params[item_id] = value

if node.case_insensitive:
return f'{op_sql} UPPER({item_id_placeholder})'

return f'{op_sql} {item_id_placeholder}'

def visit_AttrPath(self, node):
Expand Down

0 comments on commit 1302f69

Please sign in to comment.