permissionClasses() {
+ return classSignatureToConstructor.keySet();
+ }
+ };
+ }
+
+ /**
+ * Creates predicate for each secured method. Predicates are cached if possible.
+ * What we call predicate here is combination of (possibly computed) {@link Permission}s joined with
+ * logical operators 'AND' or 'OR'.
+ *
+ * For example, combination of following 2 annotation instances:
+ *
+ *
+ * @PermissionsAllowed({"createResource", "createAll"})
+ * @PermissionsAllowed({"updateResource", "updateAll"})
+ * public void createOrUpdate() {
+ * ...
+ * }
+ *
+ *
+ * leads to (pseudocode): (createResource OR createAll) AND (updateResource OR updateAll)
+ *
+ * @return PermissionSecurityChecksBuilder
+ */
+ PermissionSecurityChecksBuilder createPermissionPredicates() {
+ Map permissionCache = new HashMap<>();
+ for (Map.Entry>> entry : methodToPermissionKeys.entrySet()) {
+ final MethodInfo securedMethod = entry.getKey();
+ final LogicalAndPermissionPredicate predicate = new LogicalAndPermissionPredicate();
+
+ // 'AND' operands
+ for (List permissionKeys : entry.getValue()) {
+ var orPredicate = new LogicalOrPermissionPredicate();
+ predicate.and(orPredicate);
+
+ // 'OR' operands
+ for (PermissionKey permissionKey : permissionKeys) {
+ var permission = createPermission(permissionKey, securedMethod, permissionCache);
+ if (permission.isComputed()) {
+ predicate.markAsComputed();
+ }
+ orPredicate.or(permission);
+ }
+ }
+ methodToPredicate.put(securedMethod, predicate);
+ }
+ return this;
+ }
+
+ PermissionSecurityChecksBuilder validatePermissionClasses(IndexView index) {
+ for (List> keyLists : methodToPermissionKeys.values()) {
+ for (List keyList : keyLists) {
+ for (PermissionKey key : keyList) {
+ if (!classSignatureToConstructor.containsKey(key.classSignature())) {
+
+ // validate permission class
+ final ClassInfo clazz = index.getClassByName(key.clazz.name());
+ Objects.requireNonNull(clazz);
+ if (clazz.constructors().size() != 1) {
+ throw new RuntimeException(
+ String.format("Permission class '%s' has %d constructors, exactly one is allowed",
+ key.classSignature(), clazz.constructors().size()));
+ }
+ var constructor = clazz.constructors().get(0);
+ // first constructor parameter must be permission name
+ if (constructor.parametersCount() == 0 || !STRING.equals(constructor.parameterType(0).name())) {
+ throw new RuntimeException(
+ String.format("Permission constructor '%s' first argument must be '%s'",
+ clazz.name().toString(), String.class.getName()));
+ }
+ // rest of validation needs to be done for computed classes only and per each secured method
+ // therefore we do it later
+
+ // cache validation result
+ classSignatureToConstructor.put(key.classSignature(), constructor);
+ }
+ }
+ }
+ }
+ return this;
+ }
+
+ PermissionSecurityChecksBuilder gatherPermissionsAllowedAnnotations(List instances,
+ Map alreadyCheckedMethods,
+ Map alreadyCheckedClasses) {
+
+ // make sure we process annotations on methods first
+ instances.sort(new Comparator() {
+ @Override
+ public int compare(AnnotationInstance o1, AnnotationInstance o2) {
+ if (o1.target().kind() != o2.target().kind()) {
+ return o1.target().kind() == AnnotationTarget.Kind.METHOD ? -1 : 1;
+ }
+ // variable 'instances' won't be modified
+ return 0;
+ }
+ });
+
+ List cache = new ArrayList<>();
+ Map>> classMethodToPermissionKeys = new HashMap<>();
+ for (AnnotationInstance instance : instances) {
+
+ AnnotationTarget target = instance.target();
+ if (target.kind() == AnnotationTarget.Kind.METHOD) {
+ // method annotation
+ final MethodInfo methodInfo = target.asMethod();
+
+ // we don't allow combining @PermissionsAllowed with other security annotations as @DenyAll, ...
+ if (alreadyCheckedMethods.containsKey(methodInfo)) {
+ throw new IllegalStateException(
+ String.format("Method %s of class %s is annotated with multiple security annotations",
+ methodInfo.name(), methodInfo.declaringClass()));
+ }
+
+ gatherPermissionKeys(instance, methodInfo, cache, methodToPermissionKeys);
+ } else {
+ // class annotation
+
+ // add permissions for the class annotation if respective method haven't already been annotated
+ if (target.kind() == AnnotationTarget.Kind.CLASS) {
+ final ClassInfo clazz = target.asClass();
+
+ // ignore PermissionsAllowedInterceptor in security module
+ // we also need to check string as long as duplicate "PermissionsAllowedInterceptor" exists
+ // in RESTEasy Reactive, however this workaround should be removed when the interceptor is dropped
+ if (PERMISSIONS_ALLOWED_INTERCEPTOR.equals(clazz.name())
+ || clazz.name().toString().endsWith("PermissionsAllowedInterceptor")) {
+ continue;
+ }
+
+ // check that class wasn't annotated with other security annotation
+ final AnnotationInstance existingClassInstance = alreadyCheckedClasses.get(clazz);
+ if (existingClassInstance == null) {
+ for (MethodInfo methodInfo : clazz.methods()) {
+
+ if (!isPublicNonStaticNonConstructor(methodInfo)) {
+ continue;
+ }
+
+ // ignore method annotated with other security annotation
+ boolean noMethodLevelSecurityAnnotation = !alreadyCheckedMethods.containsKey(methodInfo);
+ // ignore method annotated with method-level @PermissionsAllowed
+ boolean noMethodLevelPermissionsAllowed = !methodToPermissionKeys.containsKey(methodInfo);
+ if (noMethodLevelSecurityAnnotation && noMethodLevelPermissionsAllowed) {
+
+ gatherPermissionKeys(instance, methodInfo, cache, classMethodToPermissionKeys);
+ }
+ }
+ } else {
+
+ // we do not allow combining @PermissionsAllowed with other security annotations as @Authenticated
+ throw new IllegalStateException(
+ String.format("Class %s is annotated with multiple security annotations %s and %s", clazz,
+ instance.name(), existingClassInstance.name()));
+ }
+ }
+ }
+ }
+ methodToPermissionKeys.putAll(classMethodToPermissionKeys);
+ return this;
+ }
+
+ private static void gatherPermissionKeys(AnnotationInstance instance, MethodInfo methodInfo, List cache,
+ Map>> methodToPermissionKeys) {
+ // @PermissionsAllowed value is in format permission:action, permission2:action, permission:action2, permission3
+ // here we transform it to permission -> actions
+ final var permissionToActions = new HashMap>();
+ for (String permissionToAction : instance.value().asStringArray()) {
+ if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) {
+
+ // expected format: permission:action
+ final String[] permissionToActionArr = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR);
+ if (permissionToActionArr.length != 2) {
+ throw new RuntimeException(String.format(
+ "PermissionsAllowed value '%s' contains more than one separator '%2$s', expected format is 'permissionName%2$saction'",
+ permissionToAction, PERMISSION_TO_ACTION_SEPARATOR));
+ }
+ final String permissionName = permissionToActionArr[0];
+ final String action = permissionToActionArr[1];
+ if (permissionToActions.containsKey(permissionName)) {
+ permissionToActions.get(permissionName).add(action);
+ } else {
+ final Set actions = new HashSet<>();
+ actions.add(action);
+ permissionToActions.put(permissionName, actions);
+ }
+ } else {
+
+ // expected format: permission
+ if (!permissionToActions.containsKey(permissionToAction)) {
+ permissionToActions.put(permissionToAction, new HashSet<>());
+ }
+ }
+ }
+
+ if (permissionToActions.isEmpty()) {
+ throw new RuntimeException(String.format(
+ "Method '%s' was annotated with '@PermissionsAllowed', but no valid permission was provided",
+ methodInfo.name()));
+ }
+
+ // permissions specified via @PermissionsAllowed has 'one of' relation, therefore we put them in one list
+ final List orPermissions = new ArrayList<>();
+ final String[] params = instance.value("params") == null ? new String[] { PermissionsAllowed.AUTODETECTED }
+ : instance.value("params").asStringArray();
+ final Type classType = instance.value("permission") == null ? Type.create(STRING_PERMISSION, Type.Kind.CLASS)
+ : instance.value("permission").asClass();
+ for (var permissionToAction : permissionToActions.entrySet()) {
+ final var key = new PermissionKey(permissionToAction.getKey(), permissionToAction.getValue(), params,
+ classType);
+ final int i = cache.indexOf(key);
+ if (i == -1) {
+ orPermissions.add(key);
+ cache.add(key);
+ } else {
+ orPermissions.add(cache.get(i));
+ }
+ }
+
+ // store annotation value as permission keys
+ methodToPermissionKeys
+ .computeIfAbsent(methodInfo, new Function>>() {
+ @Override
+ public List> apply(MethodInfo methodInfo) {
+ return new ArrayList<>();
+ }
+ })
+ .add(List.copyOf(orPermissions));
+ }
+
+ private SecurityCheck createSecurityCheck(LogicalAndPermissionPredicate andPredicate) {
+ final SecurityCheck securityCheck;
+ final boolean isSinglePermissionGroup = andPredicate.operands.size() == 1;
+ if (isSinglePermissionGroup) {
+
+ final LogicalOrPermissionPredicate orPredicate = andPredicate.operands.iterator().next();
+ final boolean isSinglePermission = orPredicate.operands.size() == 1;
+ if (isSinglePermission) {
+
+ // single permission
+ final PermissionWrapper permissionWrapper = orPredicate.operands.iterator().next();
+ securityCheck = recorder.permissionsAllowed(permissionWrapper.computedPermission,
+ permissionWrapper.permission);
+ } else {
+
+ // multiple OR operands (permission OR permission OR ...)
+ if (andPredicate.atLeastOnePermissionIsComputed) {
+ securityCheck = recorder.permissionsAllowed(orPredicate.asComputedPermissions(recorder), null);
+ } else {
+ securityCheck = recorder.permissionsAllowed(null, orPredicate.asPermissions());
+ }
+ }
+ } else {
+
+ // permission group AND permission group AND permission group AND ...
+ // permission group = (permission OR permission OR permission OR ...)
+ if (andPredicate.atLeastOnePermissionIsComputed) {
+ final List>> computedPermissionGroups = new ArrayList<>();
+ for (LogicalOrPermissionPredicate permissionGroup : andPredicate.operands) {
+ computedPermissionGroups.add(permissionGroup.asComputedPermissions(recorder));
+ }
+ securityCheck = recorder.permissionsAllowedGroups(computedPermissionGroups, null);
+ } else {
+ final List>> permissionGroups = new ArrayList<>();
+ for (LogicalOrPermissionPredicate permissionGroup : andPredicate.operands) {
+ permissionGroups.add(permissionGroup.asPermissions());
+ }
+ securityCheck = recorder.permissionsAllowedGroups(null, permissionGroups);
+ }
+ }
+
+ return securityCheck;
+ }
+
+ private PermissionWrapper createPermission(PermissionKey permissionKey, MethodInfo securedMethod,
+ Map cache) {
+ var constructor = classSignatureToConstructor.get(permissionKey.classSignature());
+ return cache.computeIfAbsent(new PermissionCacheKey(permissionKey, securedMethod, constructor),
+ new Function() {
+ @Override
+ public PermissionWrapper apply(PermissionCacheKey permissionCacheKey) {
+ if (permissionCacheKey.computed) {
+ return new PermissionWrapper(createComputedPermission(permissionCacheKey), null);
+ } else {
+ final RuntimeValue permission;
+ if (permissionCacheKey.isStringPermission()) {
+ permission = createStringPermission(permissionCacheKey.permissionKey);
+ } else {
+ permission = createCustomPermission(permissionCacheKey);
+ }
+ return new PermissionWrapper(null, permission);
+ }
+ }
+ });
+ }
+
+ private Function