Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smart Configuration #186

Closed
wants to merge 29 commits into from
Closed

Conversation

VaishSiddharth
Copy link
Contributor

Description of what I changed

Issue I worked on

see https://issues.openmrs.org/browse/FM2-

Checklist: I completed these to help reviewers :)

  • My IDE is configured to follow the code style of this project.

    No? Unsure? -> configure your IDE, format the code and add the changes with git add . && git commit --amend

  • I have added tests to cover my changes. (If you refactored
    existing code that was well tested you do not have to add tests)

    No? -> write tests and add them to this commit git add . && git commit --amend

  • I ran mvn clean package right before creating this pull request and
    added all formatting changes to my commit.

    No? -> execute above command

  • All new and existing tests passed.

    No? -> figure out why and add the fix to your commit. It is your responsibility to make sure your code works.

  • My pull request is based on the latest changes of the master branch.

    No? Unsure? -> execute command git pull --rebase upstream master

@codecov
Copy link

codecov bot commented May 27, 2020

Codecov Report

Merging #186 into master will decrease coverage by 1.78%.
The diff coverage is 27.92%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master     #186      +/-   ##
============================================
- Coverage     80.82%   79.04%   -1.78%     
- Complexity     1482     1497      +15     
============================================
  Files           161      167       +6     
  Lines          3812     3937     +125     
  Branches        497      516      +19     
============================================
+ Hits           3081     3112      +31     
- Misses          508      600      +92     
- Partials        223      225       +2     
Impacted Files Coverage Δ Complexity Δ
.../module/fhir2/web/filter/AuthenticationFilter.java 0.00% <0.00%> (ø) 0.00 <0.00> (ø)
.../openmrs/module/fhir2/web/SmartWebInitializer.java 0.00% <0.00%> (ø) 0.00 <0.00> (?)
...le/fhir2/web/filter/SmartAuthenticationFilter.java 0.00% <0.00%> (ø) 0.00 <0.00> (?)
...ule/fhir2/web/smart/SmartAuthenticationScheme.java 0.00% <0.00%> (ø) 0.00 <0.00> (?)
.../module/fhir2/web/smart/SmartTokenCredentials.java 0.00% <0.00%> (ø) 0.00 <0.00> (?)
...nmrs/module/fhir2/web/filter/ForwardingFilter.java 68.97% <70.59%> (-10.20%) 5.00 <0.00> (ø)
...org/openmrs/module/fhir2/web/SmartConformance.java 90.91% <90.91%> (ø) 10.00 <10.00> (?)
...mrs/module/fhir2/api/dao/impl/FhirUserDaoImpl.java 100.00% <100.00%> (ø) 4.00 <2.00> (+1.00)
...dule/fhir2/web/servlet/FhirSmartConfigServlet.java 100.00% <100.00%> (ø) 3.00 <3.00> (?)
... and 5 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d30e7b0...e49ffe9. Read the comment docs.

