-
Notifications
You must be signed in to change notification settings - Fork 0
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
[ID-1117] Add fence_account_key and distributed_lock tables to db #174
Changes from 8 commits
73fd8a2
6c996e9
5b99a70
0f250c9
09c2d11
da62d3b
2668876
97c8c89
c39836f
f0a6de3
ccfc78f
fcdd682
dded2a2
b81b81d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package bio.terra.externalcreds.dataAccess; | ||
|
||
import bio.terra.externalcreds.models.DistributedLock; | ||
import io.opentelemetry.instrumentation.annotations.WithSpan; | ||
import java.util.Optional; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.dao.support.DataAccessUtils; | ||
import org.springframework.jdbc.core.RowMapper; | ||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; | ||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
@Slf4j | ||
public class DistributedLockDAO { | ||
|
||
private static final RowMapper<DistributedLock> DISTRIBUTED_LOCK_ROW_MAPPER = | ||
((rs, rowNum) -> | ||
new DistributedLock.Builder() | ||
.lockName(rs.getString("lock_name")) | ||
.userId(rs.getString("user_id")) | ||
.expiresAt(rs.getTimestamp("expires_at")) | ||
.build()); | ||
|
||
final NamedParameterJdbcTemplate jdbcTemplate; | ||
|
||
public DistributedLockDAO(NamedParameterJdbcTemplate jdbcTemplate) { | ||
this.jdbcTemplate = jdbcTemplate; | ||
} | ||
|
||
/** | ||
* @param lockName The name of the lock, e.g {provider}-createKey | ||
* @param userId The Sam user id | ||
* @return Optional<DistributedLock> An optional containing the distributed lock with this | ||
* lockName and userId (or empty) | ||
*/ | ||
@WithSpan | ||
public Optional<DistributedLock> getDistributedLock(String lockName, String userId) { | ||
var namedParameters = | ||
new MapSqlParameterSource().addValue("lockName", lockName).addValue("userId", userId); | ||
var query = "SELECT * FROM distributed_lock WHERE lock_name = :lockName AND user_id = :userId"; | ||
return Optional.ofNullable( | ||
DataAccessUtils.singleResult( | ||
jdbcTemplate.query(query, namedParameters, DISTRIBUTED_LOCK_ROW_MAPPER))); | ||
} | ||
|
||
/** | ||
* @param distributedLock The DistributedLock to insert | ||
* @return distributedLock The DistributedLock that was inserted | ||
*/ | ||
@WithSpan | ||
public DistributedLock upsertDistributedLock(DistributedLock distributedLock) { | ||
var query = | ||
"INSERT INTO distributed_lock (lock_name, user_id, expires_at)" | ||
+ " VALUES (:lockName, :userId, :expiresAt)"; | ||
Ghost-in-a-Jar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var namedParameters = | ||
new MapSqlParameterSource() | ||
.addValue("lockName", distributedLock.getLockName()) | ||
.addValue("userId", distributedLock.getUserId()) | ||
.addValue("expiresAt", distributedLock.getExpiresAt()); | ||
|
||
jdbcTemplate.update(query, namedParameters); | ||
return distributedLock; | ||
} | ||
|
||
/** | ||
* @param lockName The name of the lock, e.g {provider}-createKey | ||
* @param userId The Sam user id | ||
* @return boolean whether a distributed lock was found and deleted | ||
*/ | ||
@WithSpan | ||
public boolean deleteDistributedLock(String lockName, String userId) { | ||
var query = "DELETE FROM distributed_lock WHERE lock_name = :lockName AND user_id = :userId"; | ||
var namedParameters = | ||
new MapSqlParameterSource().addValue("lockName", lockName).addValue("userId", userId); | ||
|
||
return jdbcTemplate.update(query, namedParameters) > 0; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package bio.terra.externalcreds.dataAccess; | ||
|
||
import bio.terra.externalcreds.models.FenceAccountKey; | ||
import io.opentelemetry.instrumentation.annotations.WithSpan; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.*; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.dao.support.DataAccessUtils; | ||
import org.springframework.jdbc.core.RowMapper; | ||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; | ||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; | ||
import org.springframework.jdbc.support.GeneratedKeyHolder; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
@Slf4j | ||
public class FenceAccountKeyDAO { | ||
|
||
private static final RowMapper<FenceAccountKey> FENCE_ACCOUNT_KEY_ROW_MAPPER = | ||
((rs, rowNum) -> | ||
new FenceAccountKey.Builder() | ||
.id(rs.getInt("id")) | ||
.linkedAccountId(rs.getInt("linked_account_id")) | ||
.keyJson(base64Decode(rs.getString("key_json"))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I looked at the Bond code, and its storing the keys as raw json instead of base64. I thought it about it a little bit more, and storing the keys as JSON or JSONB might actually be better, since we could then query into the keys to find info. That may be useful for debugging or analysis. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apologies for the whiplash... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries! That's a good point about search-ability There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. im confused i thought it was storing the keys as b64? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bond stores them as plain text. |
||
.expiresAt(rs.getTimestamp("expires_at")) | ||
.build()); | ||
|
||
final NamedParameterJdbcTemplate jdbcTemplate; | ||
|
||
public FenceAccountKeyDAO(NamedParameterJdbcTemplate jdbcTemplate) { | ||
this.jdbcTemplate = jdbcTemplate; | ||
} | ||
|
||
@WithSpan | ||
public Optional<FenceAccountKey> getFenceAccountKey(String userId, String providerName) { | ||
var namedParameters = | ||
new MapSqlParameterSource("userId", userId).addValue("providerName", providerName); | ||
var query = | ||
"SELECT p.* FROM fence_account_key p" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above I would name each column instead of using *. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also nitpicky but I would use something more descriptive than 'p' for fence_account_key's alias, maybe 'fence' or 'fak'. Just to make the query easier to read. |
||
+ " INNER JOIN linked_account la ON la.id = p.linked_account_id" | ||
+ " WHERE la.user_id = :userId" | ||
+ " AND la.provider_name = :providerName"; | ||
return Optional.ofNullable( | ||
DataAccessUtils.singleResult( | ||
jdbcTemplate.query(query, namedParameters, FENCE_ACCOUNT_KEY_ROW_MAPPER))); | ||
} | ||
|
||
@WithSpan | ||
public FenceAccountKey insertFenceAccountKey(FenceAccountKey fenceAccountKey) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we rename this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it just when there is a conflict on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I had to add a unique constraint on the |
||
var query = | ||
"INSERT INTO fence_account_key (linked_account_id, key_json, expires_at)" | ||
+ " VALUES (:linkedAccountId, :keyJson, :expiresAt)" | ||
+ " RETURNING id"; | ||
|
||
var namedParameters = | ||
new MapSqlParameterSource() | ||
.addValue("linkedAccountId", fenceAccountKey.getLinkedAccountId()) | ||
.addValue("keyJson", base64Encode(fenceAccountKey.getKeyJson())) | ||
.addValue("expiresAt", fenceAccountKey.getExpiresAt()); | ||
|
||
// generatedKeyHolder will hold the id returned by the query as specified by the RETURNING | ||
// clause | ||
var generatedKeyHolder = new GeneratedKeyHolder(); | ||
jdbcTemplate.update(query, namedParameters, generatedKeyHolder); | ||
|
||
return fenceAccountKey.withId(Objects.requireNonNull(generatedKeyHolder.getKey()).intValue()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love that you can get the id back from the insert query, there are limitations on this in scala libs that I have used. |
||
} | ||
|
||
/** | ||
* @param linkedAccountId id of the linked account | ||
* @return boolean whether a fence account key was deleted | ||
*/ | ||
@WithSpan | ||
public boolean deleteFenceAccountKey(int linkedAccountId) { | ||
var namedParameters = new MapSqlParameterSource("linkedAccountId", linkedAccountId); | ||
var query = "DELETE FROM fence_account_key WHERE linked_account_id = :linkedAccountId"; | ||
return jdbcTemplate.update(query, namedParameters) > 0; | ||
} | ||
|
||
private static String base64Encode(String keyJson) { | ||
return Base64.getEncoder().encodeToString(keyJson.getBytes(StandardCharsets.UTF_8)); | ||
} | ||
|
||
private static String base64Decode(String encodedKeyJson) { | ||
return new String(Base64.getDecoder().decode(encodedKeyJson), StandardCharsets.UTF_8); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package bio.terra.externalcreds.models; | ||
|
||
import java.sql.Timestamp; | ||
import org.immutables.value.Value; | ||
|
||
@Value.Immutable | ||
public interface DistributedLock extends WithDistributedLock { | ||
String getLockName(); | ||
|
||
String getUserId(); | ||
|
||
Timestamp getExpiresAt(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably use |
||
|
||
class Builder extends ImmutableDistributedLock.Builder {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package bio.terra.externalcreds.models; | ||
|
||
import java.sql.Timestamp; | ||
import java.util.Optional; | ||
import org.immutables.value.Value; | ||
|
||
@Value.Immutable | ||
public interface FenceAccountKey extends WithFenceAccountKey { | ||
Optional<Integer> getId(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI this is optional because when we pass a |
||
|
||
Integer getLinkedAccountId(); | ||
|
||
String getKeyJson(); | ||
|
||
Timestamp getExpiresAt(); | ||
|
||
class Builder extends ImmutableFenceAccountKey.Builder {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
databaseChangeLog: | ||
- changeSet: | ||
id: "add_fence_account_key_and_dist_lock_table" | ||
author: sehsan | ||
changes: | ||
- createTable: | ||
tableName: fence_account_key | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we also need a foreign key to the user's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That does make sense! |
||
columns: | ||
- column: | ||
name: id | ||
type: int | ||
autoIncrement: true | ||
constraints: | ||
primaryKey: true | ||
nullable: false | ||
- column: | ||
name: linked_account_id | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh! Can we make this cascade on delete? That way, if someone deletes their linked account, the key will automatically get deleted too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I assumed that was the default behavior 😅 I'll double check! |
||
type: int | ||
constraints: | ||
nullable: false | ||
references: linked_account(id) | ||
foreignKeyName: fk_linked_account_id | ||
- column: | ||
name: key_json | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe this should be called key_json_base64? |
||
type: text | ||
constraints: | ||
nullable: false | ||
- column: | ||
name: expires_at | ||
type: timestamp | ||
constraints: | ||
nullable: false | ||
- createTable: | ||
tableName: distributed_lock | ||
columns: | ||
- column: | ||
name: lock_name | ||
type: text | ||
constraints: | ||
primaryKey: true | ||
primaryKeyName: pk_dist_lock | ||
- column: | ||
name: user_id | ||
type: text | ||
constraints: | ||
primaryKey: true | ||
primaryKeyName: pk_dist_lock | ||
- column: | ||
name: expires_at | ||
type: timestamp | ||
constraints: | ||
nullable: false |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,3 +29,6 @@ databaseChangeLog: | |
- include: | ||
file: changesets/20220412_add_last_encrypt_timestamp_to_ssh_key_pair_table.yaml | ||
relativeToChangelogFile: true | ||
- include: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a merge conflict here. Make sure this changeset comes after the just-merged changeset. |
||
file: changesets/20240228_add_fence_account_key_and_dist_lock_table.yaml | ||
relativeToChangelogFile: true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package bio.terra.externalcreds.dataAccess; | ||
|
||
import static org.junit.jupiter.api.Assertions.*; | ||
|
||
import bio.terra.externalcreds.BaseTest; | ||
import bio.terra.externalcreds.TestUtils; | ||
import bio.terra.externalcreds.models.DistributedLock; | ||
import java.util.Optional; | ||
import java.util.UUID; | ||
import org.junit.jupiter.api.Nested; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
|
||
class DistributedLockDAOTest extends BaseTest { | ||
|
||
@Autowired private DistributedLockDAO distributedLockDAO; | ||
private final String testLockName = "provider-createKey"; | ||
private final DistributedLock testDistributedLock = | ||
new DistributedLock.Builder() | ||
.lockName(testLockName) | ||
.userId(UUID.randomUUID().toString()) | ||
.expiresAt(TestUtils.getRandomTimestamp()) | ||
.build(); | ||
|
||
@Test | ||
void testGetMissingDistributedLock() { | ||
var shouldBeEmpty = distributedLockDAO.getDistributedLock(testLockName, "nonexistent_user_id"); | ||
assertEmpty(shouldBeEmpty); | ||
} | ||
|
||
@Nested | ||
class CreateDistributedLock { | ||
|
||
@Test | ||
void testCreateAndGetDistributedLock() { | ||
DistributedLock savedLock = distributedLockDAO.upsertDistributedLock(testDistributedLock); | ||
assertEquals(testDistributedLock, savedLock); | ||
var loadedDistributedLock = | ||
distributedLockDAO.getDistributedLock(savedLock.getLockName(), savedLock.getUserId()); | ||
assertEquals(Optional.of(savedLock), loadedDistributedLock); | ||
} | ||
} | ||
|
||
@Nested | ||
class DeleteDistributedLock { | ||
|
||
@Test | ||
void testDeleteDistributedLock() { | ||
DistributedLock savedLock = distributedLockDAO.upsertDistributedLock(testDistributedLock); | ||
assertPresent( | ||
distributedLockDAO.getDistributedLock(savedLock.getLockName(), savedLock.getUserId())); | ||
assertTrue( | ||
distributedLockDAO.deleteDistributedLock(savedLock.getLockName(), savedLock.getUserId())); | ||
assertEmpty( | ||
distributedLockDAO.getDistributedLock(savedLock.getLockName(), savedLock.getUserId())); | ||
} | ||
|
||
@Test | ||
void testDeleteNonexistentDistributedLock() { | ||
|
||
assertFalse(distributedLockDAO.deleteDistributedLock(testLockName, "nonexistent_user_id")); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nitpicky but I think it is best to name each column in a DAO query.