Skip to content
This repository was archived by the owner on Jun 23, 2022. It is now read-only.

DB Fix bug#67471678. Include ACLs in the db.singleDocContentSql#110

Open
srinicodebytes wants to merge 11 commits intomasterfrom
sri-acls
Open

DB Fix bug#67471678. Include ACLs in the db.singleDocContentSql#110
srinicodebytes wants to merge 11 commits intomasterfrom
sri-acls

Conversation

@srinicodebytes
Copy link
Copy Markdown
Contributor

No description provided.

@googlebot
Copy link
Copy Markdown

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed, please reply here (e.g. I signed it!) and we'll verify. Thanks.


  • If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address. Check your existing CLA data and verify that your email is set on your git commits.
  • If your company signed a CLA, they designated a Point of Contact who decides which employees are authorized to participate. You may need to contact the Point of Contact for your company and ask to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the project maintainer to go/cla#troubleshoot.
  • In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again.

Copy link
Copy Markdown
Contributor

@PapaJoltOmega PapaJoltOmega left a comment

Choose a reason for hiding this comment

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

Thank you.

aclPrincipalDelimiter = cfg.getValue("db.aclPrincipalDelimiter");
log.config("aclPrincipalDelimiter: '" + aclPrincipalDelimiter + "'");
}
aclPrincipalDelimiter = cfg.getValue("db.aclPrincipalDelimiter");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add comment
// we might have delimiter and no aclSql as ACLs might come from singleDocContentSql rather than aclSql

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

