antlr for scim filter parsing, why
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
7f12b14145
commit
16bc7408e7
|
@ -57,6 +57,6 @@ class SCIMSourceViewSet(UsedByMixin, ModelViewSet):
|
|||
queryset = SCIMSource.objects.all()
|
||||
serializer_class = SCIMSourceSerializer
|
||||
lookup_field = "slug"
|
||||
filterset_fields = "__all__"
|
||||
filterset_fields = ["name", "slug"]
|
||||
search_fields = ["name", "slug", "token__identifier", "token__user__username"]
|
||||
ordering = ["name"]
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
"""SCIM Errors"""
|
||||
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
|
||||
|
||||
class PatchError(SentryIgnoredException):
|
||||
"""Error raised within an atomic block when an error happened
|
||||
so nothing is saved"""
|
|
@ -0,0 +1,61 @@
|
|||
grammar ScimFilter;
|
||||
|
||||
parse
|
||||
: filter
|
||||
;
|
||||
|
||||
filter
|
||||
: attrPath SP PR #presentExp
|
||||
| attrPath SP COMPAREOPERATOR SP VALUE #operatorExp
|
||||
| NOT? SP* '(' filter ')' #braceExp
|
||||
| attrPath '[' valPathFilter ']' #valPathExp
|
||||
| filter SP AND SP filter #andExp
|
||||
| filter SP OR SP filter #orExp
|
||||
;
|
||||
|
||||
valPathFilter
|
||||
: attrPath SP PR #valPathPresentExp
|
||||
| attrPath SP COMPAREOPERATOR SP VALUE #valPathOperatorExp
|
||||
| NOT? SP* '(' valPathFilter ')' #valPathBraceExp
|
||||
| valPathFilter SP AND SP valPathFilter #valPathAndExp
|
||||
| valPathFilter SP OR SP valPathFilter #valPathOrExp
|
||||
;
|
||||
|
||||
attrPath
|
||||
: (SCHEMA)? ATTRNAME ('.' ATTRNAME)?
|
||||
;
|
||||
|
||||
COMPAREOPERATOR : EQ | NE | CO | SW | EW | GT | GE | LT | LE;
|
||||
|
||||
EQ : [eE][qQ];
|
||||
NE : [nN][eE];
|
||||
CO : [cC][oO];
|
||||
SW : [sS][wW];
|
||||
EW : [eE][wW];
|
||||
PR : [pP][rR];
|
||||
GT : [gG][tT];
|
||||
GE : [gG][eE];
|
||||
LT : [lL][tT];
|
||||
LE : [lL][eE];
|
||||
|
||||
NOT : [nN][oO][tT];
|
||||
AND : [aA][nN][dD];
|
||||
OR : [oO][rR];
|
||||
|
||||
SP : ' ';
|
||||
|
||||
SCHEMA : 'urn:' (SEGMENT ':')+;
|
||||
|
||||
ATTRNAME : ALPHA (ALPHA | DIGIT | '_' | '-')+;
|
||||
|
||||
fragment SEGMENT : (ALPHA | DIGIT | '_' | '-' | '.')+;
|
||||
|
||||
fragment DIGIT : [0-9];
|
||||
|
||||
fragment ALPHA : [a-z] | [A-Z];
|
||||
|
||||
ESCAPED_QUOTE : '\\"';
|
||||
|
||||
VALUE : '"'(ESCAPED_QUOTE | ~'"')*'"' | 'true' | 'false' | 'null' | DIGIT+('.'DIGIT+)?;
|
||||
|
||||
EXCLUDE : [\b | \t | \r | \n]+ -> skip;
|
|
@ -0,0 +1,65 @@
|
|||
token literal names:
|
||||
null
|
||||
'('
|
||||
')'
|
||||
'['
|
||||
']'
|
||||
'.'
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
' '
|
||||
null
|
||||
null
|
||||
'\\"'
|
||||
null
|
||||
null
|
||||
|
||||
token symbolic names:
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
COMPAREOPERATOR
|
||||
EQ
|
||||
NE
|
||||
CO
|
||||
SW
|
||||
EW
|
||||
PR
|
||||
GT
|
||||
GE
|
||||
LT
|
||||
LE
|
||||
NOT
|
||||
AND
|
||||
OR
|
||||
SP
|
||||
SCHEMA
|
||||
ATTRNAME
|
||||
ESCAPED_QUOTE
|
||||
VALUE
|
||||
EXCLUDE
|
||||
|
||||
rule names:
|
||||
parse
|
||||
filter
|
||||
valPathFilter
|
||||
attrPath
|
||||
|
||||
|
||||
atn:
|
||||
[4, 1, 25, 106, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 23, 8, 1, 1, 1, 5, 1, 26, 8, 1, 10, 1, 12, 1, 29, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 40, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 52, 8, 1, 10, 1, 12, 1, 55, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 69, 8, 2, 1, 2, 5, 2, 72, 8, 2, 10, 2, 12, 2, 75, 9, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 81, 8, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 93, 8, 2, 10, 2, 12, 2, 96, 9, 2, 1, 3, 3, 3, 99, 8, 3, 1, 3, 1, 3, 1, 3, 3, 3, 104, 8, 3, 1, 3, 0, 2, 2, 4, 4, 0, 2, 4, 6, 0, 0, 116, 0, 8, 1, 0, 0, 0, 2, 39, 1, 0, 0, 0, 4, 80, 1, 0, 0, 0, 6, 98, 1, 0, 0, 0, 8, 9, 3, 2, 1, 0, 9, 1, 1, 0, 0, 0, 10, 11, 6, 1, -1, 0, 11, 12, 3, 6, 3, 0, 12, 13, 5, 20, 0, 0, 13, 14, 5, 12, 0, 0, 14, 40, 1, 0, 0, 0, 15, 16, 3, 6, 3, 0, 16, 17, 5, 20, 0, 0, 17, 18, 5, 6, 0, 0, 18, 19, 5, 20, 0, 0, 19, 20, 5, 24, 0, 0, 20, 40, 1, 0, 0, 0, 21, 23, 5, 17, 0, 0, 22, 21, 1, 0, 0, 0, 22, 23, 1, 0, 0, 0, 23, 27, 1, 0, 0, 0, 24, 26, 5, 20, 0, 0, 25, 24, 1, 0, 0, 0, 26, 29, 1, 0, 0, 0, 27, 25, 1, 0, 0, 0, 27, 28, 1, 0, 0, 0, 28, 30, 1, 0, 0, 0, 29, 27, 1, 0, 0, 0, 30, 31, 5, 1, 0, 0, 31, 32, 3, 2, 1, 0, 32, 33, 5, 2, 0, 0, 33, 40, 1, 0, 0, 0, 34, 35, 3, 6, 3, 0, 35, 36, 5, 3, 0, 0, 36, 37, 3, 4, 2, 0, 37, 38, 5, 4, 0, 0, 38, 40, 1, 0, 0, 0, 39, 10, 1, 0, 0, 0, 39, 15, 1, 0, 0, 0, 39, 22, 1, 0, 0, 0, 39, 34, 1, 0, 0, 0, 40, 53, 1, 0, 0, 0, 41, 42, 10, 2, 0, 0, 42, 43, 5, 20, 0, 0, 43, 44, 5, 18, 0, 0, 44, 45, 5, 20, 0, 0, 45, 52, 3, 2, 1, 3, 46, 47, 10, 1, 0, 0, 47, 48, 5, 20, 0, 0, 48, 49, 5, 19, 0, 0, 49, 50, 5, 20, 0, 0, 50, 52, 3, 2, 1, 2, 51, 41, 1, 0, 0, 0, 51, 46, 1, 0, 0, 0, 52, 55, 1, 0, 0, 0, 53, 51, 1, 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 3, 1, 0, 0, 0, 55, 53, 1, 0, 0, 0, 56, 57, 6, 2, -1, 0, 57, 58, 3, 6, 3, 0, 58, 59, 5, 20, 0, 0, 59, 60, 5, 12, 0, 0, 60, 81, 1, 0, 0, 0, 61, 62, 3, 6, 3, 0, 62, 63, 5, 20, 0, 0, 63, 64, 5, 6, 0, 0, 64, 65, 5, 20, 0, 0, 65, 66, 5, 24, 0, 0, 66, 81, 1, 0, 0, 0, 67, 69, 5, 17, 0, 0, 68, 67, 1, 0, 0, 0, 68, 69, 1, 0, 0, 0, 69, 73, 1, 0, 0, 0, 70, 72, 5, 20, 0, 0, 71, 70, 1, 0, 0, 0, 72, 75, 1, 0, 0, 0, 73, 71, 1, 0, 0, 0, 73, 74, 1, 0, 0, 0, 74, 76, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 77, 5, 1, 0, 0, 77, 78, 3, 4, 2, 0, 78, 79, 5, 2, 0, 0, 79, 81, 1, 0, 0, 0, 80, 56, 1, 0, 0, 0, 80, 61, 1, 0, 0, 0, 80, 68, 1, 0, 0, 0, 81, 94, 1, 0, 0, 0, 82, 83, 10, 2, 0, 0, 83, 84, 5, 20, 0, 0, 84, 85, 5, 18, 0, 0, 85, 86, 5, 20, 0, 0, 86, 93, 3, 4, 2, 3, 87, 88, 10, 1, 0, 0, 88, 89, 5, 20, 0, 0, 89, 90, 5, 19, 0, 0, 90, 91, 5, 20, 0, 0, 91, 93, 3, 4, 2, 2, 92, 82, 1, 0, 0, 0, 92, 87, 1, 0, 0, 0, 93, 96, 1, 0, 0, 0, 94, 92, 1, 0, 0, 0, 94, 95, 1, 0, 0, 0, 95, 5, 1, 0, 0, 0, 96, 94, 1, 0, 0, 0, 97, 99, 5, 21, 0, 0, 98, 97, 1, 0, 0, 0, 98, 99, 1, 0, 0, 0, 99, 100, 1, 0, 0, 0, 100, 103, 5, 22, 0, 0, 101, 102, 5, 5, 0, 0, 102, 104, 5, 22, 0, 0, 103, 101, 1, 0, 0, 0, 103, 104, 1, 0, 0, 0, 104, 7, 1, 0, 0, 0, 12, 22, 27, 39, 51, 53, 68, 73, 80, 92, 94, 98, 103]
|
|
@ -0,0 +1,32 @@
|
|||
T__0=1
|
||||
T__1=2
|
||||
T__2=3
|
||||
T__3=4
|
||||
T__4=5
|
||||
COMPAREOPERATOR=6
|
||||
EQ=7
|
||||
NE=8
|
||||
CO=9
|
||||
SW=10
|
||||
EW=11
|
||||
PR=12
|
||||
GT=13
|
||||
GE=14
|
||||
LT=15
|
||||
LE=16
|
||||
NOT=17
|
||||
AND=18
|
||||
OR=19
|
||||
SP=20
|
||||
SCHEMA=21
|
||||
ATTRNAME=22
|
||||
ESCAPED_QUOTE=23
|
||||
VALUE=24
|
||||
EXCLUDE=25
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
']'=4
|
||||
'.'=5
|
||||
' '=20
|
||||
'\\"'=23
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
|||
T__0=1
|
||||
T__1=2
|
||||
T__2=3
|
||||
T__3=4
|
||||
T__4=5
|
||||
COMPAREOPERATOR=6
|
||||
EQ=7
|
||||
NE=8
|
||||
CO=9
|
||||
SW=10
|
||||
EW=11
|
||||
PR=12
|
||||
GT=13
|
||||
GE=14
|
||||
LT=15
|
||||
LE=16
|
||||
NOT=17
|
||||
AND=18
|
||||
OR=19
|
||||
SP=20
|
||||
SCHEMA=21
|
||||
ATTRNAME=22
|
||||
ESCAPED_QUOTE=23
|
||||
VALUE=24
|
||||
EXCLUDE=25
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
']'=4
|
||||
'.'=5
|
||||
' '=20
|
||||
'\\"'=23
|
|
@ -0,0 +1,118 @@
|
|||
# pylint: skip-file
|
||||
# Generated from ScimFilter.g4 by ANTLR 4.10.1
|
||||
from antlr4 import *
|
||||
|
||||
if __name__ is not None and "." in __name__:
|
||||
from .ScimFilterParser import ScimFilterParser
|
||||
else:
|
||||
from ScimFilterParser import ScimFilterParser
|
||||
|
||||
# This class defines a complete listener for a parse tree produced by ScimFilterParser.
|
||||
class ScimFilterListener(ParseTreeListener):
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#parse.
|
||||
def enterParse(self, ctx: ScimFilterParser.ParseContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#parse.
|
||||
def exitParse(self, ctx: ScimFilterParser.ParseContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#andExp.
|
||||
def enterAndExp(self, ctx: ScimFilterParser.AndExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#andExp.
|
||||
def exitAndExp(self, ctx: ScimFilterParser.AndExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#valPathExp.
|
||||
def enterValPathExp(self, ctx: ScimFilterParser.ValPathExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#valPathExp.
|
||||
def exitValPathExp(self, ctx: ScimFilterParser.ValPathExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#presentExp.
|
||||
def enterPresentExp(self, ctx: ScimFilterParser.PresentExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#presentExp.
|
||||
def exitPresentExp(self, ctx: ScimFilterParser.PresentExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#operatorExp.
|
||||
def enterOperatorExp(self, ctx: ScimFilterParser.OperatorExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#operatorExp.
|
||||
def exitOperatorExp(self, ctx: ScimFilterParser.OperatorExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#braceExp.
|
||||
def enterBraceExp(self, ctx: ScimFilterParser.BraceExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#braceExp.
|
||||
def exitBraceExp(self, ctx: ScimFilterParser.BraceExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#orExp.
|
||||
def enterOrExp(self, ctx: ScimFilterParser.OrExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#orExp.
|
||||
def exitOrExp(self, ctx: ScimFilterParser.OrExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#valPathOperatorExp.
|
||||
def enterValPathOperatorExp(self, ctx: ScimFilterParser.ValPathOperatorExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#valPathOperatorExp.
|
||||
def exitValPathOperatorExp(self, ctx: ScimFilterParser.ValPathOperatorExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#valPathPresentExp.
|
||||
def enterValPathPresentExp(self, ctx: ScimFilterParser.ValPathPresentExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#valPathPresentExp.
|
||||
def exitValPathPresentExp(self, ctx: ScimFilterParser.ValPathPresentExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#valPathAndExp.
|
||||
def enterValPathAndExp(self, ctx: ScimFilterParser.ValPathAndExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#valPathAndExp.
|
||||
def exitValPathAndExp(self, ctx: ScimFilterParser.ValPathAndExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#valPathOrExp.
|
||||
def enterValPathOrExp(self, ctx: ScimFilterParser.ValPathOrExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#valPathOrExp.
|
||||
def exitValPathOrExp(self, ctx: ScimFilterParser.ValPathOrExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#valPathBraceExp.
|
||||
def enterValPathBraceExp(self, ctx: ScimFilterParser.ValPathBraceExpContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#valPathBraceExp.
|
||||
def exitValPathBraceExp(self, ctx: ScimFilterParser.ValPathBraceExpContext):
|
||||
pass
|
||||
|
||||
# Enter a parse tree produced by ScimFilterParser#attrPath.
|
||||
def enterAttrPath(self, ctx: ScimFilterParser.AttrPathContext):
|
||||
pass
|
||||
|
||||
# Exit a parse tree produced by ScimFilterParser#attrPath.
|
||||
def exitAttrPath(self, ctx: ScimFilterParser.AttrPathContext):
|
||||
pass
|
||||
|
||||
|
||||
del ScimFilterParser
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,101 @@
|
|||
"""Django listener"""
|
||||
from django.db.models import Q
|
||||
from django.utils.tree import Node
|
||||
|
||||
from authentik.sources.scim.filters.ScimFilterListener import ScimFilterListener
|
||||
from authentik.sources.scim.filters.ScimFilterParser import ScimFilterParser
|
||||
|
||||
|
||||
class DjangoQueryListener(ScimFilterListener):
|
||||
"""SCIM filter listener that converts it to a query"""
|
||||
|
||||
_query: Node
|
||||
_last_node: Node
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._query = Q()
|
||||
self._last_node = Q()
|
||||
|
||||
@property
|
||||
def query(self) -> Node:
|
||||
return self._query
|
||||
|
||||
def enterParse(self, ctx: ScimFilterParser.ParseContext):
|
||||
print("enterParse", ctx)
|
||||
|
||||
def exitParse(self, ctx: ScimFilterParser.ParseContext):
|
||||
print("exitParse", ctx)
|
||||
|
||||
def enterAndExp(self, ctx: ScimFilterParser.AndExpContext):
|
||||
print("enterAndExp", ctx)
|
||||
|
||||
def exitAndExp(self, ctx: ScimFilterParser.AndExpContext):
|
||||
print("exitAndExp", ctx)
|
||||
|
||||
def enterValPathExp(self, ctx: ScimFilterParser.ValPathExpContext):
|
||||
print("enterValPathExp", ctx.getText())
|
||||
|
||||
def exitValPathExp(self, ctx: ScimFilterParser.ValPathExpContext):
|
||||
print("exitValPathExp", ctx)
|
||||
|
||||
def enterPresentExp(self, ctx: ScimFilterParser.PresentExpContext):
|
||||
print("enterPresentExp", ctx)
|
||||
|
||||
def exitPresentExp(self, ctx: ScimFilterParser.PresentExpContext):
|
||||
print("exitPresentExp", ctx)
|
||||
|
||||
def enterOperatorExp(self, ctx: ScimFilterParser.OperatorExpContext):
|
||||
print("enterOperatorExp", ctx)
|
||||
|
||||
def exitOperatorExp(self, ctx: ScimFilterParser.OperatorExpContext):
|
||||
print("exitOperatorExp", ctx)
|
||||
|
||||
def enterBraceExp(self, ctx: ScimFilterParser.BraceExpContext):
|
||||
print("enterBraceExp", ctx)
|
||||
|
||||
def exitBraceExp(self, ctx: ScimFilterParser.BraceExpContext):
|
||||
print("exitBraceExp", ctx)
|
||||
|
||||
def enterOrExp(self, ctx: ScimFilterParser.OrExpContext):
|
||||
print("enterOrExp", ctx)
|
||||
|
||||
def exitOrExp(self, ctx: ScimFilterParser.OrExpContext):
|
||||
print("exitOrExp", ctx)
|
||||
|
||||
def enterValPathOperatorExp(self, ctx: ScimFilterParser.ValPathOperatorExpContext):
|
||||
print("enterValPathOperatorExp", ctx)
|
||||
|
||||
def exitValPathOperatorExp(self, ctx: ScimFilterParser.ValPathOperatorExpContext):
|
||||
print("exitValPathOperatorExp", ctx)
|
||||
|
||||
def enterValPathPresentExp(self, ctx: ScimFilterParser.ValPathPresentExpContext):
|
||||
print("enterValPathPresentExp", ctx)
|
||||
|
||||
def exitValPathPresentExp(self, ctx: ScimFilterParser.ValPathPresentExpContext):
|
||||
print("exitValPathPresentExp", ctx)
|
||||
|
||||
def enterValPathAndExp(self, ctx: ScimFilterParser.ValPathAndExpContext):
|
||||
print("enterValPathAndExp", ctx.getText())
|
||||
|
||||
def exitValPathAndExp(self, ctx: ScimFilterParser.ValPathAndExpContext):
|
||||
print("exitValPathAndExp", ctx)
|
||||
|
||||
def enterValPathOrExp(self, ctx: ScimFilterParser.ValPathOrExpContext):
|
||||
print("enterValPathOrExp", ctx)
|
||||
|
||||
def exitValPathOrExp(self, ctx: ScimFilterParser.ValPathOrExpContext):
|
||||
print("exitValPathOrExp", ctx)
|
||||
|
||||
def enterValPathBraceExp(self, ctx: ScimFilterParser.ValPathBraceExpContext):
|
||||
print("enterValPathBraceExp", ctx)
|
||||
|
||||
def exitValPathBraceExp(self, ctx: ScimFilterParser.ValPathBraceExpContext):
|
||||
print("exitValPathBraceExp", ctx)
|
||||
|
||||
def enterAttrPath(self, ctx: ScimFilterParser.AttrPathContext):
|
||||
self._last_node = Q(ctx.getText())
|
||||
|
||||
def exitAttrPath(self, ctx: ScimFilterParser.AttrPathContext):
|
||||
self._query = self._last_node
|
||||
self._last_node = Q()
|
|
@ -14,7 +14,7 @@ class SCIMTokenAuth(BaseAuthentication):
|
|||
|
||||
def legacy(self, key: str, source_slug: str) -> Optional[Token]: # pragma: no cover
|
||||
"""Legacy HTTP-Basic auth for testing"""
|
||||
if not settings.TEST or not settings.DEBUG:
|
||||
if not settings.TEST and not settings.DEBUG:
|
||||
return None
|
||||
_username, _, password = b64decode(key.encode()).decode().partition(":")
|
||||
token = self.check_token(password, source_slug)
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
"""SCIM Utils"""
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from antlr4 import CommonTokenStream, InputStream, ParseTreeWalker
|
||||
from django.urls import resolve
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
@ -6,6 +11,10 @@ from rest_framework.request import Request
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.sources.scim.filters.django import DjangoQueryListener
|
||||
from authentik.sources.scim.filters.ScimFilterLexer import ScimFilterLexer
|
||||
from authentik.sources.scim.filters.ScimFilterParser import ScimFilterParser
|
||||
from authentik.sources.scim.views.v2.auth import SCIMTokenAuth
|
||||
|
||||
SCIM_CONTENT_TYPE = "application/scim+json"
|
||||
|
@ -31,6 +40,39 @@ class SCIMView(APIView):
|
|||
parser_classes = [SCIMParser]
|
||||
renderer_classes = [SCIMRenderer]
|
||||
|
||||
def patch_resolve_value(self, raw_value: dict) -> Optional[User | Group]:
|
||||
"""Attempt to resolve a raw `value` attribute of a patch operation into
|
||||
a database model"""
|
||||
model = User
|
||||
query = {}
|
||||
if "$ref" in raw_value:
|
||||
url = urlparse(raw_value["$ref"])
|
||||
if match := resolve(url.path):
|
||||
if match.url_name == "v2-users":
|
||||
model = User
|
||||
query = {"pk": int(match.kwargs["user_id"])}
|
||||
elif "type" in raw_value:
|
||||
match raw_value["tyoe"]:
|
||||
case "User":
|
||||
model = User
|
||||
query = {"pk": int(raw_value["value"])}
|
||||
case "Group":
|
||||
model = Group
|
||||
else:
|
||||
return None
|
||||
return model.objects.filter(**query).first()
|
||||
|
||||
def patch_parse_path(self, path: str):
|
||||
"""Parse the path of a Patch Operation"""
|
||||
lexer = ScimFilterLexer(InputStream(path))
|
||||
stream = CommonTokenStream(lexer)
|
||||
parser = ScimFilterParser(stream)
|
||||
tree = parser.filter_()
|
||||
listener = DjangoQueryListener()
|
||||
walker = ParseTreeWalker()
|
||||
walker.walk(listener, tree)
|
||||
return listener.query
|
||||
|
||||
|
||||
class SCIMRootView(SCIMView):
|
||||
"""Root SCIM View"""
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from typing import Optional
|
||||
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.transaction import atomic
|
||||
from django.http import Http404, QueryDict
|
||||
from django.urls import reverse
|
||||
from rest_framework.request import Request
|
||||
|
@ -9,6 +10,7 @@ from rest_framework.response import Response
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.sources.scim.errors import PatchError
|
||||
from authentik.sources.scim.views.v2.base import SCIM_CONTENT_TYPE, SCIMView
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
@ -77,7 +79,28 @@ class GroupsView(SCIMView):
|
|||
|
||||
def patch(self, request: Request, group_id: str, **kwargs) -> Response:
|
||||
"""Update group handler"""
|
||||
return self.put(request, group_id, **kwargs)
|
||||
group: Optional[Group] = Group.objects.filter(pk=group_id).first()
|
||||
if not group:
|
||||
raise Http404
|
||||
if request.data.get("schemas", []) != ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]:
|
||||
return Response(status=400)
|
||||
try:
|
||||
with atomic():
|
||||
for op in request.data.get("Operations", []):
|
||||
path = self.patch_parse_path(op["path"])
|
||||
operation = op["op"]
|
||||
raw_value = op.get("value", None)
|
||||
values = []
|
||||
for value in raw_value:
|
||||
values.append(self.patch_resolve_value(value))
|
||||
match operation:
|
||||
case "add":
|
||||
group.users.add(*[x.pk for x in values])
|
||||
case "remove":
|
||||
pass
|
||||
return Response(self.group_to_scim(group), status=200)
|
||||
except (KeyError, PatchError):
|
||||
return Response(status=400)
|
||||
|
||||
def put(self, request: Request, group_id: str, **kwargs) -> Response:
|
||||
"""Update group handler"""
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
|
@ -161,6 +161,17 @@ files = [
|
|||
{file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "antlr4-python3-runtime"
|
||||
version = "4.13.1"
|
||||
description = "ANTLR 4.13.1 runtime for Python 3"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"},
|
||||
{file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.0.0"
|
||||
|
@ -2096,16 +2107,6 @@ files = [
|
|||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||
|
@ -3095,7 +3096,6 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||
|
@ -3103,15 +3103,8 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||
|
@ -3128,7 +3121,6 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||
|
@ -3136,7 +3128,6 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||
|
@ -4313,4 +4304,4 @@ files = [
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "2fc746976187f4674f04575cffd6a367744723bf78c356b6951c2370bc47ceae"
|
||||
content-hash = "a41a75fc3dac5e552ed99670c10a5a5a0947449701893fafd81d59c6439f324a"
|
||||
|
|
|
@ -118,6 +118,7 @@ description = ""
|
|||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
antlr4-python3-runtime = "*"
|
||||
argon2-cffi = "*"
|
||||
celery = "*"
|
||||
channels = { version = "*", extras = ["daphne"] }
|
||||
|
@ -143,6 +144,7 @@ facebook-sdk = "*"
|
|||
flower = "*"
|
||||
geoip2 = "*"
|
||||
gunicorn = "*"
|
||||
jsonpatch = "*"
|
||||
kubernetes = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
|
@ -171,7 +173,6 @@ webauthn = "*"
|
|||
wsproto = "*"
|
||||
xmlsec = "*"
|
||||
zxcvbn = "*"
|
||||
jsonpatch = "*"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
bandit = "*"
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from antlr4 import CommonTokenStream, InputStream, ParseTreeWalker
|
||||
from authentik.sources.scim.filters.django import DjangoQueryListener
|
||||
from authentik.sources.scim.filters.ScimFilterLexer import ScimFilterLexer
|
||||
from authentik.sources.scim.filters.ScimFilterParser import ScimFilterParser
|
||||
|
||||
|
||||
lexer = ScimFilterLexer(InputStream('emails[type eq "work" and value ew "example.com"]'))
|
||||
stream = CommonTokenStream(lexer)
|
||||
parser = ScimFilterParser(stream)
|
||||
tree = parser.filter_()
|
||||
listener = DjangoQueryListener()
|
||||
walker = ParseTreeWalker()
|
||||
walker.walk(listener, tree)
|
||||
print(listener.query)
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: SAML
|
||||
title: SAML Source
|
||||
---
|
||||
|
||||
This source allows authentik to act as a SAML Service Provider. Just like the SAML Provider, it supports signed requests. Vendor-specific documentation can be found in the Integrations Section.
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
title: SCIM
|
||||
---
|
||||
|
||||
The SCIM source allows other applications to directly create users and groups within authentik. SCIM is supported by applications such as Microsoft Azure AD, Google Workspace and Okta.
|
||||
|
||||
The base SCIM URL is in the format of `https://authentik.company/source/scim/<source-slug>/v2`. Authentication is done via Bearer tokens generated by authentik. When an SCIM source is created, a service account is created and a matching token.
|
||||
|
||||
## Supported Options & Resource types
|
||||
|
||||
### `/v2/Users`
|
||||
|
||||
Endpoint to list, create, patch and delete users.
|
||||
|
||||
### `/v2/Groups`
|
||||
|
||||
Endpoint to list, create, patch and delete groups.
|
||||
|
||||
There is also the `/v2/ServiceProviderConfig` and `/v2/ResourceTypes`, which is used by SCIM-enabled applications to find out which features authentik supports.
|
|
@ -165,6 +165,29 @@ const docsSidebar = {
|
|||
"flow/executors/headless",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Stages",
|
||||
items: [
|
||||
"flow/stages/authenticator_duo/index",
|
||||
"flow/stages/authenticator_sms/index",
|
||||
"flow/stages/authenticator_static/index",
|
||||
"flow/stages/authenticator_totp/index",
|
||||
"flow/stages/authenticator_validate/index",
|
||||
"flow/stages/authenticator_webauthn/index",
|
||||
"flow/stages/captcha/index",
|
||||
"flow/stages/deny",
|
||||
"flow/stages/email/index",
|
||||
"flow/stages/identification/index",
|
||||
"flow/stages/invitation/index",
|
||||
"flow/stages/password/index",
|
||||
"flow/stages/prompt/index",
|
||||
"flow/stages/user_delete",
|
||||
"flow/stages/user_login",
|
||||
"flow/stages/user_logout",
|
||||
"flow/stages/user_write",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -166,6 +166,7 @@ module.exports = {
|
|||
"sources/ldap/index",
|
||||
"sources/oauth/index",
|
||||
"sources/saml/index",
|
||||
"sources/scim/index",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
Reference in New Issue