Comment on lines 67 to 77
<filter-mapping>
<filter-name>clientAuthenticationFilter</filter-name>
<url-pattern>/keycloak/*</url-pattern>
<url-pattern>/protected/*</url-pattern>
</filter-mapping>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what URLs do we actually want to protect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher I am not very sure about it that's why I left it unchanged

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The url-pattern determines which URLs this filter is run against. It should probably look very similar to the AuthenticationFilter, since they're ultimately protecting the same thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you think that it'll be different than that in AuthenticationFilter because earlier we did not have any client (SMART App), we only had users

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because we're still protecting the same resources either way. That's what's important here.

Comment on lines 38 to 68
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
super.doFilter(req, res, chain);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher Could you give some idea on what all should go in here as most of the code that we might need is already present in the super function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what we need to do is to figure out the user name. From here, it looks like we can do that by getting the appropriate request attribute.

Then, we need to login the user. This can be done by running Context.becomeUser() with the user id (which we can hopefully obtain from the account.

@@ -67,7 +67,11 @@
<filter-mapping>
<filter-name>clientAuthenticationFilter</filter-name>
<url-pattern>/keycloak/*</url-pattern>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You definitely don't need this URL pattern. A URL pattern is just matched against the incoming URL without the host (http://localhost) or context (/openmrs), and OpenMRS doesn't have anything that responds to /keycloak.

Comment on lines 47 to 66
if (!httpRequest.getAuthType().contains("BASIC")) {

super.doFilter(req, res, chain);
if (httpRequest.getRequestedSessionId() != null && !httpRequest.isRequestedSessionIdValid()) {
Context.logout();
}
if (httpRequest.getAttribute(KeycloakAccount.class.getName()) != null) {
OidcKeycloakAccount account = (OidcKeycloakAccount) httpRequest
.getAttribute(KeycloakAccount.class.getName());
Context.becomeUser(account.getPrincipal().getName());
} else {
if (!res.isCommitted()) {
HttpServletResponse httpResponse = (HttpServletResponse) res;
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
return;
}
}
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher This gives an NPE but don't know why it makes sense to me. The super function will always commit to response thats why I thought to write something like this.

Error log

SEVERE: Servlet.service() for servlet [openmrs] in context with path [/openmrs] threw exception
java.lang.NullPointerException
	at org.openmrs.module.fhir2.web.filter.ClientAuthenticationFilter.doFilter(ClientAuthenticationFilter.java:47)
	at org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)
	at org.openmrs.module.referenceapplication.filter.RequireLoginLocationFilter.doFilter(RequireLoginLocationFilter.java:93)
	at org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)
	at org.openmrs.module.web.filter.ModuleFilter.doFilter(ModuleFilter.java:57)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)

The basic auth also doesn't works, neither for the last commit nor this one

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what part of the code this is referring to exactly, because I think the line numbers have shifted?

Copy link
Member

@ibacher ibacher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VaishSiddharth Thanks for your work on this! These changes are looking really goood! I've made a few comments, but this feels like it's starting to come together.

@Data
public class SmartConformance {

private String authorization_endpoint;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't have to be dealt with straight-away, but these should be camelCase, i.e., authorizationEndpoint rather than authorization_endpoint. We can then translate them to the appropriate name using an annotation, i.e.,

@JsonProperty("authorization_endpoint")
private String authorizationEndpoint;

Comment on lines 70 to 73
@Override
public void destroy() {
super.destroy();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably get rid of this method.


if (requestURI.contains("/.well-known")) {
replacement = "/ms/fhir2SmartConfig";
}
String newURI = requestURI.replace(prefix, replacement);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a new line before here.

} else {
((HttpServletResponse) res).sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think we should have a new line here

@@ -53,13 +53,17 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
} else if (R4.equals(fhirVersionCase)) {
prefix += "/" + fhirVersion;
replacement = "/ms/fhir2Servlet";

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new line should not exist

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
if (req instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) req;
if (!httpRequest.getAuthType().contains("BASIC")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be:

if (httpRequest.getAuthType() != null && !httpRequest.getAuthType().equals(HttpServletRequest.BASIC_AUTH))

Comment on lines 29 to 31
ObjectMapper objectMapper = new ObjectMapper();

SmartConformance smartConformance = new SmartConformance();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason these can't be class objects? I.e., so we only have one objectMapper and one smartConformance per servlet, rather than creating new ones for every request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher Not sure about what should be done here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh... I just meant turn them into class properties:

class Fhir2SmartConfig extends HttpServlet {
    private static final ObjectMapper objectMapper = new ObjectManager();

    private final SmartConformance smartConformance;

    public Fhir2SmartConfig() {
        this.smartConformance = new SmartConformance();
        // set properties here
    }

    public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        PrintWriter out = res.getWriter();
		res.setContentType("application/json");
		res.setCharacterEncoding("UTF-8");
		res.setStatus(200);
		objectMapper.writeValue(out, smartConformance);
        // why do we need these next two lines?
		out.print(objectMapper);
		out.flush();
    }
}

OidcKeycloakAccount account = (OidcKeycloakAccount) httpRequest
.getAttribute(KeycloakAccount.class.getName());

String userName = account.getKeycloakSecurityContext().getToken().getSubject();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bacher This still give the internal keycloak user id

This solves the error.

String userName = account.getKeycloakSecurityContext().getToken().getPreferredUsername();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, use that.

Comment on lines 98 to 113
if (httpRequest.getContextPath() != null) {
newRequestUri.append(httpRequest.getContextPath());
}

if (httpRequest.getServletPath() != null) {
newRequestUri.append(httpRequest.getServletPath());
}

if (httpRequest.getPathInfo() != null) {
newRequestUri.append(httpRequest.getPathInfo());
}

newRequestUri.append('?').append(newQueryString);

req.getRequestDispatcher(newRequestUri.toString()).forward(req, res);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher Don't you think that we still need the ForwardingFilter to execute?

Also there is a mistake in the url formation it adds /openmrs/openmrs two times

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't intended to override the ForwardingFilter, only to remove the access_token from the query before it hits HAPI. Note that this "forwarding" is really just the way to internally change the URL without having to worry about too many details.

Also there is a mistake in the url formation it adds /openmrs/openmrs two times

That clearly needs to be fixed.

Copy link
Member

@ibacher ibacher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, we can't support config.xml in a non-OMOD submodule. Which does mean that we're going to need another means of loading the SMART filter. I'll have to give that some thought.

omod/pom.xml Outdated
@@ -38,7 +38,7 @@
</dependency>
<dependency>
<groupId>${project.parent.groupId}</groupId>
<artifactId>smart</artifactId>
<artifactId>smartSupport</artifactId>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably call this smart-support

@@ -9,8 +9,8 @@
graphic logo is a trademark of OpenMRS Inc.
-->
<dataset>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is part of this commit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher Earlier I had changed this so that the tests pass for openmrsPlaformVersion 2.3.0. So I reverted it back to this after creating a new sub module

@@ -310,7 +310,7 @@ public void searchForPatients_shouldSearchForPatientsByBirthDateWithLowerBound()

assertThat(results, notNullValue());
assertThat(get(results), not(empty()));
assertThat(get(results), hasSize(equalTo(1)));
assertThat(get(results), hasSize(greaterThan(1)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is part of this commit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher Earlier I had changed this so that the tests pass for openmrsPlaformVersion 2.3.0. So I reverted it back to this after creating a new sub module

pom.xml Outdated
@@ -657,7 +657,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputEncoding>UTF-8</project.build.outputEncoding>
<lombokVersion>1.18.10</lombokVersion>
<openmrsPlatformVersion>2.3.0</openmrsPlatformVersion>
<openmrsPlatformVersion>2.0.2</openmrsPlatformVersion>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be 2.0.5. Also, please ensure your IDE is configured to indent XML files with tabs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok will make this change

Comment on lines 56 to 60
catch (Exception ignored) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
if (!response.isCommitted()) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
}
return;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher The only error that is left now is this, even on giving the correct password and username for basic auth the catch block is executed with invalid credentials error

I guess that there is some problem with the authentication scheme I mean either its not set or it is changed because of the keycloak filter. I can say this because I copy pasted the same AuthenticationFilter code to the master branch and tried it with the same request and it worked perfectly fine.

Copy link
Member

@ibacher ibacher Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VaishSiddharth This change should be made outside of this PR, since it's outside of the scope of the SMART stuff. I'm aware that's not really the question, but to see why this is failing, this really needs to be stepped through with a debugger.

Comment on lines 44 to 48

if (!(credentials instanceof SmartTokenCredentials)) {
throw new ContextAuthenticationException("Invalid credentials");
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibacher I figured out that the error is because of this part. I tried changing it to

if (!(credentials instanceof SmartTokenCredentials)) {
			return new BasicAuthenticated(dao.getUserByUserName(credentials.getClientName()),
			        UsernamePasswordCredentials.SCHEME);
		}

But it didn't work.

Could you help me with this?

@ibacher
Copy link
Member

ibacher commented Jun 18, 2020

Closing this as we are moving this support to a new module.

@ibacher ibacher closed this Jun 18, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants