You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ju...@apache.org on 2019/10/19 18:08:42 UTC
[fineract-cn-permitted-feign-client] 01/36: Initial commit.
This is an automated email from the ASF dual-hosted git repository.
juhan pushed a commit to branch 0.1.x
in repository https://gitbox.apache.org/repos/asf/fineract-cn-permitted-feign-client.git
commit 44a4105a9b209126f7e1d196105d026d3e0ce258
Author: myrle-krantz <mk...@mifos.org>
AuthorDate: Wed May 3 23:56:33 2017 +0200
Initial commit.
---
.gitignore | 14 ++
HEADER | 13 ++
LICENSE | 201 +++++++++++++++++++++
README.md | 24 +++
another-for-test/build.gradle | 61 +++++++
another-for-test/settings.gradle | 1 +
.../main/java/io/mifos/another/api/Another.java | 40 ++++
.../another/service/AnotherConfiguration.java | 48 +++++
.../another/service/AnotherRestController.java | 53 ++++++
another-for-test/src/main/resources/logback.xml | 55 ++++++
api/build.gradle | 44 +++++
api/settings.gradle | 1 +
.../client/ApplicationPermissionRequirements.java | 39 ++++
.../api/v1/domain/ApplicationPermission.java | 80 ++++++++
build.gradle | 44 +++++
component-test/build.gradle | 35 ++++
component-test/settings.gradle | 1 +
.../src/main/java/TestAccessAnother.java | 191 ++++++++++++++++++++
.../main/java/accessanother/api/AccessAnother.java | 34 ++++
.../service/AccessAnotherConfiguration.java | 48 +++++
.../service/AccessAnotherRestController.java | 51 ++++++
.../AnotherWithApplicationPermissions.java | 49 +++++
gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54212 bytes
gradle/wrapper/gradle-wrapper.properties | 6 +
gradlew | 172 ++++++++++++++++++
gradlew.bat | 84 +++++++++
library/build.gradle | 56 ++++++
library/settings.gradle | 1 +
.../annotation/EndpointSet.java | 28 +++
.../PermittedFeignClientsConfiguration.java | 92 ++++++++++
.../EnablePermissionRequestingFeignClient.java | 39 ++++
...ermittedFeignClientBeanDefinitionRegistrar.java | 49 +++++
.../config/PermittedFeignClientConfiguration.java | 29 +++
.../config/PermittedFeignClientImportSelector.java | 39 ++++
...cationPermissionRequirementsRestController.java | 59 ++++++
.../ApplicationTokenedTargetInterceptor.java | 55 ++++++
.../service/ApplicationAccessTokenService.java | 143 +++++++++++++++
.../ApplicationPermissionRequirementsService.java | 134 ++++++++++++++
.../service/ApplicationAccessTokenServiceTest.java | 62 +++++++
...plicationPermissionRequirementsServiceTest.java | 104 +++++++++++
settings.gradle | 6 +
shared.gradle | 79 ++++++++
42 files changed, 2364 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..300fb08
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+.gradle
+.idea
+build/
+target/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+*.iml
+
+*.log
diff --git a/HEADER b/HEADER
new file mode 100644
index 0000000..d47a70e
--- /dev/null
+++ b/HEADER
@@ -0,0 +1,13 @@
+Copyright ${year} ${name}.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1f44f4c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# Mifos I/O Permitted Feign Client Library
+
+[![Join the chat at https://gitter.im/mifos-initiative/mifos.io](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mifos-initiative/mifos.io?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+This project provides secured access to other microservices via Feign. For this it uses anubis and identity to provide refresh and access tokens transparently.
+
+## Abstract
+Mifos I/O is an application framework for digital financial services, a system to support nationwide and cross-national financial transactions and help to level and speed the creation of an inclusive, interconnected digital economy for every nation in the world.
+
+## Versioning
+The version numbers follow the [Semantic Versioning](http://semver.org/) scheme.
+
+In addition to MAJOR.MINOR.PATCH the following postfixes are used to indicate the development state.
+
+* BUILD-SNAPSHOT - A release currently in development.
+* RELEASE - _General availability_ indicates that this release is the best available version and is recommended for all usage.
+
+The versioning layout is {MAJOR}.{MINOR}.{PATCH}-{INDICATOR}[.{PATCH}]. Only milestones and release candidates can have patch versions. Some examples:
+
+1.2.3-BUILD-SNAPSHOT
+1.3.5-RELEASE
+
+## License
+See [LICENSE](LICENSE) file.
diff --git a/another-for-test/build.gradle b/another-for-test/build.gradle
new file mode 100644
index 0000000..56ee324
--- /dev/null
+++ b/another-for-test/build.gradle
@@ -0,0 +1,61 @@
+buildscript {
+ ext {
+ springBootVersion = '1.4.1.RELEASE'
+ }
+
+ repositories {
+ jcenter()
+ }
+
+ dependencies {
+ classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+ }
+}
+
+plugins {
+ id 'com.github.hierynomus.license' version '0.13.1'
+}
+
+apply from: '../shared.gradle'
+
+apply plugin: 'spring-boot'
+
+springBoot {
+ executable = true
+ classifier = 'boot'
+}
+
+dependencies {
+ compile(
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-config'],
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka'],
+ [group: 'org.springframework.boot', name: 'spring-boot-starter-jetty'],
+ [group: 'org.hibernate', name: 'hibernate-validator', version: versions.hibernatevalidator],
+ [group: 'io.mifos.core', name: 'lang', version: versions.frameworklang],
+ [group: 'io.mifos.core', name: 'cassandra', version: versions.frameworkcassandra],
+ [group: 'io.jsonwebtoken', name: 'jjwt', version: versions.jjwt],
+ [group: 'io.mifos.anubis', name: 'api', version: versions.frameworkanubis],
+ [group: 'io.mifos.anubis', name: 'library', version: versions.frameworkanubis],
+ )
+}
+
+publishToMavenLocal.dependsOn bootRepackage
+
+
+publishing {
+ publications {
+ service(MavenPublication) {
+ from components.java
+ groupId project.group
+ artifactId project.name
+ version project.version
+ }
+ bootService(MavenPublication) {
+ // "boot" jar
+ artifact ("$buildDir/libs/$project.name-$version-boot.jar")
+ groupId project.group
+ artifactId ("service-boot")
+ version project.version
+ }
+ }
+}
diff --git a/another-for-test/settings.gradle b/another-for-test/settings.gradle
new file mode 100644
index 0000000..bd1dca3
--- /dev/null
+++ b/another-for-test/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'another-for-test'
diff --git a/another-for-test/src/main/java/io/mifos/another/api/Another.java b/another-for-test/src/main/java/io/mifos/another/api/Another.java
new file mode 100644
index 0000000..7b72fd4
--- /dev/null
+++ b/another-for-test/src/main/java/io/mifos/another/api/Another.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.another.api;
+
+import io.mifos.anubis.api.v1.client.Anubis;
+import io.mifos.core.api.util.CustomFeignClientsConfiguration;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@FeignClient(name="another-v1", path="/another/v1", configuration = CustomFeignClientsConfiguration.class)
+public interface Another extends Anubis {
+ @RequestMapping(value = "/foo", method = RequestMethod.POST,
+ consumes = {MediaType.APPLICATION_JSON_VALUE},
+ produces = {MediaType.ALL_VALUE})
+ void createFoo();
+
+ @RequestMapping(value = "/foo", method = RequestMethod.GET,
+ consumes = {MediaType.APPLICATION_JSON_VALUE},
+ produces = {MediaType.ALL_VALUE})
+ boolean getFoo();
+}
diff --git a/another-for-test/src/main/java/io/mifos/another/service/AnotherConfiguration.java b/another-for-test/src/main/java/io/mifos/another/service/AnotherConfiguration.java
new file mode 100644
index 0000000..a3ade44
--- /dev/null
+++ b/another-for-test/src/main/java/io/mifos/another/service/AnotherConfiguration.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.another.service;
+
+import io.mifos.anubis.config.EnableAnubis;
+import io.mifos.core.lang.config.EnableApplicationName;
+import io.mifos.core.lang.config.EnableServiceException;
+import io.mifos.core.lang.config.EnableTenantContext;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+/**
+ * @author Myrle Krantz
+ */
+@Configuration
+@EnableAutoConfiguration
+@EnableWebMvc
+@EnableDiscoveryClient
+@EnableTenantContext
+@EnableAnubis(generateEmptyInitializeEndpoint = true)
+@EnableApplicationName
+@EnableServiceException
+@ComponentScan({
+ "io.mifos.another.service"
+})
+public class AnotherConfiguration {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AnotherConfiguration.class, args);
+ }
+}
diff --git a/another-for-test/src/main/java/io/mifos/another/service/AnotherRestController.java b/another-for-test/src/main/java/io/mifos/another/service/AnotherRestController.java
new file mode 100644
index 0000000..f1630a6
--- /dev/null
+++ b/another-for-test/src/main/java/io/mifos/another/service/AnotherRestController.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.another.service;
+
+import io.mifos.anubis.annotation.AcceptedTokenType;
+import io.mifos.anubis.annotation.Permittable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Myrle Krantz
+ */
+@RestController
+public class AnotherRestController {
+
+ private boolean fooWasPosted;
+
+ @RequestMapping(
+ value = "/foo",
+ method = RequestMethod.POST
+ )
+ @Permittable(value = AcceptedTokenType.GUEST)
+ public @ResponseBody
+ ResponseEntity<Void> resourceThatNeedsAnotherResource() {
+ fooWasPosted = true;
+ return ResponseEntity.ok().build();
+ }
+
+ @RequestMapping(
+ value = "/foo",
+ method = RequestMethod.GET
+ )
+ @Permittable(value = AcceptedTokenType.GUEST)
+ public @ResponseBody ResponseEntity<Boolean> getFoo() {
+ return ResponseEntity.ok(fooWasPosted);
+ }
+}
diff --git a/another-for-test/src/main/resources/logback.xml b/another-for-test/src/main/resources/logback.xml
new file mode 100644
index 0000000..73b6cf3
--- /dev/null
+++ b/another-for-test/src/main/resources/logback.xml
@@ -0,0 +1,55 @@
+<!--
+
+ Copyright 2017 The Mifos Initiative.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<configuration>
+ <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>logs/another.log</file>
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <fileNamePattern>another.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <maxHistory>7</maxHistory>
+ <totalSizeCap>2GB</totalSizeCap>
+ </rollingPolicy>
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="com" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="org" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="io" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="net" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <root level="DEBUG">
+ <appender-ref ref="FILE"/>
+ </root>
+</configuration>
\ No newline at end of file
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 0000000..1a5afae
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,44 @@
+buildscript {
+
+ repositories {
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'io.spring.gradle:dependency-management-plugin:0.6.0.RELEASE'
+ }
+}
+
+plugins {
+ id 'com.github.hierynomus.license' version '0.13.1'
+}
+
+
+apply from: '../shared.gradle'
+
+apply plugin: 'io.spring.dependency-management'
+
+dependencies {
+ compile (
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'],
+ [group: 'org.hibernate', name: 'hibernate-validator', version: versions.hibernatevalidator],
+ [group: 'com.google.code.gson', name: 'gson'],
+ [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+ [group: 'io.mifos.identity', name: 'api', version: versions.frameworkidentity]
+ )
+
+ testCompile(
+ [group: 'io.mifos.core', name: 'test', version: versions.frameworktest],
+ )
+}
+
+publishing {
+ publications {
+ apiPublication(MavenPublication) {
+ from components.java
+ groupId project.group
+ artifactId project.name
+ version project.version
+ }
+ }
+}
diff --git a/api/settings.gradle b/api/settings.gradle
new file mode 100644
index 0000000..5cd7dd3
--- /dev/null
+++ b/api/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'api'
diff --git a/api/src/main/java/io/mifos/permittedfeignclient/api/v1/client/ApplicationPermissionRequirements.java b/api/src/main/java/io/mifos/permittedfeignclient/api/v1/client/ApplicationPermissionRequirements.java
new file mode 100644
index 0000000..b3a971b
--- /dev/null
+++ b/api/src/main/java/io/mifos/permittedfeignclient/api/v1/client/ApplicationPermissionRequirements.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.api.v1.client;
+
+import io.mifos.permittedfeignclient.api.v1.domain.ApplicationPermission;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+@FeignClient
+public interface ApplicationPermissionRequirements {
+ @RequestMapping(
+ value = "/requiredpermissions",
+ method = RequestMethod.GET,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.ALL_VALUE
+ )
+ List<ApplicationPermission> getRequiredPermissions();
+}
diff --git a/api/src/main/java/io/mifos/permittedfeignclient/api/v1/domain/ApplicationPermission.java b/api/src/main/java/io/mifos/permittedfeignclient/api/v1/domain/ApplicationPermission.java
new file mode 100644
index 0000000..84e999e
--- /dev/null
+++ b/api/src/main/java/io/mifos/permittedfeignclient/api/v1/domain/ApplicationPermission.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.api.v1.domain;
+
+import io.mifos.core.lang.validation.constraints.ValidIdentifier;
+import io.mifos.identity.api.v1.domain.Permission;
+
+import javax.annotation.Nullable;
+import javax.validation.Valid;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+public class ApplicationPermission {
+ @ValidIdentifier
+ private String endpointSetIdentifier;
+ @Valid
+ private Permission permission;
+
+ public ApplicationPermission() {
+ }
+
+ public ApplicationPermission(@Nullable String endpointSetIdentifier, Permission permission) {
+ this.endpointSetIdentifier = endpointSetIdentifier;
+ this.permission = permission;
+ }
+
+ public String getEndpointSetIdentifier() {
+ return endpointSetIdentifier;
+ }
+
+ public void setEndpointSetIdentifier(String endpointSetIdentifier) {
+ this.endpointSetIdentifier = endpointSetIdentifier;
+ }
+
+ public Permission getPermission() {
+ return permission;
+ }
+
+ public void setPermission(Permission permission) {
+ this.permission = permission;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ApplicationPermission that = (ApplicationPermission) o;
+ return Objects.equals(endpointSetIdentifier, that.endpointSetIdentifier) &&
+ Objects.equals(permission, that.permission);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(endpointSetIdentifier, permission);
+ }
+
+ @Override
+ public String toString() {
+ return "ApplicationPermission{" +
+ "endpointSetIdentifier='" + endpointSetIdentifier + '\'' +
+ ", permission=" + permission +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..e2216eb
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,44 @@
+group 'io.mifos'
+
+task publishApiToMavenLocal {
+ dependsOn gradle.includedBuild('api').task(':publishToMavenLocal')
+}
+
+task publishLibraryToMavenLocal {
+ mustRunAfter publishApiToMavenLocal
+ dependsOn gradle.includedBuild('library').task(':publishToMavenLocal')
+}
+
+task publishAnotherForTestToMavenLocal {
+ mustRunAfter publishLibraryToMavenLocal
+ dependsOn gradle.includedBuild('another-for-test').task(':publishToMavenLocal')
+}
+
+task publishComponentTestToMavenLocal {
+ mustRunAfter publishApiToMavenLocal
+ mustRunAfter publishLibraryToMavenLocal
+ mustRunAfter publishAnotherForTestToMavenLocal
+ dependsOn gradle.includedBuild('component-test').task(':publishToMavenLocal')
+}
+
+task publishToMavenLocal {
+ group 'all'
+ dependsOn publishApiToMavenLocal
+ dependsOn publishLibraryToMavenLocal
+ dependsOn publishAnotherForTestToMavenLocal
+ dependsOn publishComponentTestToMavenLocal
+}
+
+task licenseFormat {
+ group 'all'
+ dependsOn gradle.includedBuild('api').task(':licenseFormat')
+ dependsOn gradle.includedBuild('library').task(':licenseFormat')
+ dependsOn gradle.includedBuild('another-for-test').task(':licenseFormat')
+ dependsOn gradle.includedBuild('component-test').task(':licenseFormat')
+}
+
+task prepareForTest {
+ group 'all'
+ dependsOn publishToMavenLocal
+ dependsOn gradle.includedBuild('component-test').task(':build')
+}
diff --git a/component-test/build.gradle b/component-test/build.gradle
new file mode 100644
index 0000000..4ed858f
--- /dev/null
+++ b/component-test/build.gradle
@@ -0,0 +1,35 @@
+buildscript {
+ ext {
+ springBootVersion = '1.4.1.RELEASE'
+ }
+
+ repositories {
+ jcenter()
+ }
+
+ dependencies {
+ classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+ }
+}
+
+plugins {
+ id 'com.github.hierynomus.license' version '0.13.1'
+}
+
+apply from: '../shared.gradle'
+
+dependencies {
+ compile(
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-config'],
+ [group: 'org.springframework.boot', name: 'spring-boot-starter-jetty'],
+ [group: 'org.springframework.boot', name: 'spring-boot-starter-test'],
+ [group: 'com.google.code.gson', name: 'gson'],
+ [group: 'io.jsonwebtoken', name: 'jjwt', version: versions.jjwt],
+ [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+ [group: 'io.mifos.core', name: 'test', version: versions.frameworktest],
+ [group: 'io.mifos', name: 'service-starter', version: versions.frameworkservicestarter],
+ [group: 'io.mifos.permitted-feign-client', name: 'another-for-test', version: rootProject.version],
+ [group: 'io.mifos.permitted-feign-client', name: 'library', version: rootProject.version],
+ [group: 'io.mifos.permitted-feign-client', name: 'api', version: rootProject.version],
+ )
+}
diff --git a/component-test/settings.gradle b/component-test/settings.gradle
new file mode 100644
index 0000000..b2e36e3
--- /dev/null
+++ b/component-test/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'component-test'
diff --git a/component-test/src/main/java/TestAccessAnother.java b/component-test/src/main/java/TestAccessAnother.java
new file mode 100644
index 0000000..c896c9d
--- /dev/null
+++ b/component-test/src/main/java/TestAccessAnother.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import accessanother.api.AccessAnother;
+import accessanother.service.AccessAnotherConfiguration;
+import io.mifos.another.api.Another;
+import io.mifos.anubis.api.v1.domain.AllowedOperation;
+import io.mifos.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import io.mifos.core.api.config.EnableApiFactory;
+import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.api.util.ApiFactory;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.servicestarter.EurekaForTest;
+import io.mifos.core.test.servicestarter.InitializedMicroservice;
+import io.mifos.core.test.servicestarter.IntegrationTestEnvironment;
+import io.mifos.identity.api.v1.client.IdentityManager;
+import io.mifos.identity.api.v1.domain.Authentication;
+import io.mifos.identity.api.v1.domain.Permission;
+import io.mifos.permittedfeignclient.api.v1.client.ApplicationPermissionRequirements;
+import io.mifos.permittedfeignclient.api.v1.domain.ApplicationPermission;
+import org.junit.*;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static accessanother.service.apiforother.AnotherWithApplicationPermissions.ENDPOINT_SET_IDENTIFIER;
+import static io.mifos.core.test.env.TestEnvironment.RIBBON_USES_EUREKA_PROPERTY;
+import static io.mifos.core.test.env.TestEnvironment.SPRING_CLOUD_DISCOVERY_ENABLED_PROPERTY;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+public class TestAccessAnother {
+ private static final String APP_NAME = "accessanother-v1";
+ private static final String LOGGER_QUALIFIER = "test-logger";
+ private static final String USER_NAME = "blah";
+
+ @Configuration
+ @EnableApiFactory
+ @Import({AccessAnotherConfiguration.class})
+ public static class TestConfiguration {
+ public TestConfiguration() {
+ super();
+ }
+
+ @Bean(name = LOGGER_QUALIFIER)
+ public Logger logger() {
+ return LoggerFactory.getLogger(APP_NAME + "-logger");
+ }
+ }
+
+ private final static EurekaForTest eurekaForTest = new EurekaForTest();
+ private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+ private final static IntegrationTestEnvironment integrationTestEnvironment = new IntegrationTestEnvironment(cassandraInitializer);
+ private final static InitializedMicroservice<Another> another= new InitializedMicroservice<>(Another.class, "permitted-feign-client", "0.1.0-BUILD-SNAPSHOT", integrationTestEnvironment);
+
+ @ClassRule
+ public static TestRule orderedRules = RuleChain
+ .outerRule(eurekaForTest)
+ .around(cassandraInitializer)
+ .around(integrationTestEnvironment)
+ .around(another);
+
+
+ private final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+
+ @BeforeClass
+ public static void someExtraTestEnvironmentStuff() {
+ testEnvironment.setKeyPair(integrationTestEnvironment.getSeshatKeyTimestamp(), integrationTestEnvironment.getSeshatPublicKey(), integrationTestEnvironment.getSeshatPrivateKey());
+ testEnvironment.setProperty("eureka.client.serviceUrl.defaultZone", "http://localhost:8761/eureka");
+ testEnvironment.setProperty(SPRING_CLOUD_DISCOVERY_ENABLED_PROPERTY, "true");
+ testEnvironment.setProperty(RIBBON_USES_EUREKA_PROPERTY, "true");
+ testEnvironment.setProperty("eureka.instance.hostname", "localhost");
+ testEnvironment.setProperty("eureka.client.fetchRegistry", "true");
+ testEnvironment.setProperty("eureka.registration.enabled", "true");
+ testEnvironment.setProperty("eureka.instance.leaseRenewalIntervalInSeconds", "1"); //Speed up registration for test purposes.
+ testEnvironment.setProperty("eureka.client.initialInstanceInfoReplicationIntervalSeconds", "0"); //Speed up initial registration for test purposes.
+ testEnvironment.setProperty("eureka.client.instanceInfoReplicationIntervalSeconds", "1");
+ testEnvironment.populate();
+ }
+
+ @Rule
+ public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
+ = new TenantApplicationSecurityEnvironmentTestRule(APP_NAME,
+ testEnvironment.serverURI(), integrationTestEnvironment.getSystemSecurityEnvironment(),
+ this::waitForInitialize);
+
+ @SuppressWarnings({"SpringAutowiredFieldsWarningInspection", "SpringJavaAutowiredMembersInspection"})
+ @Autowired
+ private ApiFactory apiFactory;
+
+ @MockBean
+ private IdentityManager identityManager;
+
+ private AccessAnother accessAnother;
+ private ApplicationPermissionRequirements applicationPermissionRequirements;
+
+ @Before
+ public void before()
+ {
+ another.setApiFactory(apiFactory);
+ accessAnother = apiFactory.create(AccessAnother.class, testEnvironment.serverURI());
+ applicationPermissionRequirements = apiFactory.create(ApplicationPermissionRequirements.class, testEnvironment.serverURI());
+ }
+
+
+ public boolean waitForInitialize() {
+ try {
+ TimeUnit.SECONDS.sleep(15);
+ return true;
+ } catch (final InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Test
+ public void permissionRequirementsListedProperly() {
+ final List<ApplicationPermission> requiredPermissions = applicationPermissionRequirements.getRequiredPermissions();
+ Assert.assertFalse(requiredPermissions.isEmpty());
+ Assert.assertTrue(requiredPermissions.toString(), requiredPermissions.contains(
+ new ApplicationPermission(ENDPOINT_SET_IDENTIFIER,
+ new Permission(accessanother.service.apiforother.AnotherWithApplicationPermissions.ANOTHER_FOO_PERMITTABLE_GROUP,
+ new HashSet<>(Arrays.asList(AllowedOperation.READ, AllowedOperation.CHANGE))))));
+ }
+
+ @Test
+ public void canAccessAnother()
+ {
+ try (final AutoUserContext ignored = integrationTestEnvironment.createAutoUserContext(USER_NAME)) {
+ Assert.assertFalse(another.api().getFoo());
+ }
+
+ mockIdentityManagerInteraction();
+ try (final AutoUserContext ignored = tenantApplicationSecurityEnvironment.createAutoUserContext("blah")) {
+ accessAnother.createDummy();
+ }
+ try (final AutoUserContext ignored = integrationTestEnvironment.createAutoUserContext("blah")) {
+ Assert.assertTrue(another.api().getFoo());
+ }
+ }
+
+ private void mockIdentityManagerInteraction() {
+ final String token = tenantApplicationSecurityEnvironment.getSystemSecurityEnvironment()
+ .getPermissionToken(USER_NAME, "another-v1", "/foo", AllowedOperation.CHANGE);
+
+ final String expirationString = getExpirationString();
+ final Authentication applicationAuthentication = new Authentication(token, expirationString, expirationString, null);
+ Mockito.doReturn(applicationAuthentication).when(identityManager).refresh(Mockito.anyString());
+ }
+
+ private String getExpirationString() {
+ final long issued = System.currentTimeMillis();
+ final Date expiration = new Date(issued + TimeUnit.SECONDS.toMillis(30));
+ final LocalDateTime localDateTimeExpiration = LocalDateTime.ofInstant(expiration.toInstant(), ZoneId.of("UTC"));
+ return DateConverter.toIsoString(localDateTimeExpiration);
+ }
+}
diff --git a/component-test/src/main/java/accessanother/api/AccessAnother.java b/component-test/src/main/java/accessanother/api/AccessAnother.java
new file mode 100644
index 0000000..ea1ad7d
--- /dev/null
+++ b/component-test/src/main/java/accessanother/api/AccessAnother.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package accessanother.api;
+
+import io.mifos.anubis.api.v1.client.Anubis;
+import io.mifos.core.api.util.CustomFeignClientsConfiguration;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+/**
+ * @author Myrle Krantz
+ */
+@FeignClient(name="accessanother-v1", path="/accessanother/v1", configuration = CustomFeignClientsConfiguration.class)
+public interface AccessAnother extends Anubis {
+ @RequestMapping(value = "/dummy", method = RequestMethod.POST,
+ consumes = {MediaType.APPLICATION_JSON_VALUE},
+ produces = {MediaType.ALL_VALUE})
+ void createDummy();
+}
diff --git a/component-test/src/main/java/accessanother/service/AccessAnotherConfiguration.java b/component-test/src/main/java/accessanother/service/AccessAnotherConfiguration.java
new file mode 100644
index 0000000..486d491
--- /dev/null
+++ b/component-test/src/main/java/accessanother/service/AccessAnotherConfiguration.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package accessanother.service;
+
+import accessanother.service.apiforother.AnotherWithApplicationPermissions;
+import io.mifos.anubis.config.EnableAnubis;
+import io.mifos.core.lang.config.EnableServiceException;
+import io.mifos.core.lang.config.EnableTenantContext;
+import io.mifos.permittedfeignclient.config.EnablePermissionRequestingFeignClient;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+/**
+ * @author Myrle Krantz
+ */
+@Configuration
+@EnableAutoConfiguration
+@EnableWebMvc
+@EnableDiscoveryClient
+@EnableTenantContext
+@EnableAnubis(generateEmptyInitializeEndpoint = true)
+@EnableFeignClients(basePackages = {"accessanother.service.apiforother"})
+@RibbonClient(name = "accessanother-v1")
+@EnableServiceException
+@EnablePermissionRequestingFeignClient(feignClasses = {AnotherWithApplicationPermissions.class})
+@ComponentScan({
+ "accessanother.service"
+})
+public class AccessAnotherConfiguration {
+}
diff --git a/component-test/src/main/java/accessanother/service/AccessAnotherRestController.java b/component-test/src/main/java/accessanother/service/AccessAnotherRestController.java
new file mode 100644
index 0000000..31f89e0
--- /dev/null
+++ b/component-test/src/main/java/accessanother/service/AccessAnotherRestController.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package accessanother.service;
+
+import accessanother.service.apiforother.AnotherWithApplicationPermissions;
+import io.mifos.anubis.annotation.Permittable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+
+/**
+ * @author Myrle Krantz
+ */
+@RestController
+public class AccessAnotherRestController {
+ private final AnotherWithApplicationPermissions anotherWithApplicationPermissions;
+
+ @Autowired
+ public AccessAnotherRestController(@SuppressWarnings("SpringJavaAutowiringInspection") final AnotherWithApplicationPermissions anotherWithApplicationPermissions) {
+
+ this.anotherWithApplicationPermissions = anotherWithApplicationPermissions;
+ }
+
+ @RequestMapping(
+ value = "/dummy",
+ method = RequestMethod.POST
+ )
+ @Permittable()
+ public @ResponseBody
+ ResponseEntity<Void> resourceThatNeedsAnotherResource() {
+ anotherWithApplicationPermissions.createFoo();
+ return ResponseEntity.ok().build();
+ }
+}
\ No newline at end of file
diff --git a/component-test/src/main/java/accessanother/service/apiforother/AnotherWithApplicationPermissions.java b/component-test/src/main/java/accessanother/service/apiforother/AnotherWithApplicationPermissions.java
new file mode 100644
index 0000000..f9ccc7e
--- /dev/null
+++ b/component-test/src/main/java/accessanother/service/apiforother/AnotherWithApplicationPermissions.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package accessanother.service.apiforother;
+
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.permittedfeignclient.annotation.EndpointSet;
+import io.mifos.permittedfeignclient.annotation.PermittedFeignClientsConfiguration;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+/**
+ * @author Myrle Krantz
+ */
+@EndpointSet(identifier = AnotherWithApplicationPermissions.ENDPOINT_SET_IDENTIFIER)
+@FeignClient(name="another-v1", path="/another/v1", configuration = PermittedFeignClientsConfiguration.class)
+public interface AnotherWithApplicationPermissions {
+ String ENDPOINT_SET_IDENTIFIER = "x";
+ String ANOTHER_FOO_PERMITTABLE_GROUP = "group_for_another";
+
+ @RequestMapping(value = "/foo", method = RequestMethod.POST,
+ consumes = {MediaType.APPLICATION_JSON_VALUE},
+ produces = {MediaType.ALL_VALUE})
+ @Permittable(groupId = ANOTHER_FOO_PERMITTABLE_GROUP)
+ void createFoo();
+
+ @RequestMapping(value = "/foo", method = RequestMethod.GET,
+ consumes = {MediaType.APPLICATION_JSON_VALUE},
+ produces = {MediaType.ALL_VALUE})
+ @Permittable(groupId = ANOTHER_FOO_PERMITTABLE_GROUP)
+ boolean getFoo();
+
+ //TODO: also test multiple permittables.
+ //TODO: also think about upgradeability when permission needs change.
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7724e6e
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..8521cd8
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Apr 18 15:55:31 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4453cce
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/library/build.gradle b/library/build.gradle
new file mode 100644
index 0000000..39c27a4
--- /dev/null
+++ b/library/build.gradle
@@ -0,0 +1,56 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'io.spring.gradle:dependency-management-plugin:0.6.0.RELEASE'
+ }
+}
+
+plugins {
+ id 'com.github.hierynomus.license' version '0.13.1'
+}
+
+apply from: '../shared.gradle'
+
+apply plugin: 'io.spring.dependency-management'
+
+dependencyManagement {
+ imports {
+ mavenBom 'org.springframework.cloud:spring-cloud-netflix:1.2.0.RELEASE'
+ }
+}
+
+dependencies {
+ compile(
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'],
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka'],
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-security'],
+ [group: 'org.hibernate', name: 'hibernate-validator', version: versions.hibernatevalidator],
+ [group: 'io.jsonwebtoken', name: 'jjwt', version: versions.jjwt],
+ [group: 'io.mifos.core', name: 'lang', version: versions.frameworklang],
+ [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+ [group: 'io.mifos.core', name: 'cassandra', version: versions.frameworkcassandra],
+ [group: 'io.mifos.anubis', name: 'api', version: versions.frameworkanubis],
+ [group: 'io.mifos.anubis', name: 'library', version: versions.frameworkanubis],
+ [group: 'io.mifos.permitted-feign-client', name: 'api', version: rootProject.version],
+ [group: 'net.jodah', name: 'expiringmap', version: versions.expiringmap],
+
+ )
+}
+
+jar {
+ from sourceSets.main.allSource
+}
+
+publishing {
+ publications {
+ libraryPublication(MavenPublication) {
+ from components.java
+ groupId project.group
+ artifactId project.name
+ version project.version
+ }
+ }
+}
diff --git a/library/settings.gradle b/library/settings.gradle
new file mode 100644
index 0000000..835224a
--- /dev/null
+++ b/library/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'library'
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/annotation/EndpointSet.java b/library/src/main/java/io/mifos/permittedfeignclient/annotation/EndpointSet.java
new file mode 100644
index 0000000..def34f4
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/annotation/EndpointSet.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Documented
+public @interface EndpointSet {
+ String identifier() default "";
+}
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/annotation/PermittedFeignClientsConfiguration.java b/library/src/main/java/io/mifos/permittedfeignclient/annotation/PermittedFeignClientsConfiguration.java
new file mode 100644
index 0000000..cf84031
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/annotation/PermittedFeignClientsConfiguration.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.annotation;
+
+import feign.Feign;
+import feign.Target;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import feign.gson.GsonDecoder;
+import feign.gson.GsonEncoder;
+import io.mifos.core.api.util.AnnotatedErrorDecoder;
+import io.mifos.core.api.util.TenantedTargetInterceptor;
+import io.mifos.permittedfeignclient.security.ApplicationTokenedTargetInterceptor;
+import io.mifos.permittedfeignclient.service.ApplicationAccessTokenService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.cloud.netflix.feign.FeignClientsConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Scope;
+
+import static io.mifos.core.api.config.ApiConfiguration.LOGGER_NAME;
+
+/**
+ * @author Myrle Krantz
+ */
+public class PermittedFeignClientsConfiguration extends FeignClientsConfiguration {
+ private static class FeignBuilder extends Feign.Builder {
+ private final ApplicationAccessTokenService applicationAccessTokenService;
+ private final Logger logger;
+
+ FeignBuilder(
+ final ApplicationAccessTokenService applicationAccessTokenService,
+ final Logger logger) {
+ this.applicationAccessTokenService = applicationAccessTokenService;
+ this.logger = logger;
+ }
+
+ public <T> T target(final Target<T> target) {
+ this.errorDecoder(new AnnotatedErrorDecoder(logger, target.type()));
+ this.requestInterceptor(new TenantedTargetInterceptor());
+ this.requestInterceptor(new ApplicationTokenedTargetInterceptor(
+ applicationAccessTokenService,
+ target.type()));
+ return build().newInstance(target);
+ }
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public Decoder feignDecoder() {
+ return new GsonDecoder();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public Encoder feignEncoder() {
+ return new GsonEncoder();
+ }
+
+ @Bean(name = LOGGER_NAME)
+ @ConditionalOnMissingBean
+ public Logger logger() {
+ return LoggerFactory.getLogger(LOGGER_NAME);
+ }
+
+ @Bean
+ @Scope("prototype")
+ @ConditionalOnMissingBean
+ public Feign.Builder permittedFeignBuilder(
+ @SuppressWarnings("SpringJavaAutowiringInspection")
+ final ApplicationAccessTokenService applicationAccessTokenService,
+ @Qualifier(LOGGER_NAME) final Logger logger) {
+ return new FeignBuilder(
+ applicationAccessTokenService,
+ logger);
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/config/EnablePermissionRequestingFeignClient.java b/library/src/main/java/io/mifos/permittedfeignclient/config/EnablePermissionRequestingFeignClient.java
new file mode 100644
index 0000000..93213dc
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/config/EnablePermissionRequestingFeignClient.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.config;
+
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Import({
+ PermittedFeignClientBeanDefinitionRegistrar.class,
+ PermittedFeignClientImportSelector.class,
+ PermittedFeignClientConfiguration.class,
+})
+public @interface EnablePermissionRequestingFeignClient {
+ /**
+ * @return A list of classes annotated with @EndpointSet and @FeignClient
+ */
+ Class<?>[] feignClasses() default {};
+}
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientBeanDefinitionRegistrar.java b/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientBeanDefinitionRegistrar.java
new file mode 100644
index 0000000..879e39b
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientBeanDefinitionRegistrar.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.config;
+
+import io.mifos.permittedfeignclient.service.ApplicationPermissionRequirementsService;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
+import org.springframework.core.type.AnnotationMetadata;
+
+import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_SINGLETON;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("WeakerAccess")
+public class PermittedFeignClientBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
+
+ @Override
+ public void registerBeanDefinitions(
+ final AnnotationMetadata importingClassMetadata,
+ final BeanDefinitionRegistry registry) {
+
+ final Object clients = importingClassMetadata.getAnnotationAttributes(
+ EnablePermissionRequestingFeignClient.class.getTypeName()).get("feignClasses");
+
+ final AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
+ .genericBeanDefinition(ApplicationPermissionRequirementsService.class)
+ .addConstructorArgValue(clients)
+ .setScope(SCOPE_SINGLETON)
+ .getBeanDefinition();
+
+ registry.registerBeanDefinition("applicationPermissionRequirementsService", beanDefinition);
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientConfiguration.java b/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientConfiguration.java
new file mode 100644
index 0000000..b37a43b
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.config;
+
+import io.mifos.anubis.config.EnableAnubis;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Myrle Krantz
+ */
+@EnableAnubis
+@EnableFeignClients(basePackages = {"io.mifos.identity.api.v1"})
+@Configuration
+public class PermittedFeignClientConfiguration {
+}
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientImportSelector.java b/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientImportSelector.java
new file mode 100644
index 0000000..3ecb36c
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/config/PermittedFeignClientImportSelector.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.config;
+
+import io.mifos.permittedfeignclient.controller.ApplicationPermissionRequirementsRestController;
+import io.mifos.permittedfeignclient.service.ApplicationAccessTokenService;
+import org.springframework.context.annotation.ImportSelector;
+import org.springframework.core.type.AnnotationMetadata;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author Myrle Krantz
+ */
+class PermittedFeignClientImportSelector implements ImportSelector {
+ PermittedFeignClientImportSelector() { }
+
+ @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) {
+ final Set<Class> classesToImport = new HashSet<>();
+ classesToImport.add(ApplicationPermissionRequirementsRestController.class);
+ classesToImport.add(ApplicationAccessTokenService.class);
+
+ return classesToImport.stream().map(Class::getCanonicalName).toArray(String[]::new);
+ }
+}
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/controller/ApplicationPermissionRequirementsRestController.java b/library/src/main/java/io/mifos/permittedfeignclient/controller/ApplicationPermissionRequirementsRestController.java
new file mode 100644
index 0000000..b34a147
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/controller/ApplicationPermissionRequirementsRestController.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.controller;
+
+import io.mifos.anubis.annotation.AcceptedTokenType;
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.permittedfeignclient.service.ApplicationPermissionRequirementsService;
+import io.mifos.permittedfeignclient.api.v1.domain.ApplicationPermission;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@RestController
+@RequestMapping("/requiredpermissions")
+public class ApplicationPermissionRequirementsRestController {
+
+ private final ApplicationPermissionRequirementsService service;
+
+ @Autowired
+ public ApplicationPermissionRequirementsRestController(final ApplicationPermissionRequirementsService service) {
+ this.service = service;
+ }
+
+ @Permittable(AcceptedTokenType.GUEST)
+ @RequestMapping(
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public
+ @ResponseBody
+ ResponseEntity<List<ApplicationPermission>> getRequiredPermissions() {
+ final List<ApplicationPermission> requiredPermissions = service.getRequiredPermissions();
+
+ return ResponseEntity.ok(new ArrayList<>(requiredPermissions));
+ }
+}
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/security/ApplicationTokenedTargetInterceptor.java b/library/src/main/java/io/mifos/permittedfeignclient/security/ApplicationTokenedTargetInterceptor.java
new file mode 100644
index 0000000..2f3c7e7
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/security/ApplicationTokenedTargetInterceptor.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.security;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import io.mifos.core.api.util.ApiConstants;
+import io.mifos.core.api.util.UserContext;
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.permittedfeignclient.annotation.EndpointSet;
+import io.mifos.permittedfeignclient.service.ApplicationAccessTokenService;
+import org.springframework.util.Assert;
+
+import javax.annotation.Nonnull;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ApplicationTokenedTargetInterceptor implements RequestInterceptor {
+ private final ApplicationAccessTokenService applicationAccessTokenService;
+ private final String endpointSetIdentifier;
+
+ public <T> ApplicationTokenedTargetInterceptor(
+ final @Nonnull ApplicationAccessTokenService applicationAccessTokenService,
+ final @Nonnull Class<T> type) {
+ Assert.notNull(applicationAccessTokenService);
+ Assert.notNull(type);
+
+ this.applicationAccessTokenService = applicationAccessTokenService;
+ final EndpointSet endpointSet = type.getAnnotation(EndpointSet.class);
+ Assert.notNull(endpointSet, "Permitted feign clients require an endpoint set identifier provided via @EndpointSet.");
+ this.endpointSetIdentifier = endpointSet.identifier();
+ }
+
+ @Override
+ public void apply(final RequestTemplate template) {
+ template.header(ApiConstants.AUTHORIZATION_HEADER, applicationAccessTokenService.getAccessToken(endpointSetIdentifier));
+ UserContextHolder.getUserContext()
+ .map(UserContext::getUser)
+ .ifPresent(user -> template.header(ApiConstants.USER_HEADER, user));
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/service/ApplicationAccessTokenService.java b/library/src/main/java/io/mifos/permittedfeignclient/service/ApplicationAccessTokenService.java
new file mode 100644
index 0000000..b97b4dd
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/service/ApplicationAccessTokenService.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.service;
+
+import io.mifos.anubis.config.TenantSignatureRepository;
+import io.mifos.anubis.security.AmitAuthenticationException;
+import io.mifos.anubis.token.TenantRefreshTokenSerializer;
+import io.mifos.anubis.token.TokenSerializationResult;
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.lang.ApplicationName;
+import io.mifos.core.lang.TenantContextHolder;
+import io.mifos.core.lang.security.RsaKeyPairFactory;
+import io.mifos.identity.api.v1.client.IdentityManager;
+import io.mifos.identity.api.v1.domain.Authentication;
+import net.jodah.expiringmap.ExpirationPolicy;
+import net.jodah.expiringmap.ExpiringMap;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Nonnull;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Myrle Krantz
+ */
+@Component
+public class ApplicationAccessTokenService {
+ private static final long REFRESH_TOKEN_LIFESPAN = TimeUnit.SECONDS.convert(1, TimeUnit.MINUTES);
+ private static class TokenCacheKey {
+ final String user;
+ final String tenant;
+ final String endpointSet;
+
+ private TokenCacheKey(final String user, final String tenant, final String endpointSet) {
+ this.user = user;
+ this.tenant = tenant;
+ this.endpointSet = endpointSet;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TokenCacheKey that = (TokenCacheKey) o;
+ return Objects.equals(user, that.user) &&
+ Objects.equals(tenant, that.tenant) &&
+ Objects.equals(endpointSet, that.endpointSet);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(user, tenant, endpointSet);
+ }
+
+ @Override
+ public String toString() {
+ return "TokenCacheKey{" +
+ "user='" + user + '\'' +
+ ", tenant='" + tenant + '\'' +
+ ", endpointSet='" + endpointSet + '\'' +
+ '}';
+ }
+ }
+
+ private final String applicationName;
+ private final TenantSignatureRepository tenantSignatureRepository;
+ private final IdentityManager identityManager;
+ private final TenantRefreshTokenSerializer tenantRefreshTokenSerializer;
+
+ private final Map<TokenCacheKey, TokenSerializationResult> refreshTokenCache;
+ private final Map<TokenCacheKey, Authentication> accessTokenCache;
+
+ @Autowired
+ public ApplicationAccessTokenService(
+ final @Nonnull ApplicationName applicationName,
+ final @Nonnull TenantSignatureRepository tenantSignatureRepository,
+ final @Nonnull IdentityManager identityManager,
+ final @Nonnull TenantRefreshTokenSerializer tenantRefreshTokenSerializer) {
+
+ this.applicationName = applicationName.toString();
+ this.tenantSignatureRepository = tenantSignatureRepository;
+ this.identityManager = identityManager;
+ this.tenantRefreshTokenSerializer = tenantRefreshTokenSerializer;
+
+ this.refreshTokenCache = ExpiringMap.builder()
+ .maxSize(300)
+ .expirationPolicy(ExpirationPolicy.CREATED)
+ .expiration(30, TimeUnit.SECONDS)
+ .entryLoader(tokenCacheKey -> this.createRefreshToken((TokenCacheKey)tokenCacheKey))
+ .build();
+ this.accessTokenCache = ExpiringMap.builder()
+ .maxSize(300)
+ .expirationPolicy(ExpirationPolicy.CREATED)
+ .expiration(30, TimeUnit.SECONDS)
+ .entryLoader(tokenCacheKey -> this.createAccessToken((TokenCacheKey)tokenCacheKey))
+ .build();
+ }
+
+ public String getAccessToken(final String endpointSetIdentifier) {
+ final TokenCacheKey tokenCacheKey
+ = new TokenCacheKey(UserContextHolder.checkedGetUser(), TenantContextHolder.checkedGetIdentifier(), endpointSetIdentifier);
+ final Authentication authentication = accessTokenCache.get(tokenCacheKey);
+ return authentication.getAccessToken();
+ }
+
+ private Authentication createAccessToken(final TokenCacheKey tokenCacheKey) {
+ final String refreshToken = refreshTokenCache.get(tokenCacheKey).getToken();
+ return identityManager.refresh(refreshToken);
+ }
+
+ private TokenSerializationResult createRefreshToken(final TokenCacheKey tokenCacheKey) {
+ final Optional<RsaKeyPairFactory.KeyPairHolder> optionalSigningKeyPair
+ = tenantSignatureRepository.getLatestApplicationSigningKeyPair();
+
+ final RsaKeyPairFactory.KeyPairHolder signingKeyPair = optionalSigningKeyPair.orElseThrow(AmitAuthenticationException::missingTenant);
+
+ final TenantRefreshTokenSerializer.Specification specification = new TenantRefreshTokenSerializer.Specification()
+ .setSourceApplication(applicationName)
+ .setUser(tokenCacheKey.user)
+ .setSecondsToLive(REFRESH_TOKEN_LIFESPAN)
+ .setPrivateKey(signingKeyPair.privateKey())
+ .setKeyTimestamp(signingKeyPair.getTimestamp())
+ .setEndpointSet(tokenCacheKey.endpointSet);
+
+ return tenantRefreshTokenSerializer.build(specification);
+ }
+}
diff --git a/library/src/main/java/io/mifos/permittedfeignclient/service/ApplicationPermissionRequirementsService.java b/library/src/main/java/io/mifos/permittedfeignclient/service/ApplicationPermissionRequirementsService.java
new file mode 100644
index 0000000..a0450d3
--- /dev/null
+++ b/library/src/main/java/io/mifos/permittedfeignclient/service/ApplicationPermissionRequirementsService.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.service;
+
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.anubis.annotation.Permittables;
+import io.mifos.anubis.api.v1.domain.AllowedOperation;
+import io.mifos.identity.api.v1.domain.Permission;
+import io.mifos.permittedfeignclient.annotation.EndpointSet;
+import io.mifos.permittedfeignclient.api.v1.domain.ApplicationPermission;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+@Component
+public class ApplicationPermissionRequirementsService{
+ private final Class[] classes;
+
+ @Autowired
+ public ApplicationPermissionRequirementsService(final Class[] classes) {
+ this.classes = classes;
+ }
+
+ public List<ApplicationPermission> getRequiredPermissions() {
+ return Stream.of(classes)
+ .flatMap(ApplicationPermissionRequirementsService::getApplicationPermissionsFromInterface)
+ .collect(Collectors.toList());
+ }
+
+ static Stream<ApplicationPermission> getApplicationPermissionsFromInterface(
+ final Class permissionRequestedFeignInterface) {
+ final Map<String, List<Permission>> grouped = getPermissionsFromInterface(permissionRequestedFeignInterface)
+ .collect(Collectors.groupingBy(Permission::getPermittableEndpointGroupIdentifier));
+ return grouped.entrySet().stream().map(x -> getApplicationPermissionfromEntry(permissionRequestedFeignInterface, x));
+ }
+
+ private static <T> ApplicationPermission getApplicationPermissionfromEntry(final Class<T> permissionRequestedFeignInterface,
+ final Map.Entry<String, List<Permission>> entry) {
+ final Optional<EndpointSet> permissionRequiredForAnnotation
+ = Optional.ofNullable(permissionRequestedFeignInterface.getAnnotation(EndpointSet.class));
+
+ final String permittableGroupId = permissionRequiredForAnnotation.map(EndpointSet::identifier)
+ .orElse(null);
+ final Permission permission = getPermissionfromEntry(entry);
+ return new ApplicationPermission(permittableGroupId, permission);
+ }
+
+ private static Permission getPermissionfromEntry(final Map.Entry<String, List<Permission>> entry) {
+ final Set<AllowedOperation> allowedOperations = entry.getValue().stream().flatMap(x -> x.getAllowedOperations().stream()).collect(Collectors.toSet());
+ return new Permission(entry.getKey(), allowedOperations);
+ }
+
+ private static Stream<Permission> getPermissionsFromInterface(final Class permissionRequestedFeignInterface) {
+ final Method[] methods = permissionRequestedFeignInterface.getMethods();
+ return Stream.of(methods)
+ .filter((method) -> method.isAnnotationPresent(Permittables.class) || method.isAnnotationPresent(Permittable.class))
+ .flatMap(ApplicationPermissionRequirementsService::extractPermissionsFromMethod);
+ }
+
+ static private Stream<Permission> extractPermissionsFromMethod(final Method method) {
+ final Permittables permittablesAnnotation = method.getAnnotation(Permittables.class);
+ final Permittable[] permittables;
+ if (permittablesAnnotation != null)
+ permittables = permittablesAnnotation.value();
+ else {
+ final Permittable permittableAnnotation = method.getAnnotation(Permittable.class);
+ permittables = new Permittable[]{permittableAnnotation};
+ }
+ return Stream.of(permittables)
+ .map(x -> mapPermittableToPermission(x, method));
+ }
+
+ static private Permission mapPermittableToPermission(final Permittable permittable,
+ final Method method) {
+ final RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
+ final RequestMethod[] httpMethods = requestMapping.method();
+
+ return new Permission(permittable.groupId(), mapMethodsToAllowedOperations(httpMethods));
+ }
+
+ static private Set<AllowedOperation> mapMethodsToAllowedOperations(final RequestMethod[] httpMethods) {
+ return Stream.of(httpMethods)
+ .map(ApplicationPermissionRequirementsService::mapRequestMethodToAllowedOperation)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+ }
+
+ static private Optional<AllowedOperation> mapRequestMethodToAllowedOperation(final RequestMethod requestMethod) {
+ switch (requestMethod) {
+ case GET:
+ return Optional.of(AllowedOperation.READ);
+ case HEAD:
+ return Optional.of(AllowedOperation.READ);
+ case POST:
+ return Optional.of(AllowedOperation.CHANGE);
+ case PUT:
+ return Optional.of(AllowedOperation.CHANGE);
+ case PATCH:
+ return Optional.of(AllowedOperation.CHANGE);
+ case DELETE:
+ return Optional.of(AllowedOperation.DELETE);
+ default:
+ case OPTIONS:
+ case TRACE:
+ return Optional.empty();
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/test/java/io/mifos/permittedfeignclient/service/ApplicationAccessTokenServiceTest.java b/library/src/test/java/io/mifos/permittedfeignclient/service/ApplicationAccessTokenServiceTest.java
new file mode 100644
index 0000000..c940668
--- /dev/null
+++ b/library/src/test/java/io/mifos/permittedfeignclient/service/ApplicationAccessTokenServiceTest.java
@@ -0,0 +1,62 @@
+package io.mifos.permittedfeignclient.service;
+
+import io.mifos.anubis.config.TenantSignatureRepository;
+import io.mifos.anubis.token.TenantRefreshTokenSerializer;
+import io.mifos.anubis.token.TokenSerializationResult;
+import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.lang.ApplicationName;
+import io.mifos.core.lang.AutoTenantContext;
+import io.mifos.core.lang.security.RsaKeyPairFactory;
+import io.mifos.identity.api.v1.client.IdentityManager;
+import io.mifos.identity.api.v1.domain.Authentication;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ApplicationAccessTokenServiceTest {
+ private static final String APP_NAME = "app-v1";
+ private static final String BEARER_TOKEN_MOCK = "bearer token mock";
+ private static final String USER_NAME = "user";
+ private static final String TENANT_NAME = "tenant";
+ private static final String BEARER_INCOMING_ACCESS_TOKEN_MOCK = "bearer incoming access token mock";
+
+ @Test
+ public void testHappyCase() {
+ final ApplicationName applicationNameMock = Mockito.mock(ApplicationName.class);
+ Mockito.when(applicationNameMock.toString()).thenReturn(APP_NAME);
+
+ final TenantSignatureRepository tenantSignatureRepositoryMock = Mockito.mock(TenantSignatureRepository.class);
+ final Optional<RsaKeyPairFactory.KeyPairHolder> keyPair = Optional.of(RsaKeyPairFactory.createKeyPair());
+ Mockito.when(tenantSignatureRepositoryMock.getLatestApplicationSigningKeyPair()).thenReturn(keyPair);
+
+ final IdentityManager identityManagerMock = Mockito.mock(IdentityManager.class);
+ Mockito.when(identityManagerMock.refresh(Mockito.anyString()))
+ .thenReturn(new Authentication(BEARER_TOKEN_MOCK, "accesstokenexpiration", "refreshtokenexpiration", null));
+
+ final TenantRefreshTokenSerializer tenantRefreshTokenSerializerMock = Mockito.mock(TenantRefreshTokenSerializer.class);
+ Mockito.when(tenantRefreshTokenSerializerMock.build(Mockito.anyObject()))
+ .thenReturn(new TokenSerializationResult(BEARER_TOKEN_MOCK, LocalDateTime.now()));
+
+ final ApplicationAccessTokenService testSubject = new ApplicationAccessTokenService(
+ applicationNameMock,
+ tenantSignatureRepositoryMock,
+ identityManagerMock,
+ tenantRefreshTokenSerializerMock);
+
+ try (final AutoTenantContext ignored1 = new AutoTenantContext(TENANT_NAME)) {
+ try (final AutoUserContext ignored2 = new AutoUserContext(USER_NAME, BEARER_INCOMING_ACCESS_TOKEN_MOCK)) {
+ final String accessToken = testSubject.getAccessToken("blah");
+ Assert.assertEquals(BEARER_TOKEN_MOCK, accessToken);
+
+ final String accessTokenAgain = testSubject.getAccessToken("blah");
+ Assert.assertEquals(BEARER_TOKEN_MOCK, accessTokenAgain);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/test/java/io/mifos/permittedfeignclient/service/ApplicationPermissionRequirementsServiceTest.java b/library/src/test/java/io/mifos/permittedfeignclient/service/ApplicationPermissionRequirementsServiceTest.java
new file mode 100644
index 0000000..1de7734
--- /dev/null
+++ b/library/src/test/java/io/mifos/permittedfeignclient/service/ApplicationPermissionRequirementsServiceTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.permittedfeignclient.service;
+
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.anubis.api.v1.domain.AllowedOperation;
+import io.mifos.identity.api.v1.domain.Permission;
+import io.mifos.permittedfeignclient.annotation.EndpointSet;
+import io.mifos.permittedfeignclient.api.v1.domain.ApplicationPermission;
+import org.junit.Assert;
+import org.junit.Test;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ApplicationPermissionRequirementsServiceTest {
+ @FeignClient
+ @EndpointSet(identifier = "z")
+ interface SampleWithPermissionRequiredFor {
+ @RequestMapping(method = RequestMethod.GET)
+ @Permittable(groupId = "x")
+ void getFoo();
+
+ @RequestMapping(method = RequestMethod.PUT)
+ @Permittable(groupId = "x")
+ @Permittable(groupId = "y")
+ void getBar();
+
+ @RequestMapping(method = RequestMethod.HEAD)
+ @Permittable(groupId = "m")
+ void headBar();
+
+ @RequestMapping(method = RequestMethod.DELETE)
+ @Permittable(groupId = "n")
+ void deleteBar();
+
+ @RequestMapping(method = RequestMethod.POST)
+ @Permittable(groupId = "o")
+ void postBar();
+
+ @RequestMapping(method = RequestMethod.PATCH)
+ @Permittable(groupId = "p")
+ void patchBar();
+ }
+
+ @Test
+ public void shouldReturnApplicationPermissionWithRequiredForPermittableGroup() throws Exception {
+ final ApplicationPermissionRequirementsService testSubject = new ApplicationPermissionRequirementsService(new Class[]{ SampleWithPermissionRequiredFor.class} );
+ final Set<ApplicationPermission> applicationPermissions =
+ testSubject.getRequiredPermissions().stream().collect(Collectors.toSet());
+
+ Assert.assertTrue(applicationPermissions.contains(new ApplicationPermission("z",
+ new Permission("x", Stream.of(AllowedOperation.READ, AllowedOperation.CHANGE).collect(Collectors.toSet())))));
+ Assert.assertTrue(applicationPermissions.contains(new ApplicationPermission("z",
+ new Permission("y", Collections.singleton(AllowedOperation.CHANGE)))));
+ Assert.assertTrue(applicationPermissions.contains(new ApplicationPermission("z",
+ new Permission("m", Collections.singleton(AllowedOperation.READ)))));
+ Assert.assertTrue(applicationPermissions.contains(new ApplicationPermission("z",
+ new Permission("n", Collections.singleton(AllowedOperation.DELETE)))));
+ Assert.assertTrue(applicationPermissions.contains(new ApplicationPermission("z",
+ new Permission("o", Collections.singleton(AllowedOperation.CHANGE)))));
+ Assert.assertTrue(applicationPermissions.contains(new ApplicationPermission("z",
+ new Permission("p", Collections.singleton(AllowedOperation.CHANGE)))));
+ }
+
+ @FeignClient
+ interface SampleWithoutPermissionRequiredFor {
+ @RequestMapping(method = RequestMethod.GET)
+ @Permittable(groupId = "x")
+ void getFoo();
+ }
+
+ @Test
+ public void shouldReturnApplicationPermissionWithoutRequiredForPermittableGroup() throws Exception {
+ final Set<ApplicationPermission> applicationPermissions =
+ ApplicationPermissionRequirementsService.getApplicationPermissionsFromInterface(SampleWithoutPermissionRequiredFor.class)
+ .collect(Collectors.toSet());
+
+ Assert.assertTrue(applicationPermissions.contains(new ApplicationPermission(null,
+ new Permission("x", Collections.singleton(AllowedOperation.READ)))));
+ }
+
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..f43e401
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,6 @@
+rootProject.name = 'permitted-feign-client'
+
+includeBuild 'api'
+includeBuild 'library'
+includeBuild 'another-for-test'
+includeBuild 'component-test'
diff --git a/shared.gradle b/shared.gradle
new file mode 100644
index 0000000..ca25a25
--- /dev/null
+++ b/shared.gradle
@@ -0,0 +1,79 @@
+group 'io.mifos.permitted-feign-client'
+version '0.1.0-BUILD-SNAPSHOT'
+
+apply plugin: 'java'
+apply plugin: 'idea'
+apply plugin: 'maven-publish'
+apply plugin: 'io.spring.dependency-management'
+
+
+ext.versions = [
+ frameworktest : '0.1.0-BUILD-SNAPSHOT',
+ frameworkapi : '0.1.0-BUILD-SNAPSHOT',
+ frameworkcassandra : '0.1.0-BUILD-SNAPSHOT',
+ frameworklang : '0.1.0-BUILD-SNAPSHOT',
+ frameworkidentity : '0.1.0-BUILD-SNAPSHOT',
+ frameworkanubis : '0.1.0-BUILD-SNAPSHOT',
+ frameworkservicestarter : '0.1.0-BUILD-SNAPSHOT',
+ jjwt : '0.6.0',
+ hibernatevalidator : '5.3.0.Final',
+ expiringmap : '0.5.8'
+]
+
+tasks.withType(JavaCompile) {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+repositories {
+ jcenter()
+ mavenLocal()
+}
+
+configurations {
+ compile.exclude group: 'io.mifos.core', module: 'mariadb'
+ compile.exclude group: 'ch.vorburger', module: 'mariaDB4j'
+}
+
+dependencyManagement {
+ imports {
+ mavenBom 'io.spring.platform:platform-bom:Athens-RELEASE'
+ mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Camden.SR1'
+ }
+}
+
+// override certain dependency provided by Spring platform using newer releases
+ext['cassandra.version'] = '3.6'
+ext['cassandra-driver.version'] = '3.1.2'
+ext['activemq.version'] = '5.13.2'
+ext['spring-data-releasetrain.version'] = 'Gosling-SR2A'
+
+dependencies {
+ compile(
+ [group: 'com.google.code.findbugs', name: 'jsr305']
+ )
+
+ testCompile(
+ [group: 'org.springframework.boot', name: 'spring-boot-starter-test'],
+ [group: 'io.mifos.core', name: 'test', version: versions.frameworktest],
+ )
+}
+
+jar {
+ from sourceSets.main.allSource
+}
+
+license {
+ header rootProject.file('../HEADER')
+ strictCheck true
+ mapping {
+ java = 'SLASHSTAR_STYLE'
+ xml = 'XML_STYLE'
+ yml = 'SCRIPT_STYLE'
+ yaml = 'SCRIPT_STYLE'
+ }
+ ext.year = Calendar.getInstance().get(Calendar.YEAR)
+ ext.name = 'The Mifos Initiative'
+}
+
+task ci(dependsOn: ['clean', 'test', 'publish'])