api.citadel.citadel-api.3.41.2.source-code.citadel-rules.gdsl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of citadel-api Show documentation
Show all versions of citadel-api Show documentation
This artifact provides the API classes that are necessary to implement general configuration Rules on the Memority IM platform.
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.tree.TokenSet
import com.intellij.testFramework.LightVirtualFile
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod
import java.util.regex.Matcher
import static com.intellij.openapi.util.text.StringUtil.unquoteString
/**
* Citadel rule DSL definition, for automagic autocompletion in "modern" IDEs (aka. IntelliJ).
* Usage:
*
* - Set the
toolkit.rule.flavor
maven property to toolkit,citadel
* - OR start your groovy script with
//@ruleFlavor:toolkit,citadel
*
*/
class Constants {
static final String SEARCH_EXPRESSION_DSL_METHOD = "expr"
static final String SEARCH_EXPRESSION_HAS_ROLE_MATCHING_METHOD = "hasRoleMatching"
static final String BUILTIN_ATTRIBUTE_IDS = "com.memority.citadel.shared.api.im.BuiltinAttributeIds"
static final String META_ATTRIBUTE_IDS = "com.memority.citadel.shared.api.im.MetaAttributeIds"
static final String CUSTOM_ATTRIBUTE_IDS = "com.memority.toolkit.rule.groovyHints.citadel.CustomAttributeIds"
static final String CITADEL_API_TYPE_ANNOTATION = "com.memority.citadel.shared.api.im.CitadelApiType"
static final String CITADEL_SEARCH_CRITERION_ATTRIBUTE_ANNOTATION = "com.memority.citadel.shared.api.im.CitadelSearchCriterionAttribute"
static final ROLE_ASSIGNMENT_NON_SEARCHABLE_ATTRIBUTES = ["identity", "metadata", "roleBinding"] as Set
static final String ID_PATTERN_STR = $/[a-zA-Z]|[a-zA-Z0-9_]+[a-zA-Z0-9\-_]*[a-zA-Z0-9]/$
static final String CUSTOM_ATTRIBUTE_MAGIC_COMMENT = $///\s*@customAttribute\s*:\s*(${ID_PATTERN_STR})\s*=\s*(.*)\s*/$
static final String CUSTOM_SEARCHABLE_ATTRIBUTE_MAGIC_COMMENT = $///\s*@customSearchableAttribute\s*:\s*(${ID_PATTERN_STR})\s*=\s*(.*)\s*/$
}
/**
* Contexts
*/
//noinspection GroovyAssignabilityCheck
contribute(context(scope: scriptScope())) {
if (!ruleFlavor(place, "citadel")) {
return
}
//Contexts
property(name: "CTXT",
type: "com.memority.citadel.shared.api.context.CitadelContext",
doc: 'The parent context made available to all Citadel rules.')
property(name: "OPERATION",
type: "com.memority.citadel.shared.api.context.OperationContext",
doc: '''
This context provides information about an operation performed on a Managed Object. It holds
all of the current object apiObject, potentially being worked on, as well as the original
apiObject, as existing in the IDM repository (if available).
''')
property(name: "PRIMARY_OPERATION",
type: "com.memority.citadel.shared.api.context.OperationContext",
doc: '''
This context provides information about the primary operation of an Object or Business Policy when handling the
secondary Scope.
''')
property(name: "OBJECT",
type: "com.memority.citadel.shared.api.im.ApiObject",
doc: 'The IM object being worked on.')
property(name: "ATTRIBUTE",
type: "com.memority.citadel.shared.api.context.AttributeContext",
doc: '''
The context of an individual attribute, when running while processing a single attribute. Note that
this attribute may be processed in the context of an IM object, in which case an accompanying OPERATION
context is also available.
Note: this context is immutable.
''')
property(name: "FEATURE",
type: "com.memority.citadel.shared.api.context.FeatureContext",
doc: 'Context information about the Feature being executed.')
property(name: "EXTERNAL",
type: "com.memority.citadel.shared.api.context.ExternalContext",
doc: 'Additional information sent by the API caller, that will be returned as is')
property(name: "SUBJECT",
type: "com.memority.citadel.shared.api.im.ApiObject",
doc: 'The authenticated principal, if any.')
property(name: "REQUESTER", type: "com.memority.citadel.shared.api.im.ApiObject",
doc: 'The current operation\'s requester')
property(name: "WORKFLOW",
type: "com.memority.citadel.shared.api.context.WorkflowContext",
doc: 'Context information about the Workflow being executed.')
property(name: "DIMENSION",
type: "com.memority.citadel.shared.api.context.DimensionContext",
doc: 'Context information for Rule-based right/role dimension mapping')
property(name: "ROLE_REQUEST",
type: "com.memority.citadel.shared.api.context.RoleRequestContext",
doc: 'Context information for Rule-based Role Workflow Strategy')
property(name: "ROLE_ASSIGNMENT",
type: "com.memority.citadel.shared.api.context.RoleAssignmentOperationContext",
doc: 'Context information about the role assignment operation')
//APIs
property(name: "FIND",
type: "com.memority.citadel.shared.api.services.finder.ObjectFinderProvider",
doc: 'Provides an {@link ObjectFinder} for each Object Kind')
property(name: "SEQ",
type: "com.memority.citadel.shared.api.services.seq.SequenceProvider",
doc: 'Provides access to the sequences that are managed by the IDM.')
property(name: "REF",
type: "com.memority.citadel.shared.api.services.reftable.ReferenceTableDataFinderProvider",
doc: '''
Provides access access to a given ReferenceTable's rows, using simple semantics.
It is the main entry point to reference table data through the Citadel API.
''')
property(name: "NOTIFY",
type: "com.memority.citadel.shared.api.services.notification.NotificationService",
doc: '''
Provides the ability to send notifications using the Citadel notification facility.
Usage:
notificationService.create()
.withType("some-notification-type)
.withNotifications(new HashSet<>(Arrays.asList("notif1", "notif2"))
.withActor("jdoe")
.role("some-role")
.name("John Doe")
.email("[email protected]")
.speaking("fr")
.end()
.withPayload(new HashMap
''')
property(name: "MANAGE",
type: "com.memority.citadel.shared.api.services.manager.ObjectManager",
doc: 'Service to instantiate, save, patch and and delete managed objects.')
property(name: "ACCESS_CODE",
type: "com.memority.citadel.shared.api.services.accesscode.ApiAccessCodeManager",
doc: 'This service allows to handle the access code lifecycle (use an access code, handle its expiration...)')
// TODO CTD-9248 remove
property(name: "API_INWEBO",
type: "com.memority.citadel.shared.api.services.inwebo.ApiInWeboManager",
doc: 'This service allows to manage InWebo accounts (delete, unlock, send enrollment link...)')
property(name: "API_MYMFA",
type: "com.memority.citadel.shared.api.services.myMFA.ApiMyMFAManager",
doc: 'This service allows to manage MyMFA accounts (delete, unlock, send enrollment link...)')
property(name: "API_FEATURE",
type: "com.memority.citadel.shared.api.services.feature.ApiFeatureManager",
doc: 'This service allows to execute features')
property(name: "API_WORKFLOW",
type: "com.memority.citadel.shared.api.services.workflow.ApiWorkflow",
doc: 'This service allows to manage workflow execution')
property(name: "CHANGES",
type: "com.memority.citadel.shared.api.services.attributes.OperationChanges",
doc: 'Provides a {@link OperationChanges}')
property(name: "API_REPORTING",
type: "com.memority.citadel.shared.api.services.reporting.ApiReportingManager",
doc: 'This service allows to execute operations on reporting collections')
property(name: "REPORTING_DOCUMENT",
type: "com.memority.citadel.shared.api.services.reporting.ApiReportingDocument",
doc: 'Holds the current reporting document in some contexts such as display conditions')
property(name: "API_RECERTIFICATION",
type: "com.memority.citadel.shared.api.services.recertification.ApiRecertification",
doc: 'The Role Assignment Recertification API')
property(name: "API_RECERTIFICATION_CAMPAIGN",
type: "com.memority.citadel.shared.api.services.recertification.ApiRecertificationCampaign",
doc: 'The Role Assignment Recertification Campaign API')
property(name: "API_OBJECT_RECERTIFICATION",
type: "com.memority.citadel.shared.api.services.recertification.ApiObjectRecertification",
doc: 'The Object Recertification API')
property(name: "API_OBJECT_RECERTIFICATION_CAMPAIGN",
type: "com.memority.citadel.shared.api.services.recertification.ApiObjectRecertificationCampaign",
doc: 'The Object Recertification Campaign API')
property(name: "API_OTP",
type: "com.memority.citadel.shared.api.services.otp.ApiOtpManager",
doc: 'Synchronize OTP modules registrations')
property(name: "API_ROLE_ASSIGNMENT",
type: "com.memority.citadel.shared.api.services.roleAssignment.ApiRoleAssignmentManager",
doc: 'The Role Assignment API (Request/Update/Revoke Role Assignments)')
}
/**
* Built in and meta attributes typing
*/
//noinspection GroovyAssignabilityCheck
contribute(context(ctype: 'com.memority.citadel.shared.api.im.ApiObject')) {
if (!(ruleFlavor(place, "citadel") || ruleFlavor(place, "domino"))) {
return
}
findAttributes(findClass(Constants.META_ATTRIBUTE_IDS))
.each { property(name: it.name, type: it.groovyType, doc: "Meta attribute") }
findAttributes(findClass(Constants.BUILTIN_ATTRIBUTE_IDS))
.each { property(name: it.name, type: it.groovyType, doc: "Builtin attribute") }
findCustomAttributes(place, findClass(Constants.CUSTOM_ATTRIBUTE_IDS))
.each { property(name: it.name, type: it.groovyType, doc: "Custom attribute") }
}
/**
* Search expression DSL: builtin and meta attributes completion and hasRoleMatching support
*/
//noinspection GroovyAssignabilityCheck
contribute(context(scope: closureScope(isArg: true))) {
if (!(ruleFlavor(place, "toolkit") && (ruleFlavor(place, "citadel") || ruleFlavor(place, "domino")))) {
return
}
def call = enclosingCall(Constants.SEARCH_EXPRESSION_DSL_METHOD) as GrMethodCallExpression
def callBind = call?.bind()
if (!(callBind instanceof GrMethod && !callBind?.containingClass)) {
return
}
if (!(call.children.size() == 3 && call.children[2] instanceof GrClosableBlock)) {
return
}
method(name: Constants.SEARCH_EXPRESSION_HAS_ROLE_MATCHING_METHOD,
type: "com.memority.toolkit.core.api.search.expression.FunctionExpression",
params: [expression: "com.memority.toolkit.core.api.search.expression.SearchExpression"],
doc: '''
Search expression DSL function to match identities having a role assignment matching a given search expression.
Example:
expr {firstName.eq('John') & hasRoleMatching(role.eq('admin') & role.type.eq('appRole') & dimensions.contains('{"answer": 42}'))}
''')
// Would have loved to have different completions based on the enclosing function call
// Unfortunately, IntelliJ doesn't handle this and conditions on `place` is not epileptic-friendly
// -> contribute all builtin, custom and meta attributes as well as RoleAssignment fields for hasRoleMatching
def allAttributes = [] as Set
allAttributes.addAll(
findClass("com.memority.citadel.shared.api.im.identity.RoleAssignment")
.allFields
.findAll { !Constants.ROLE_ASSIGNMENT_NON_SEARCHABLE_ATTRIBUTES.contains(it.name) }
.collect { it.name }
)
//ease completion with all builtin/meta attribute ids
allAttributes.addAll(
findAttributes(findClass(Constants.META_ATTRIBUTE_IDS))
.findAll { it.searchCriterion }
.collect { it.name }
)
allAttributes.addAll(
findAttributes(findClass(Constants.BUILTIN_ATTRIBUTE_IDS))
.findAll { it.searchCriterion }
.collect { it.name }
)
allAttributes.addAll(
findCustomAttributes(place, findClass(Constants.CUSTOM_ATTRIBUTE_IDS))
.findAll { it.searchCriterion }
.collect { it.name }
)
allAttributes
.sort()
.each {
property(name: it,
type: "com.memority.toolkit.core.api.search.expression.SearchExpressions.OperatorExpressionBuilder",
doc: 'Search expression prop \'' + it + '\'.
' +
'
' +
'Equivalent to prop("' + it + '")
.')
}
}
class Attribute {
final String name
final String groovyType
final boolean searchCriterion
Attribute(String name, String groovyType, boolean searchCriterion) {
this.name = name
this.groovyType = groovyType
this.searchCriterion = searchCriterion
}
}
static List findAttributes(PsiClass attributeIdsClass) {
attributeIdsClass.allFields
.findAll { it.initializer instanceof PsiLiteralExpression }
.findAll { it.getAnnotation(Constants.CITADEL_API_TYPE_ANNOTATION) } //not annotated, do not advertise
.collect {
def apiTypeAnnot = it.getAnnotation(Constants.CITADEL_API_TYPE_ANNOTATION)
def searchCriterionAnnot = it.getAnnotation(Constants.CITADEL_SEARCH_CRITERION_ATTRIBUTE_ANNOTATION)
def type = unquoteString(apiTypeAnnot.findAttributeValue("value").text)
def attributeName = unquoteString(it.initializer.text)
return new Attribute(attributeName, type, searchCriterionAnnot != null)
}
}
/**
* Finds the custom object attributes using the following strategies:
*
* - Adds all fields of the
com.memority.citadelTenant.groovyHints.ApiObjectHints
class
* //@customAttribute: attributeName = type
magic comments
* //@customSearchableAttribute: attributeName = type
magic comments
*
* Magic comments take precedence on ApiObjectHints.
*
* @param place the place to look at (typically the place
member available in contribute
closures
* @param attributeIdsClass the ApiObjectHints class (because `findClass is only available in `contribute` closure)
* @return a name -> type
map
*/
static List findCustomAttributes(PsiElement place, PsiClass attributeIdsClass) {
def result = [] as List
if (attributeIdsClass) {
result.addAll(findAttributes(attributeIdsClass))
}
result.addAll(
findMagicComments(place, Constants.CUSTOM_ATTRIBUTE_MAGIC_COMMENT)
.collect { new Attribute(it.group(1), it.group(2), false) }
)
result.addAll(
findMagicComments(place, Constants.CUSTOM_SEARCHABLE_ATTRIBUTE_MAGIC_COMMENT)
.collect { new Attribute(it.group(1), it.group(2), true) }
)
return result
}
/**
* Look for "magic" comments at the beginning of the script matching a given regex
* @param place the place to look at (typically the place
member available in contribute
closures
* @param re the regex to match
* @return a list of matchers for each matching comment
*/
static List findMagicComments(PsiElement place, String re) {
if (!place.containingFile.node) {
return []
}
def result = [] as List
loop:
for (def ast : place.containingFile.node.getChildren(TokenSet.ANY)) {
sw:
//noinspection GroovyFallthrough
switch (ast.elementType.toString()) {
case "new line":
case "WHITE_SPACE":
case "PACKAGE_DEFINITION":
case "IMPORT":
continue loop
case "line comment":
def magicComment = (ast as PsiComment).text
def matcher = magicComment =~ re
if (matcher.matches()) {
result << matcher
}
break sw
default:
break loop
}
}
return result
}
/**
* Checks that the enclosing groovy script has a given "rule flavor". It looks for, in order:
*
* - A
//@ruleFlavor:flavor1,flavor2,...
magic comment
* - The
toolkit.rule.flavor
Maven property
*
* @param place the place to look at (typically the place
member available in contribute
closures
* @param flavor the desired flavor
*/
static boolean ruleFlavor(PsiElement place, String flavor) {
def enabledFlavors = null
//check for magic comment
def magicCommentMatchers = findMagicComments(place, $///\s*@ruleFlavor\s*:\s*([\w,\s]+)\s*/$)
if (magicCommentMatchers) {
enabledFlavors = magicCommentMatchers.last().group(1)
}
//if no magic comment, check maven property
if (!enabledFlavors) {
def project = place.manager.project
def file = place.context.containingFile.originalFile.containingFile.virtualFile
//Groovy fragment: get original file
if (file instanceof LightVirtualFile && file.originalFile) {
file = file.originalFile
}
if (file) {
//noinspection GrUnresolvedAccess
def mavenProject = project
.getServiceByClassName("org.jetbrains.idea.maven.project.MavenProjectsManager")
.findContainingProject(file)
if (mavenProject) {
//noinspection GroovyAssignabilityCheck
enabledFlavors = (mavenProject.properties.getProperty("toolkit.rule.flavor") as String)
}
}
}
return enabledFlavors && enabledFlavors.split(",")
.collect { it.trim() }
.contains(flavor)
}