if (aclSql != null) {
resp.setAcl(getAcl(conn, id.getUniqueId()));
} else {
resp.setAcl(buildAcl(rs, aclPrincipalDelimiter, aclNamespace,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add comment
// see if ACL came as part of result to singleDocContentSql query

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Nov 29, 2017

Choose a reason for hiding this comment

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

Done.

denyGroups.addAll(getGroupPrincipalsFromResultSet(rs,
GsaSpecialColumns.GSA_DENY_GROUPS, delim, namespace));
}
if (rsNext == ResultSetNext.STOP_NEXT) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please help me understand the constraint that ACL coming from singleDocContentSql imposes that requires this statement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We shouldn't be calling rs.next() when processing ACLs from singleDocContentSql, or else we would be moving to next or end of result set and wouldn't be able to get content from result set.

String uniqueId) throws SQLException {
PreparedStatement st = conn.prepareStatement(singleDocContentSql);
uniqueKey.setContentSqlValues(st, uniqueId);
uniqueKey.setContentSqlValues(st, uniqueId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

has something changed on this line? github shows it as changed.

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Nov 29, 2017

Choose a reason for hiding this comment

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

Looks like I removed the whitespace at the end. Restored it.

st = conn.prepareStatement(singleDocContentSql);
} else {
st = conn.prepareStatement(aclSql);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

indent looks 1off

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thx, fixed it.

CONTINUE_NEXT,
STOP_NEXT,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How about
PROCESS_ALL_RESULTS
PROCESS_ONE_RESULT

Am I understanding the semantics?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thx, modified these.

}

@VisibleForTesting
static Acl buildAcl(ResultSet rs, String delim, String namespace)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Who calls this overload?

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Nov 29, 2017

Choose a reason for hiding this comment

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

Only tests currently. Moved this to tests.

if (aclSql != null) {
resp.setAcl(getAcl(conn, id.getUniqueId()));
} else {
resp.setAcl(buildAcl(rs, aclPrincipalDelimiter, aclNamespace,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like it's going to set the empty ACL (an explicitly constructed one, rather than Acl.EMPTY) on a document that should be public.

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Nov 29, 2017

Choose a reason for hiding this comment

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

Thx, fixed it by returning Acl.EMPTY when no acl info in the result

String uniqueId) throws SQLException {
PreparedStatement st = conn.prepareStatement(singleDocContentSql);
uniqueKey.setContentSqlValues(st, uniqueId);
uniqueKey.setContentSqlValues(st, uniqueId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restore trailing whitespace on this unmodified line.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

PreparedStatement st = conn.prepareStatement(aclSql);
PreparedStatement st;
if (aclSql == null) {
st = conn.prepareStatement(singleDocContentSql);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems worth a comment here that this only happens when called from isUserAuthorized. Also, we will execute db.singleDocContentSql twice in that case. I suppose that's inevitable, but did you think about trying to avoid that?

}

enum ResultSetNext {
CONTINUE_NEXT,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What do you think about ALL_ROWS, ONE_ROW?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Combining your and PJ's suggestion, modified to PROCESS_ALL_ROWS , PROCESS_ONE_ROW.

+ GsaSpecialColumns.GSA_PERMIT_USERS + " varchar(20),"
+ GsaSpecialColumns.GSA_DENY_GROUPS + " varchar(20),"
+ GsaSpecialColumns.GSA_DENY_USERS + " varchar(20))");
executeUpdate("insert into data("
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test should minimally have 2 rows as a test of STOP_NEXT.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thx, added another row

.build();
ResultSet rs = executeQuery("select * from data");
rs.next();
Acl acl = DatabaseAdaptor.buildAcl(rs, ",", DEFAULT_NAMESPACE,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There are only six tests that use buildAcl instead of getDocContent. I think we should convert all of them, perhaps in a separate CL. Perhaps minimally we could use getDocContent for this new test. We have more logic around db.aclSql now, and I don't like bypassing that in the tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree. Modified this test to call getDocContent instead of buildAcl.


if (!hasPermitUsers && !hasDenyUsers
&& !hasPermitGroups && !hasDenyGroups) {
return Acl.EMPTY;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Whether implicit or explicit, the empty ACL is wrong in the case of getting ACLs from db.singleDocContentSql. I think we could just add another argument for the default ACL to use here, either null or Acl.EMPTY. These values are paired with the ResultSetNext enum, so we could conceivably collapse them into a single argument, but that feels a little obfuscated.

Since this is a bug, albeit newly introduced, you should add a test for it.

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Nov 30, 2017

Choose a reason for hiding this comment

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

Done. Added test for it.

GsaSpecialColumns.GSA_TIMESTAMP));
}

private Acl buildAcl(ResultSet rs, String delim, String namespace)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would have left this alone with a TODO, since we're planning to replace it entirely, or you could have used executeQueryAndNext instead of executeQuery. Just mentioning other ideas for posterity, since we're planning to sweep this away anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree. Keeping as is for now.


Map<String, String> configEntries = new HashMap<String, String>();
configEntries.put("db.uniqueKey", "id:int");
configEntries.put("db.everyDocIdSql", "select id from data");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Follow the boilerplate and put db.everyDocIdSql after the "Required for validation, ..." comment.

RecordingResponse response = new RecordingResponse();
adaptor.getDocContent(request, response);

Acl acl = response.getAcl();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's more consistent now, but if we're planning to use this as an exemplar, I think it's nicer to just inline this value in the assert.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

PreparedStatement st = conn.prepareStatement(aclSql);
PreparedStatement st;
if (aclSql == null) {
st = conn.prepareStatement(singleDocContentSql);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Forgotten comment here?

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Nov 30, 2017

Choose a reason for hiding this comment

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

Added comment. Done.

PreparedStatement st = conn.prepareStatement(singleDocContentSql);
uniqueKey.setContentSqlValues(st, uniqueId);
if (aclSql == null) {
uniqueKey.setAclSqlValues(st, uniqueId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems wrong. The db.aclSqlParameters columns are specific to db.aclSql, and may not be the same in db.singleDocContentSql. Did something fail without this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this is wrong, too, maybe add a test where doing this would fail, too.

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Nov 30, 2017

Choose a reason for hiding this comment

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

Deleted the newly added lines, as these are not required. And no test required.

private PreparedStatement getAclFromDb(Connection conn,
String uniqueId) throws SQLException {
PreparedStatement st = conn.prepareStatement(aclSql);
PreparedStatement st;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe simpler:

String sql = (aclSql == null) ? singleDocContentSql : aclSql;
PreparedStatement st = conn.prepareStatement(sql);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems a little redundant that we run this check before calling this function on line 681. I don't, at this time, have a better suggestion. Let's keep an eye out to see if there is a better way to keep the functions orthogonality composable. The current way is fine, but I will keep an eye out to see if there is something better. It is also possible that you are planning to revisit/refactor in follow up. Thank you

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, thx. Done.

Copy link
Copy Markdown
Contributor

@wiarlawd wiarlawd left a comment

Choose a reason for hiding this comment

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

Nits

denyGroups.addAll(getGroupPrincipalsFromResultSet(rs,
GsaSpecialColumns.GSA_DENY_GROUPS, delim, namespace));
}
if (rsNext == ResultSetNext.PROCESS_ONE_ROW) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Or maybe just:

} while (rsNext == ResultSetNext.PROCESS_ALL_ROWS && rs.next());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

RecordingResponse response = new RecordingResponse();
adaptor.getDocContent(request, response);

assertEquals(golden, response.getAcl());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The use of golden here seems silly. assertNull(response.getAcl()) is more clear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, Done.


Map<String, String> configEntries = new HashMap<String, String>();
configEntries.put("db.uniqueKey", "id:int");
configEntries.put("db.everyDocIdSql", "select id from data");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missed comment here: put this at the end of the configEntries, after a comment:

// Required for validation, but not specific to this test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

private PreparedStatement getAclFromDb(Connection conn,
String uniqueId) throws SQLException {
PreparedStatement st = conn.prepareStatement(aclSql);
// aclSql will be null when called from isUserAuthorized.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

s/will/will only/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

private PreparedStatement getAclFromDb(Connection conn,
String uniqueId) throws SQLException {
PreparedStatement st = conn.prepareStatement(aclSql);
PreparedStatement st;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems a little redundant that we run this check before calling this function on line 681. I don't, at this time, have a better suggestion. Let's keep an eye out to see if there is a better way to keep the functions orthogonality composable. The current way is fine, but I will keep an eye out to see if there is something better. It is also possible that you are planning to revisit/refactor in follow up. Thank you

boolean hasResult = rs.next();
if (!hasResult) {
// empty Acl ensures adaptor will mark this document as secure
return Acl.EMPTY;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why move this code from buildAcl?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because the other call to buildAcl from getDocContent using the content SQL doesn't ever want to call rs.next(). We could check rsNext at the top and bottom of the loop in buildAcl, which is messier and another double-check, but it's in one method.

@@ -697,18 +703,20 @@ private Acl getAcl(Connection conn, String uniqueId) throws SQLException {
try (PreparedStatement stmt = getAclFromDb(conn, uniqueId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps rename getAclFromDb to runAclSqlStatement (to disambiguate that it is not running content sql)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As I mentioned, I didn't entirely like the structure of getAclFromDb, but it has parallel methods for getDocFromDb and getStreamFromDb. All three methods just create a PreparedStatement, so a basic rename to prepare{Acl,Doc,Stream}Statement seems reasonable, but feels like a separate CL. Maybe we can finish this one and start a new pull request to massage the structure?

Also, these methods have functional utility for try-with-resources: we want to call prepareStatement and executeQuery but do something in between. So we either need these helper methods, or we need two nested try-with-resources.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, yes, that changed here afe59e4#diff-dd03438283161f00e652d4769de767bf

I agree that some structure/renaming changes are fitting.

I have made a client and am reading DatabaseAdaptor.java file to re-acquaint myself with it. I plan to read it on Monday and make some suggestions then.

Thank you

@@ -678,6 +680,10 @@ public void getDocContent(Request req, Response resp) throws IOException {
// Generate Acl if aclSql is provided.
if (aclSql != null) {
resp.setAcl(getAcl(conn, id.getUniqueId()));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps rename getAcl to getAclFromAclSql (to disambiguate that it is not running the content sql)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This call site for getAcl isn't running the content SQL, but when called from isUserAuthorized it is. I don't think that exposing the conditional from getAclFromDb in isUserAuthorized feels right. Perhaps better would be to bury this check for aclSql != null in getAcl as well, so that both call sites just always call getAcl, which figures out how to do the work.


if (!hasPermitUsers && !hasDenyUsers
&& !hasPermitGroups && !hasDenyGroups) {
return defaultAcl;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why do we need default acl parameter?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we have aclSql but no ACL entries, we want an empty ACL to mark the document secure. But if we have no aclSql and no ACL entries, then we want a null ACL to mark the document public.

Copy link
Copy Markdown
Contributor

@wiarlawd wiarlawd left a comment

Choose a reason for hiding this comment

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

Replies to PJ.

@@ -678,6 +680,10 @@ public void getDocContent(Request req, Response resp) throws IOException {
// Generate Acl if aclSql is provided.
if (aclSql != null) {
resp.setAcl(getAcl(conn, id.getUniqueId()));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This call site for getAcl isn't running the content SQL, but when called from isUserAuthorized it is. I don't think that exposing the conditional from getAclFromDb in isUserAuthorized feels right. Perhaps better would be to bury this check for aclSql != null in getAcl as well, so that both call sites just always call getAcl, which figures out how to do the work.

@@ -697,18 +703,20 @@ private Acl getAcl(Connection conn, String uniqueId) throws SQLException {
try (PreparedStatement stmt = getAclFromDb(conn, uniqueId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As I mentioned, I didn't entirely like the structure of getAclFromDb, but it has parallel methods for getDocFromDb and getStreamFromDb. All three methods just create a PreparedStatement, so a basic rename to prepare{Acl,Doc,Stream}Statement seems reasonable, but feels like a separate CL. Maybe we can finish this one and start a new pull request to massage the structure?

Also, these methods have functional utility for try-with-resources: we want to call prepareStatement and executeQuery but do something in between. So we either need these helper methods, or we need two nested try-with-resources.

boolean hasResult = rs.next();
if (!hasResult) {
// empty Acl ensures adaptor will mark this document as secure
return Acl.EMPTY;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because the other call to buildAcl from getDocContent using the content SQL doesn't ever want to call rs.next(). We could check rsNext at the top and bottom of the loop in buildAcl, which is messier and another double-check, but it's in one method.


if (!hasPermitUsers && !hasDenyUsers
&& !hasPermitGroups && !hasDenyGroups) {
return defaultAcl;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we have aclSql but no ACL entries, we want an empty ACL to mark the document secure. But if we have no aclSql and no ACL entries, then we want a null ACL to mark the document public.

Copy link
Copy Markdown
Contributor

@PapaJoltOmega PapaJoltOmega left a comment

Choose a reason for hiding this comment

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

Thanks for your thoughtful review John.

@@ -697,18 +703,20 @@ private Acl getAcl(Connection conn, String uniqueId) throws SQLException {
try (PreparedStatement stmt = getAclFromDb(conn, uniqueId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, yes, that changed here afe59e4#diff-dd03438283161f00e652d4769de767bf

I agree that some structure/renaming changes are fitting.

I have made a client and am reading DatabaseAdaptor.java file to re-acquaint myself with it. I plan to read it on Monday and make some suggestions then.

Thank you

String uniqueId) throws SQLException {
PreparedStatement st = conn.prepareStatement(aclSql);
// aclSql will only be null when called from isUserAuthorized.
String sql = (aclSql == null) ? singleDocContentSql : aclSql;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There are two bugs here. We'll have to go back to the if statement, because as I suggested elsewhere, the UniqueKey method depends on the query being executed and not the data being read from the result. So if we use singleDocContentSql here, we need to call setContentSqlValues rather than setAclSqlValues. We could just call getDocFromDb in that case, but the logging would be odd, and I think it's worth being clear that we're executing this statement to fetch the ACL in this case. We need some tests for mismatches in singleDocContentSqlParameters and aclSqlParameters (note that everyDocIdSql must return all the columns needed for both, but it could, for example, return 2 columns, and have each of the other queries use a different column; to check for errors I think something like a varchar and an integer column might do the trick, or one query could use both columns and the other just one).

Second, we're actually using AllPublic rather than AccessChecker when aclSql is null (this is set in init). At the moment, aclSql really can't be null here, but that's a bug. We need to always check for ACLs in the AuthzAuthority, but AccessChecker needs some work. We need to deal with the two TODOs, and the fact that getAcl can return null (I think the latter is the key to the former: a null ACL makes a null identity acceptable, otherwise we need the identity to compare to the ACL). Obviously, we need some tests here as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As discussed, changed the code and added tests.

 - added processAclQuery()
 - removed getAclFromDb()

Added tests.
}
}

private Acl processAclQuery(PreparedStatement st, Acl defaultAcl)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This was a surprising choice. Why did you extract this method instead of leaving getAclFromDb? The latter is a parallel method with two siblings, so instead of that consistency, you added this one-off method, and I don't think it isolates concerns very well, or reads any cleaner (it checks aclSql for null 3 times in getAcl, for example).

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Dec 8, 2017

Choose a reason for hiding this comment

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

Was trying to make it cleaner. Can't avoid aclSql check multiple times though.
Using getAclFromDb(), the code would be

  private Acl getAcl(Connection conn, ResultSet rs, String uniqueId)
      throws SQLException {
    if (aclSql != null || rs == null) {
      try (PreparedStatement stmt = getAclFromDb(conn, uniqueId);
          ResultSet resSet = stmt.executeQuery()) {
        log.finer("got acl");
        boolean hasResult = resSet.next();
        if (!hasResult) {
          if (aclSql == null) {
            return null;
          } else {
            // empty Acl ensures adaptor will mark this document as secure
            return Acl.EMPTY;
          }
        } else {
          return buildAcl(resSet, aclPrincipalDelimiter, aclNamespace,
              ResultSetNext.PROCESS_ALL_ROWS, Acl.EMPTY);
        }
      }
    } else {
      // Generate ACL if it came as part of result to singleDocContentSql.
      return buildAcl(rs, aclPrincipalDelimiter, aclNamespace,
          ResultSetNext.PROCESS_ONE_ROW, null);
    }
  }

Restored it back.

}

if (aclSql == null) {
context.setAuthzAuthority(new AllPublic());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllPublic is unused now.

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Dec 8, 2017

Choose a reason for hiding this comment

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

Yes missed it, removed it.

for (DocId id : ids) {
log.log(Level.FINE, "about to get acl of doc {0}", id);
Acl acl = getAcl(conn, id.getUniqueId());
Acl acl = getAcl(conn, null, id.getUniqueId());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You missed some changes here. Try running this code live by fetching public docs directly from the adaptor.

try (ResultSet resSet = st.executeQuery()) {
log.finer("got acl");
boolean hasResult = rs.next();
boolean hasResult = resSet.next();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why s/rs/resSet/? It's a non-standard abbreviation, and rs in a database connector seems perfectly clear.

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Dec 8, 2017

Choose a reason for hiding this comment

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

Unintended change in this new method, and carried over from trying to make it two nested try-with-resources blocks.

if (!hasResult) {
// empty Acl ensures adaptor will mark this document as secure
return Acl.EMPTY;
return defaultAcl;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a behavioral change, albeit a minor one. Why make this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When calling from isUserAuthorized and aclSql is null and has no results this needs to return null. Hence passing the defaultAcl and returning it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In fact, it needs to not return null. I thought it was just a nuisance to return null (it will execute the content SQL rather than just skipping this search result), but it's actually a security hole. If the row doesn't exist, then it makes no sense to return a PERMIT for it.

Copy link
Copy Markdown
Contributor

@wiarlawd wiarlawd left a comment

Choose a reason for hiding this comment

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

Partial review. I didn't finish looking at the tests.

moreEntries.put("db.everyDocIdSql", "select id from data");
moreEntries.put("db.singleDocContentSql",
"select * from data where id = ?");
executeUpdate("insert into data(id) values (1),(2)");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This syntax is H2-specific and we eliminated it when porting the unit tests to other databases. What's the point of having two rows here, instead of just one? Also, why separate this from the CREATE TABLE? Next, note that this "public" test specifies a non-null identity. Finally, both the executeUpdate calls and the db.uniqueKey and db.singleDocContentSql properties are now specific to this test. In that case, we put the executeUpdate calls first, then create the moreEntries hash map and put db.singleDocContentSql into it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Only inserted data to existing test. Made changes as per comments.

@Test
public void testAuthzAuthoritySingleDocContentSql_permit() throws Exception {
executeUpdate("create table data(id integer, loginid varchar(20), "
executeUpdate("create table data(id integer, author varchar(20), "
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the s/loginid/author/g for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Instead of id and loginid as unique keys, id and author sounds better. Hence changed it.

ResultSetNext.PROCESS_ALL_ROWS, Acl.EMPTY);
} else {
return buildAcl(resSet, aclPrincipalDelimiter, aclNamespace,
ResultSetNext.PROCESS_ALL_ROWS, null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be PROCESS_ONE_ROW. We're back to 3 checks for aclSql == null, which I think suggests we need some refactoring. Maybe some kind of ACL strategy pattern, where we construct an appropriate object once for each scenario?

Copy link
Copy Markdown
Contributor Author

@srinicodebytes srinicodebytes Dec 12, 2017

Choose a reason for hiding this comment

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

Yes, it should be PROCESS_ONE_ROW, thx. Done.
Yes, agree, refactoring to reduce the null check for aclSql will look cleaner.

throws SQLException {
if (aclSql != null || rs == null) {
try (PreparedStatement stmt = getAclFromDb(conn, uniqueId);
ResultSet resSet = stmt.executeQuery()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

resSet is a terrible name. It gives no indication of how it's different from rs, is uses a short name for a parameter and a longer name for a local variable, and it uses a non-standard abbreviation. Better would be something like s/rs/contentRs/g and then s/resSet/rs/g.

if (!hasResult) {
// empty Acl ensures adaptor will mark this document as secure
return Acl.EMPTY;
} else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

else if

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

"authorization decision {0} for user {1} and doc {2}",
new Object[]{decision, user, id});
result.put(id, decision);
if (userIdentity == null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What did you hope to gain by just moving this check inside the loop? Did you try this live?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As discussed, changed to get and check Acl first.

if (acl == null) {
result.put(id, AuthzStatus.PERMIT);
} else {
log.log(Level.INFO, "about to authorize {0} {1}",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What this meant to stay here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Existing log statement moved.

Copy link
Copy Markdown
Contributor

@wiarlawd wiarlawd left a comment

Choose a reason for hiding this comment

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

This looks right to me. Since we had to work so hard to get the parameters right, let's be sure to capture that in the tests. Some missing tests that I found:

  • PROCESS_ONE_ROW for both calls (testSingleDocSqlResultSetHasTwoRecords is not doing the job for the contentRs case because the query only returns one row)

Previously untested behaviors that we changed the code paths for, including:

  • PROCESS_ALL_ROWS (there are apparently no tests with multiple rows returned by aclSql, despite the existing code to process such)
  • Acl.EMPTY (ditto: the implicit empty ACL the code used to return for what's now the Acl.EMPTY defaultAcl parameter is untested: an aclSql with no ACL columns)
  • Combining aclSql with setContentSqlColumns (this was never happening, but I think it's reasonable to augment testAuthzAuthorityAcl_permit with your author field and swizzle things so that this combo would fail there)

I don't think this needs to be 5 new tests, maybe just some config or test data variations in existing tests could cover these.

} else if (userIdentity == null || userIdentity.getUser() == null) {
result.put(id, AuthzStatus.DENY);
} else {
log.log(Level.INFO, "about to authorize {0} {1}",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

s/INFO/FINE/, since it's executed now for every document in the authZ request.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

getAuthnIdentity(new UserPrincipal("alice")),
Arrays.asList(new DocId("1"), new DocId("2")));
null,
Arrays.asList(new DocId("1")));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, I see now, there were two docs in this test. I somehow missed that. I'm OK with restoring that. It seems reasonable to maintain that and test multiple IDs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Restored to maintain testing multiple IDs.

wiarlawd pushed a commit that referenced this pull request Dec 14, 2017
* Remove persistent if statements, especially checking whether aclSql is null.
* Make it clear that there are two choices, whether aclSql is null,
  and whether we are coming from getDocContent or isUserAuthorized.
* Make most of the config decisions at init-time.

Notes:
* Indentation is incorrect to reduce diffs.
* Additional refactoring can eliminate the additional arguments for
  buildAcl and getAclFromDb by moving them inside of AclHandler.
 - Added test testAuthzAuthoritySingleDocContentSql_muiltiRowAcl
   to test PROCESS_ONE_ROW
 - Modified test testAuthzAuthorityAcl_permit to test aclSql with
   setContentSqlColumns
 - Other changes as per review
Copy link
Copy Markdown
Contributor

@wiarlawd wiarlawd left a comment

Choose a reason for hiding this comment

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

Just some organizational nits in the tests.


Map<String, String> moreEntries = new HashMap<String, String>();
moreEntries.put("db.aclSql", "select * from acl where id = ?");
// Required for validation, but not specific to this test.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why did you remove this, or treat the data table, modeOfOperation, or everyDocIdSql as specific to this test? I can see moving db.uniqueKey and db.everyDocIdSql up (though I probably wouldn't have move the latter, since it's never executed).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It looked odd to have "create table data.." after "moreEntries.put("db.aclSql"..", hence moved it to top. However, I see that the other tests have this pattern. Restored. it. Thx.

executeUpdate("create table data(id integer)");

Map<String, String> moreEntries = new HashMap<String, String>();
moreEntries.put("db.modeOfOperation", "rowToText");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Move to "Required for validation"

Map<String, String> moreEntries = new HashMap<String, String>();
moreEntries.put("db.aclSqlParameters", "author");
moreEntries.put("db.singleDocContentSqlParameters", "id, author");
moreEntries.put("db.modeOfOperation", "rowToText");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Move to "Required for validation"

@Test
public void testAuthzAuthorityAcl_permit() throws Exception {
executeUpdate("create table acl(id integer, gsa_permit_users varchar(20))");
executeUpdate("create table data(id integer, author varchar(20))");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This shouldn't be here at all, but elsewhere the pattern is "create data, insert data, create acl, insert acl".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

executeUpdate("insert into data(id, gsa_permit_users) values (1, 'alice')");

Map<String, String> moreEntries = new HashMap<String, String>();
moreEntries.put("db.modeOfOperation", "rowToText");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You see the pattern.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, thx. Moved it in all the new tests.

Copy link
Copy Markdown
Contributor

@wiarlawd wiarlawd left a comment

Choose a reason for hiding this comment

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

Looks good. We're waiting a bit for PJ, and in the meantime, please do a "git merge master" locally and resolve the conflicts and push the commit here.

wiarlawd pushed a commit that referenced this pull request Mar 2, 2018
* Remove persistent if statements, especially checking whether aclSql is null.
* Make it clear that there are two choices, whether aclSql is null,
  and whether we are coming from getDocContent or isUserAuthorized.
* Make most of the config decisions at init-time.

Notes:
* Indentation is incorrect to reduce diffs.
* Additional refactoring can eliminate the additional arguments for
  buildAcl and getAclFromDb by moving them inside of AclHandler.
wiarlawd pushed a commit that referenced this pull request Mar 2, 2018
* Remove persistent if statements, especially checking whether aclSql is null.
* Make it clear that there are two choices, whether aclSql is null,
  and whether we are coming from getDocContent or isUserAuthorized.
* Make most of the config decisions at init-time.

Notes:
* Indentation is incorrect to reduce diffs.
* Additional refactoring can eliminate the additional arguments for
  buildAcl and getAclFromDb by moving them inside of AclHandler.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants