Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
Stephan Richter | 893f6f418c | 3 months ago |
Stephan Richter | f232c01460 | 3 months ago |
Stephan Richter | 7e60c52f45 | 3 months ago |
Stephan Richter | bb45910cb3 | 3 months ago |
Stephan Richter | b767d3ede9 | 3 months ago |
20 changed files with 1391 additions and 34 deletions
@ -0,0 +1,26 @@ |
|||||||
|
plugins { |
||||||
|
id 'java' |
||||||
|
} |
||||||
|
|
||||||
|
group = 'de.srsoftware' |
||||||
|
version = '1.0-SNAPSHOT' |
||||||
|
|
||||||
|
repositories { |
||||||
|
mavenCentral() |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
testImplementation platform('org.junit:junit-bom:5.10.0') |
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter' |
||||||
|
testImplementation project(path: ':de.srsoftware.oidc.api', configuration: "testBundle") |
||||||
|
implementation project(':de.srsoftware.oidc.api') |
||||||
|
implementation project(':de.srsoftware.utils') |
||||||
|
implementation 'org.bitbucket.b_c:jose4j:0.9.6' |
||||||
|
implementation 'org.xerial:sqlite-jdbc:3.46.0.0' |
||||||
|
implementation 'com.sun.mail:jakarta.mail:2.0.1' |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
test { |
||||||
|
useJUnitPlatform() |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.sql.Connection; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.util.HashMap; |
||||||
|
import org.sqlite.SQLiteDataSource; |
||||||
|
|
||||||
|
public class ConnectionProvider extends HashMap<File, Connection> { |
||||||
|
public Connection get(Object o) { |
||||||
|
if (o instanceof File dbFile) try { |
||||||
|
var conn = super.get(dbFile); |
||||||
|
if (conn == null) put(dbFile, conn = open(dbFile)); |
||||||
|
return conn; |
||||||
|
} catch (SQLException sqle) { |
||||||
|
throw new RuntimeException(sqle); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private Connection open(File dbFile) throws SQLException { |
||||||
|
SQLiteDataSource dataSource = new SQLiteDataSource(); |
||||||
|
dataSource.setUrl("jdbc:sqlite:%s".formatted(dbFile)); |
||||||
|
return dataSource.getConnection(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,170 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.utils.Optionals.nullable; |
||||||
|
import static de.srsoftware.utils.Strings.uuid; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.AuthorizationService; |
||||||
|
import de.srsoftware.oidc.api.data.AuthResult; |
||||||
|
import de.srsoftware.oidc.api.data.Authorization; |
||||||
|
import de.srsoftware.oidc.api.data.AuthorizedScopes; |
||||||
|
import java.sql.Connection; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.time.Instant; |
||||||
|
import java.time.temporal.ChronoUnit; |
||||||
|
import java.util.*; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
public class SqliteAuthService extends SqliteStore implements AuthorizationService { |
||||||
|
private static final String STORE_VERSION = "auth_store_version"; |
||||||
|
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')"; |
||||||
|
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
|
||||||
|
private static final String CREATE_AUTHSTORE_TABLE = "CREATE TABLE IF NOT EXISTS authorizations(userId VARCHAR(255), clientId VARCHAR(255), scope VARCHAR(255), expiration LONG, PRIMARY KEY(userId, clientId, scope));"; |
||||||
|
private static final String SAVE_AUTHORIZATION = "INSERT INTO authorizations(userId, clientId, scope, expiration) VALUES (?,?,?,?) ON CONFLICT DO UPDATE SET expiration = ?"; |
||||||
|
private static final String SELECT_AUTH = "SELECT * FROM authorizations WHERE userId = ? AND clientId = ? AND scope IN"; |
||||||
|
private static final String SELECT_USER_CLIENTS = "SELECT DISTINCT clientId FROM authorizations WHERE userId = ?"; |
||||||
|
private Map<String, Authorization> authCodes = new HashMap<>(); |
||||||
|
|
||||||
|
private Map<String, String> nonceMap = new HashMap<>(); |
||||||
|
|
||||||
|
public SqliteAuthService(Connection connection) throws SQLException { |
||||||
|
super(connection); |
||||||
|
} |
||||||
|
|
||||||
|
private void createStoreTables() throws SQLException { |
||||||
|
conn.prepareStatement(CREATE_AUTHSTORE_TABLE).execute(); |
||||||
|
} |
||||||
|
@Override |
||||||
|
protected void initTables() throws SQLException { |
||||||
|
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery(); |
||||||
|
int availableVersion = 1; |
||||||
|
int currentVersion; |
||||||
|
if (rs.next()) { |
||||||
|
currentVersion = rs.getInt("value"); |
||||||
|
rs.close(); |
||||||
|
} else { |
||||||
|
rs.close(); |
||||||
|
conn.prepareStatement(CREATE_STORE_VERSION).execute(); |
||||||
|
currentVersion = 0; |
||||||
|
} |
||||||
|
|
||||||
|
conn.setAutoCommit(false); |
||||||
|
var stmt = conn.prepareStatement(SET_STORE_VERSION); |
||||||
|
while (currentVersion < availableVersion) { |
||||||
|
try { |
||||||
|
switch (currentVersion) { |
||||||
|
case 0: |
||||||
|
createStoreTables(); |
||||||
|
break; |
||||||
|
} |
||||||
|
stmt.setInt(1, ++currentVersion); |
||||||
|
stmt.execute(); |
||||||
|
conn.commit(); |
||||||
|
} catch (Exception e) { |
||||||
|
conn.rollback(); |
||||||
|
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
conn.setAutoCommit(true); |
||||||
|
} |
||||||
|
|
||||||
|
private String authCode(Authorization authorization) { |
||||||
|
var code = uuid(); |
||||||
|
authCodes.put(code, authorization); |
||||||
|
return code; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthorizationService authorize(String userId, String clientId, Collection<String> scopes, Instant expiration) { |
||||||
|
try { |
||||||
|
conn.setAutoCommit(false); |
||||||
|
var stmt = conn.prepareStatement(SAVE_AUTHORIZATION); |
||||||
|
stmt.setString(1, userId); |
||||||
|
stmt.setString(2, clientId); |
||||||
|
stmt.setLong(4, expiration.getEpochSecond()); |
||||||
|
stmt.setLong(5, expiration.getEpochSecond()); |
||||||
|
for (var scope : scopes) { |
||||||
|
stmt.setString(3, scope); |
||||||
|
stmt.execute(); |
||||||
|
} |
||||||
|
conn.commit(); |
||||||
|
conn.setAutoCommit(true); |
||||||
|
return this; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<String> authorizedClients(String userId) { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(SELECT_USER_CLIENTS); |
||||||
|
stmt.setString(1, userId); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
var result = new ArrayList<String>(); |
||||||
|
while (rs.next()) result.add(rs.getString(1)); |
||||||
|
rs.close(); |
||||||
|
return result; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<Authorization> consumeAuthorization(String authCode) { |
||||||
|
return nullable(authCodes.remove(authCode)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<String> consumeNonce(String userId, String clientId) { |
||||||
|
var nonceKey = String.join("@", userId, clientId); |
||||||
|
return nullable(nonceMap.get(nonceKey)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthResult getAuthorization(String userId, String clientId, Collection<String> scopes) { |
||||||
|
try { |
||||||
|
var scopeList = "(" + scopes.stream().map(s -> "?").collect(Collectors.joining(", ")) + ")"; |
||||||
|
var sql = SELECT_AUTH + scopeList; |
||||||
|
var stmt = conn.prepareStatement(sql); |
||||||
|
stmt.setString(1, userId); |
||||||
|
stmt.setString(2, clientId); |
||||||
|
int i = 3; |
||||||
|
for (var scope : scopes) stmt.setString(i++, scope); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
var unauthorized = new HashSet<String>(scopes); |
||||||
|
var authorized = new HashSet<String>(); |
||||||
|
var now = Instant.now(); |
||||||
|
Instant earliestExp = null; |
||||||
|
while (rs.next()) { |
||||||
|
long expiration = rs.getLong("expiration"); |
||||||
|
String scope = rs.getString("scope"); |
||||||
|
Instant ex = Instant.ofEpochSecond(expiration).truncatedTo(ChronoUnit.SECONDS); |
||||||
|
if (ex.isAfter(now)) { |
||||||
|
unauthorized.remove(scope); |
||||||
|
authorized.add(scope); |
||||||
|
if (earliestExp == null || ex.isBefore(earliestExp)) earliestExp = ex; |
||||||
|
} |
||||||
|
} |
||||||
|
rs.close(); |
||||||
|
if (authorized.isEmpty()) return new AuthResult(null, unauthorized, null); |
||||||
|
var authorizedScopes = new AuthorizedScopes(authorized, earliestExp); |
||||||
|
var authorization = new Authorization(clientId, userId, authorizedScopes); |
||||||
|
return new AuthResult(authorizedScopes, unauthorized, authCode(authorization)); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void nonce(String userId, String clientId, String nonce) { |
||||||
|
var nonceKey = String.join("@", userId, clientId); |
||||||
|
if (nonce != null) { |
||||||
|
nonceMap.put(nonceKey, nonce); |
||||||
|
} else |
||||||
|
nonceMap.remove(nonceKey); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,176 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.oidc.api.Constants.*; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.ClientService; |
||||||
|
import de.srsoftware.oidc.api.data.Client; |
||||||
|
import java.sql.Connection; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.util.*; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
public class SqliteClientService extends SqliteStore implements ClientService { |
||||||
|
private static final String STORE_VERSION = "client_store_version"; |
||||||
|
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')"; |
||||||
|
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
|
||||||
|
private static final String CREATE_CLIENT_TABLE = "CREATE TABLE IF NOT EXISTS clients(id VARCHAR(255) NOT NULL PRIMARY KEY, name VARCHAR(255), secret VARCHAR(255), landing_page VARCHAR(255));"; |
||||||
|
private static final String CREATE_REDIRECT_TABLE = "CREATE TABLE IF NOT EXISTS client_redirects(clientId VARCHAR(255), uri VARCHAR(255), PRIMARY KEY(clientId, uri));"; |
||||||
|
private static final String SAVE_CLIENT = "INSERT INTO clients (id, name, secret, landing_page) VALUES (?,?,?,?) ON CONFLICT DO UPDATE SET name = ?, secret = ?, landing_page = ?;"; |
||||||
|
private static final String SAVE_REDIRECT = "INSERT OR IGNORE INTO client_redirects(clientId, uri) VALUES (?, ?)"; |
||||||
|
private static final String DROP_OTHER_REDIRECTS = "DELETE FROM client_redirects WHERE clientId = ? AND uri NOT IN"; |
||||||
|
private static final String SELECT_CLIENT = "SELECT * FROM clients WHERE id = ?"; |
||||||
|
private static final String SELECT_CLIENT_REDIRECTS = "SELECT uri FROM client_redirects WHERE clientId = ?"; |
||||||
|
private static final String LIST_CLIENT_REDIRECTS = "SELECT * FROM client_redirects"; |
||||||
|
private static final String LIST_CLIENTS = "SELECT * FROM clients"; |
||||||
|
private static final String DELETE_CLIENT = "DELETE FROM clients WHERE id = ?"; |
||||||
|
private static final String DELETE_CLIENT_REDIRECTS = "DELETE FROM client_redirects WHERE clientId = ?"; |
||||||
|
|
||||||
|
public SqliteClientService(Connection connection) throws SQLException { |
||||||
|
super(connection); |
||||||
|
} |
||||||
|
|
||||||
|
private void createStoreTables() throws SQLException { |
||||||
|
conn.prepareStatement(CREATE_CLIENT_TABLE).execute(); |
||||||
|
conn.prepareStatement(CREATE_REDIRECT_TABLE).execute(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void initTables() throws SQLException { |
||||||
|
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery(); |
||||||
|
int availableVersion = 1; |
||||||
|
int currentVersion; |
||||||
|
if (rs.next()) { |
||||||
|
currentVersion = rs.getInt("value"); |
||||||
|
rs.close(); |
||||||
|
} else { |
||||||
|
rs.close(); |
||||||
|
conn.prepareStatement(CREATE_STORE_VERSION).execute(); |
||||||
|
currentVersion = 0; |
||||||
|
} |
||||||
|
|
||||||
|
conn.setAutoCommit(false); |
||||||
|
var stmt = conn.prepareStatement(SET_STORE_VERSION); |
||||||
|
while (currentVersion < availableVersion) { |
||||||
|
try { |
||||||
|
switch (currentVersion) { |
||||||
|
case 0: |
||||||
|
createStoreTables(); |
||||||
|
break; |
||||||
|
} |
||||||
|
stmt.setInt(1, ++currentVersion); |
||||||
|
stmt.execute(); |
||||||
|
conn.commit(); |
||||||
|
} catch (Exception e) { |
||||||
|
conn.rollback(); |
||||||
|
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
conn.setAutoCommit(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<Client> getClient(String clientId) { |
||||||
|
Optional<Client> result = Optional.empty(); |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(SELECT_CLIENT_REDIRECTS); |
||||||
|
stmt.setString(1, clientId); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
var uris = new HashSet<String>(); |
||||||
|
while (rs.next()) uris.add(rs.getString("uri")); |
||||||
|
rs.close(); |
||||||
|
stmt = conn.prepareStatement(SELECT_CLIENT); |
||||||
|
stmt.setString(1, clientId); |
||||||
|
rs = stmt.executeQuery(); |
||||||
|
if (rs.next()) { |
||||||
|
var name = rs.getString(NAME); |
||||||
|
var secret = rs.getString(SECRET); |
||||||
|
var landing = rs.getString(LANDING_PAGE); |
||||||
|
result = Optional.of(new Client(clientId, name, secret, uris).landingPage(landing)); |
||||||
|
} |
||||||
|
rs.close(); |
||||||
|
return result; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Client> listClients() { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(LIST_CLIENT_REDIRECTS); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
var redirects = new HashMap<String, Set<String>>(); |
||||||
|
while (rs.next()) { |
||||||
|
var clientId = rs.getString("clientId"); |
||||||
|
var uri = rs.getString("uri"); |
||||||
|
var set = redirects.computeIfAbsent(clientId, k -> new HashSet<>()); |
||||||
|
set.add(uri); |
||||||
|
} |
||||||
|
rs.close(); |
||||||
|
stmt = conn.prepareStatement(LIST_CLIENTS); |
||||||
|
rs = stmt.executeQuery(); |
||||||
|
var result = new ArrayList<Client>(); |
||||||
|
while (rs.next()) { |
||||||
|
var id = rs.getString("id"); |
||||||
|
var name = rs.getString(NAME); |
||||||
|
var secret = rs.getString(SECRET); |
||||||
|
var landing = rs.getString(LANDING_PAGE); |
||||||
|
result.add(new Client(id, name, secret, redirects.get(id)).landingPage(landing)); |
||||||
|
} |
||||||
|
rs.close(); |
||||||
|
return result; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ClientService remove(String clientId) { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(DELETE_CLIENT); |
||||||
|
stmt.setString(1, clientId); |
||||||
|
stmt.execute(); |
||||||
|
stmt = conn.prepareStatement(DELETE_CLIENT_REDIRECTS); |
||||||
|
stmt.setString(1, clientId); |
||||||
|
stmt.execute(); |
||||||
|
return this; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ClientService save(Client client) { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(SAVE_CLIENT); |
||||||
|
stmt.setString(1, client.id()); |
||||||
|
stmt.setString(2, client.name()); |
||||||
|
stmt.setString(3, client.secret()); |
||||||
|
stmt.setString(4, client.landingPage()); |
||||||
|
stmt.setString(5, client.name()); |
||||||
|
stmt.setString(6, client.secret()); |
||||||
|
stmt.setString(7, client.landingPage()); |
||||||
|
stmt.execute(); |
||||||
|
stmt = conn.prepareStatement(SAVE_REDIRECT); |
||||||
|
stmt.setString(1, client.id()); |
||||||
|
for (var redirect : client.redirectUris()) { |
||||||
|
stmt.setString(2, redirect); |
||||||
|
stmt.execute(); |
||||||
|
} |
||||||
|
var where = "(" + client.redirectUris().stream().map(u -> "?").collect(Collectors.joining(", ")) + ")"; |
||||||
|
var sql = DROP_OTHER_REDIRECTS + where; |
||||||
|
stmt = conn.prepareStatement(sql); |
||||||
|
stmt.setString(1, client.id()); |
||||||
|
int i = 2; |
||||||
|
for (var redirect : client.redirectUris()) stmt.setString(i++, redirect); |
||||||
|
stmt.execute(); |
||||||
|
return this; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.KeyStorage; |
||||||
|
import java.io.IOException; |
||||||
|
import java.sql.Connection; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import org.jose4j.jwk.PublicJsonWebKey; |
||||||
|
|
||||||
|
public class SqliteKeyStore extends SqliteStore implements KeyStorage { |
||||||
|
private static final String STORE_VERSION = "key_store_version"; |
||||||
|
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')"; |
||||||
|
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
|
||||||
|
private static final String SET_KEYSTORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = 'key_store_version'"; |
||||||
|
private static final String CREATE_KEYSTORE_TABLE = "CREATE TABLE IF NOT EXISTS keystore(key_id VARCHAR(255) PRIMARY KEY, json TEXT NOT NULL);"; |
||||||
|
private static final String SAVE_KEY = "INSERT INTO keystore(key_id, json) values (?,?) ON CONFLICT(key_id) DO UPDATE SET json = ?"; |
||||||
|
private static final String SELECT_KEY_IDS = "SELECT key_id FROM keystore"; |
||||||
|
private static final String LOAD_KEY = "SELECT json FROM keystore WHERE key_id = ?"; |
||||||
|
private static final String DROP_KEY = "DELETE FROM keystore WHERE key_id = ?"; |
||||||
|
|
||||||
|
private HashMap<String, PublicJsonWebKey> loaded = new HashMap<>(); |
||||||
|
|
||||||
|
public SqliteKeyStore(Connection connection) throws SQLException { |
||||||
|
super(connection); |
||||||
|
} |
||||||
|
|
||||||
|
private void createStoreTables() throws SQLException { |
||||||
|
conn.prepareStatement(CREATE_KEYSTORE_TABLE).execute(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public KeyStorage drop(String keyId) { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(DROP_KEY); |
||||||
|
stmt.setString(1, keyId); |
||||||
|
stmt.execute(); |
||||||
|
} catch (SQLException e) { |
||||||
|
LOG.log(System.Logger.Level.WARNING, "Failed to drop key {0} from database:", keyId, e); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void initTables() throws SQLException { |
||||||
|
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery(); |
||||||
|
int availableVersion = 1; |
||||||
|
int currentVersion; |
||||||
|
if (rs.next()) { |
||||||
|
currentVersion = rs.getInt("value"); |
||||||
|
rs.close(); |
||||||
|
} else { |
||||||
|
rs.close(); |
||||||
|
conn.prepareStatement(CREATE_STORE_VERSION).execute(); |
||||||
|
currentVersion = 0; |
||||||
|
} |
||||||
|
|
||||||
|
conn.setAutoCommit(false); |
||||||
|
var stmt = conn.prepareStatement(SET_STORE_VERSION); |
||||||
|
while (currentVersion < availableVersion) { |
||||||
|
try { |
||||||
|
switch (currentVersion) { |
||||||
|
case 0: |
||||||
|
createStoreTables(); |
||||||
|
break; |
||||||
|
} |
||||||
|
stmt.setInt(1, ++currentVersion); |
||||||
|
stmt.execute(); |
||||||
|
conn.commit(); |
||||||
|
} catch (Exception e) { |
||||||
|
conn.rollback(); |
||||||
|
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
conn.setAutoCommit(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<String> listKeys() { |
||||||
|
var result = new ArrayList<String>(); |
||||||
|
try { |
||||||
|
var rs = conn.prepareStatement(SELECT_KEY_IDS).executeQuery(); |
||||||
|
while (rs.next()) result.add(rs.getString(1)); |
||||||
|
rs.close(); |
||||||
|
} catch (SQLException e) { |
||||||
|
LOG.log(System.Logger.Level.WARNING, "Failed to read key ids from table!"); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String loadJson(String keyId) throws IOException { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(LOAD_KEY); |
||||||
|
stmt.setString(1, keyId); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
String json = null; |
||||||
|
if (rs.next()) json = rs.getString(1); |
||||||
|
rs.close(); |
||||||
|
return json; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public KeyStorage store(String keyId, String json) throws IOException { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(SAVE_KEY); |
||||||
|
stmt.setString(1, keyId); |
||||||
|
stmt.setString(2, json); |
||||||
|
stmt.setString(3, json); |
||||||
|
stmt.execute(); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,183 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.oidc.api.Constants.*; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.MailConfig; |
||||||
|
import jakarta.mail.Authenticator; |
||||||
|
import jakarta.mail.PasswordAuthentication; |
||||||
|
import java.sql.Connection; |
||||||
|
import java.sql.SQLException; |
||||||
|
|
||||||
|
public class SqliteMailConfig extends SqliteStore implements MailConfig { |
||||||
|
private static final String STORE_VERSION = "mail_config_store_version"; |
||||||
|
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')"; |
||||||
|
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
private static final String CREATE_MAIL_CONFIG_TABLE = "CREATE TABLE mail_config (key VARCHAR(64) PRIMARY KEY, value VARCHAR(255));"; |
||||||
|
private static final String SAVE_MAILCONFIG = "INSERT INTO mail_config (key, value) VALUES (?, ?) ON CONFLICT DO UPDATE SET value = ?"; |
||||||
|
private static final String SELECT_MAILCONFIG = "SELECT * FROM mail_config"; |
||||||
|
private String smtpHost, senderAddress, password; |
||||||
|
|
||||||
|
private int smtpPort; |
||||||
|
private boolean smtpAuth, startTls; |
||||||
|
private Authenticator auth; |
||||||
|
|
||||||
|
public SqliteMailConfig(Connection connection) throws SQLException { |
||||||
|
super(connection); |
||||||
|
smtpHost = ""; |
||||||
|
smtpPort = 0; |
||||||
|
senderAddress = ""; |
||||||
|
password = ""; |
||||||
|
smtpAuth = true; |
||||||
|
startTls = true; |
||||||
|
var rs = conn.prepareStatement(SELECT_MAILCONFIG).executeQuery(); |
||||||
|
while (rs.next()) { |
||||||
|
var key = rs.getString(1); |
||||||
|
switch (key) { |
||||||
|
case SMTP_PORT -> smtpPort = rs.getInt(2); |
||||||
|
case SMTP_HOST -> smtpHost = rs.getString(2); |
||||||
|
case START_TLS -> startTls = rs.getBoolean(2); |
||||||
|
case SMTP_AUTH -> smtpAuth = rs.getBoolean(2); |
||||||
|
case SMTP_PASSWORD -> password = rs.getString(2); |
||||||
|
case SMTP_USER -> senderAddress = rs.getString(2); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void createStoreTables() throws SQLException { |
||||||
|
conn.prepareStatement(CREATE_MAIL_CONFIG_TABLE).execute(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
protected void initTables() throws SQLException { |
||||||
|
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery(); |
||||||
|
int availableVersion = 1; |
||||||
|
int currentVersion; |
||||||
|
if (rs.next()) { |
||||||
|
currentVersion = rs.getInt("value"); |
||||||
|
rs.close(); |
||||||
|
} else { |
||||||
|
rs.close(); |
||||||
|
conn.prepareStatement(CREATE_STORE_VERSION).execute(); |
||||||
|
currentVersion = 0; |
||||||
|
} |
||||||
|
|
||||||
|
conn.setAutoCommit(false); |
||||||
|
var stmt = conn.prepareStatement(SET_STORE_VERSION); |
||||||
|
while (currentVersion < availableVersion) { |
||||||
|
try { |
||||||
|
switch (currentVersion) { |
||||||
|
case 0: |
||||||
|
createStoreTables(); |
||||||
|
break; |
||||||
|
} |
||||||
|
stmt.setInt(1, ++currentVersion); |
||||||
|
stmt.execute(); |
||||||
|
conn.commit(); |
||||||
|
} |
||||||
|
catch (Exception e) { |
||||||
|
conn.rollback(); |
||||||
|
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
conn.setAutoCommit(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String smtpHost() { |
||||||
|
return smtpHost; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MailConfig smtpHost(String newValue) { |
||||||
|
smtpHost = newValue; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int smtpPort() { |
||||||
|
return smtpPort; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MailConfig smtpPort(int newValue) { |
||||||
|
smtpPort = newValue; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String senderAddress() { |
||||||
|
return senderAddress; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MailConfig senderAddress(String newValue) { |
||||||
|
senderAddress = newValue; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String senderPassword() { |
||||||
|
return password; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MailConfig senderPassword(String newValue) { |
||||||
|
password = newValue; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean startTls() { |
||||||
|
return startTls; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean smtpAuth() { |
||||||
|
return smtpAuth; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MailConfig startTls(boolean newValue) { |
||||||
|
startTls = newValue; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MailConfig smtpAuth(boolean newValue) { |
||||||
|
smtpAuth = newValue; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Authenticator authenticator() { |
||||||
|
if (auth == null) { |
||||||
|
auth = new Authenticator() { |
||||||
|
// override the getPasswordAuthentication method
|
||||||
|
protected PasswordAuthentication getPasswordAuthentication() { |
||||||
|
return new PasswordAuthentication(senderAddress(), senderPassword()); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
return auth; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MailConfig save() { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(SAVE_MAILCONFIG); |
||||||
|
for (var entry : map().entrySet()) { |
||||||
|
stmt.setString(1, entry.getKey()); |
||||||
|
stmt.setObject(2, entry.getValue()); |
||||||
|
stmt.setObject(3, entry.getValue()); |
||||||
|
stmt.execute(); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,130 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.utils.Strings.uuid; |
||||||
|
import static java.time.temporal.ChronoUnit.SECONDS; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.SessionService; |
||||||
|
import de.srsoftware.oidc.api.data.Session; |
||||||
|
import de.srsoftware.oidc.api.data.User; |
||||||
|
import java.sql.Connection; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.time.Instant; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
public class SqliteSessionService extends SqliteStore implements SessionService { |
||||||
|
private static final String STORE_VERSION = "session_store_version"; |
||||||
|
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')"; |
||||||
|
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
|
||||||
|
private static final String CREATE_SESSION_TABLE = "CREATE TABLE sessions (id VARCHAR(64) PRIMARY KEY, userId VARCHAR(64) NOT NULL, expiration LONG NOT NULL, trust_browser BOOLEAN DEFAULT false)"; |
||||||
|
private static final String SAVE_SESSION = "INSERT INTO sessions (id, userId, expiration, trust_browser) VALUES (?,?,?, ?) ON CONFLICT DO UPDATE SET expiration = ?, trust_browser = ?;"; |
||||||
|
private static final String DROP_SESSION = "DELETE FROM sessions WHERE id = ?"; |
||||||
|
private static final String SELECT_SESSION = "SELECT * FROM sessions WHERE id = ?"; |
||||||
|
|
||||||
|
public SqliteSessionService(Connection connection) throws SQLException { |
||||||
|
super(connection); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Session createSession(User user, boolean trustBrowser) { |
||||||
|
var now = Instant.now(); |
||||||
|
var endOfSession = now.plus(user.sessionDuration()).truncatedTo(SECONDS); |
||||||
|
return save(new Session(user.uuid(), endOfSession, uuid(), trustBrowser)); |
||||||
|
} |
||||||
|
|
||||||
|
private void createStoreTables() throws SQLException { |
||||||
|
conn.prepareStatement(CREATE_SESSION_TABLE).execute(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SessionService dropSession(String sessionId) { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(DROP_SESSION); |
||||||
|
stmt.setString(1, sessionId); |
||||||
|
stmt.execute(); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Session extend(Session session, User user) { |
||||||
|
var endOfSession = Instant.now().plus(user.sessionDuration()); |
||||||
|
return save(new Session(user.uuid(), endOfSession, session.id(), session.trustBrowser())); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void initTables() throws SQLException { |
||||||
|
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery(); |
||||||
|
int availableVersion = 1; |
||||||
|
int currentVersion; |
||||||
|
if (rs.next()) { |
||||||
|
currentVersion = rs.getInt("value"); |
||||||
|
rs.close(); |
||||||
|
} else { |
||||||
|
rs.close(); |
||||||
|
conn.prepareStatement(CREATE_STORE_VERSION).execute(); |
||||||
|
currentVersion = 0; |
||||||
|
} |
||||||
|
|
||||||
|
conn.setAutoCommit(false); |
||||||
|
var stmt = conn.prepareStatement(SET_STORE_VERSION); |
||||||
|
while (currentVersion < availableVersion) { |
||||||
|
try { |
||||||
|
switch (currentVersion) { |
||||||
|
case 0: |
||||||
|
createStoreTables(); |
||||||
|
break; |
||||||
|
} |
||||||
|
stmt.setInt(1, ++currentVersion); |
||||||
|
stmt.execute(); |
||||||
|
conn.commit(); |
||||||
|
} catch (Exception e) { |
||||||
|
conn.rollback(); |
||||||
|
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
conn.setAutoCommit(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<Session> retrieve(String sessionId) { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(SELECT_SESSION); |
||||||
|
stmt.setString(1, sessionId); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
Optional<Session> result = Optional.empty(); |
||||||
|
if (rs.next()) { |
||||||
|
var userID = rs.getString("userId"); |
||||||
|
var expiration = Instant.ofEpochSecond(rs.getLong("expiration")); |
||||||
|
var trustBrowser = rs.getBoolean("trust_browser"); |
||||||
|
if (expiration.isAfter(Instant.now())) result = Optional.of(new Session(userID, expiration, sessionId, trustBrowser)); |
||||||
|
} |
||||||
|
rs.close(); |
||||||
|
return result; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private Session save(Session session) { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(SAVE_SESSION); |
||||||
|
var expiration = session.expiration().getEpochSecond(); |
||||||
|
stmt.setString(1, session.id()); |
||||||
|
stmt.setString(2, session.userId()); |
||||||
|
stmt.setLong(3, expiration); |
||||||
|
stmt.setBoolean(4, session.trustBrowser()); |
||||||
|
stmt.setLong(5, expiration); |
||||||
|
stmt.setBoolean(6, session.trustBrowser()); |
||||||
|
stmt.execute(); |
||||||
|
return session; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import java.sql.Connection; |
||||||
|
import java.sql.SQLException; |
||||||
|
|
||||||
|
public abstract class SqliteStore { |
||||||
|
public static System.Logger LOG = System.getLogger(SqliteStore.class.getSimpleName()); |
||||||
|
private static final String CREATE_MIGRATION_TABLE = "CREATE TABLE IF NOT EXISTS metainfo(key VARCHAR(255) PRIMARY KEY, value TEXT);"; |
||||||
|
|
||||||
|
protected final Connection conn; |
||||||
|
|
||||||
|
public SqliteStore(Connection connection) throws SQLException { |
||||||
|
conn = connection; |
||||||
|
conn.prepareStatement(CREATE_MIGRATION_TABLE).execute(); |
||||||
|
initTables(); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void initTables() throws SQLException; |
||||||
|
} |
@ -0,0 +1,289 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.oidc.api.Constants.*; |
||||||
|
import static de.srsoftware.utils.Optionals.nullable; |
||||||
|
import static de.srsoftware.utils.Strings.uuid; |
||||||
|
import static java.lang.System.Logger.Level.WARNING; |
||||||
|
import static java.util.Optional.empty; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.UserService; |
||||||
|
import de.srsoftware.oidc.api.data.AccessToken; |
||||||
|
import de.srsoftware.oidc.api.data.Permission; |
||||||
|
import de.srsoftware.oidc.api.data.User; |
||||||
|
import de.srsoftware.utils.Error; |
||||||
|
import de.srsoftware.utils.PasswordHasher; |
||||||
|
import de.srsoftware.utils.Payload; |
||||||
|
import de.srsoftware.utils.Result; |
||||||
|
import java.sql.Connection; |
||||||
|
import java.sql.ResultSet; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.time.Duration; |
||||||
|
import java.time.Instant; |
||||||
|
import java.time.temporal.ChronoUnit; |
||||||
|
import java.util.*; |
||||||
|
|
||||||
|
public class SqliteUserService extends SqliteStore implements UserService { |
||||||
|
private static final String STORE_VERSION = "user_store_version"; |
||||||
|
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')"; |
||||||
|
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'"; |
||||||
|
|
||||||
|
private static final String CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS users(uuid VARCHAR(255) NOT NULL PRIMARY KEY, password VARCHAR(255), email VARCHAR(255), session_duration INT NOT NULL DEFAULT 10, username VARCHAR(255), realname VARCHAR(255));"; |
||||||
|
private static final String CREATE_USER_PERMISSION_TABLE = "CREATE TABLE IF NOT EXISTS user_permissions(uuid VARCHAR(255), permission VARCHAR(50), PRIMARY KEY(uuid,permission));"; |
||||||
|
private static final String COUNT_USERS = "SELECT count(*) FROM users"; |
||||||
|
private static final String LOAD_USER = "SELECT * FROM users WHERE uuid = ?"; |
||||||
|
private static final String LOAD_PERMISSIONS = "SELECT permission FROM user_permissions WHERE uuid = ?"; |
||||||
|
private static final String FIND_USER = "SELECT * FROM users WHERE uuid = ? OR username LIKE ? OR realname LIKE ? OR email = ? ORDER BY COALESCE(uuid, ?), username"; |
||||||
|
private static final String LIST_USERS = "SELECT * FROM users"; |
||||||
|
private static final String SAVE_USER = "INSERT INTO users (uuid,password,email,session_duration,username,realname) VALUES (?,?,?,?,?,?) ON CONFLICT DO UPDATE SET password = ?, email = ?, session_duration = ?, username = ?, realname = ?;"; |
||||||
|
private static final String INSERT_PERMISSIONS = "INSERT INTO user_permissions (uuid, permission) VALUES (?,?)"; |
||||||
|
private static final String DROP_PERMISSIONS = "DELETE FROM user_permissions WHERE uuid = ?"; |
||||||
|
private static final String DROP_USER = "DELETE FROM users WHERE uuid = ?"; |
||||||
|
private static final String UPDATE_PASSWORD = "UPDATE users SET password = ? WHERE uuid = ?"; |
||||||
|
private final PasswordHasher<String> hasher; |
||||||
|
|
||||||
|
private Map<String, AccessToken> accessTokens = new HashMap<>(); |
||||||
|
|
||||||
|
|
||||||
|
public SqliteUserService(Connection connection, PasswordHasher<String> passHasher) throws SQLException { |
||||||
|
super(connection); |
||||||
|
hasher = passHasher; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AccessToken accessToken(User user) { |
||||||
|
var token = new AccessToken(uuid(), Objects.requireNonNull(user), Instant.now().plus(1, ChronoUnit.HOURS)); |
||||||
|
accessTokens.put(token.id(), token); |
||||||
|
return token; |
||||||
|
} |
||||||
|
|
||||||
|
private User addPermissions(User user) { |
||||||
|
try { |
||||||
|
var stmt = conn.prepareStatement(LOAD_PERMISSIONS); |
||||||
|
stmt.setString(1, user.uuid()); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
while (rs.next()) try { |
||||||
|
user.add(Permission.valueOf(rs.getString("permission"))); |
||||||
|
} catch (IllegalArgumentException ignored) { |
||||||
|
} |
||||||
|
rs.close(); |
||||||
|
return user; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<User> consumeToken(String id) { |
||||||
|
var user = forToken(id); |
||||||
|
accessTokens.remove(id); |
||||||
|
return user; |
||||||
|
} |
||||||
|
|
||||||
|
private void createStoreTables() throws SQLException { |
||||||
|
conn.prepareStatement(CREATE_USER_TABLE).execute(); |
||||||
|
conn.prepareStatement(CREATE_USER_PERMISSION_TABLE).execute(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public UserService delete(User user) { |
||||||
|
try { |
||||||
|
conn.setAutoCommit(false); |
||||||
|
dropPermissionsOf(user.uuid()); |
||||||
|
var stmt = conn.prepareStatement(DROP_USER); |
||||||
|
stmt.setString(1, user.uuid()); |
||||||
|
stmt.execute(); |
||||||
|
conn.commit(); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
private void dropPermissionsOf(String uuid) throws SQLException { |
||||||
|
var stmt = conn.prepareStatement(DROP_PERMISSIONS); |
||||||
|
stmt.setString(1, uuid); |
||||||
|
stmt.execute(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<User> find(String idOrEmail) { |
||||||
|
try { |
||||||
|
var result = new HashSet<User>(); |
||||||
|
var stmt = conn.prepareStatement(FIND_USER); // TODO: implement test for this query
|
||||||
|
stmt.setString(1, idOrEmail); |
||||||
|
stmt.setString(2, "%" + idOrEmail + "%"); |
||||||
|
stmt.setString(3, "%" + idOrEmail + "%"); |
||||||
|
stmt.setString(4, idOrEmail); |
||||||
|
stmt.setString(5, idOrEmail); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
while (rs.next()) result.add(userFrom(rs)); |
||||||
|
rs.close(); |
||||||
|
return result; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<User> forToken(String id) { |
||||||
|
AccessToken token = accessTokens.get(id); |
||||||
|
if (token == null) return empty(); |
||||||
|
if (token.valid()) return Optional.of(token.user()); |
||||||
|
accessTokens.remove(id); |
||||||
|
return empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public UserService init(User defaultUser) { |
||||||
|
try { |
||||||
|
var rs = conn.prepareStatement(COUNT_USERS).executeQuery(); |
||||||
|
var count = rs.next() ? rs.getInt(1) : 0; |
||||||
|
rs.close(); |
||||||
|
if (count < 1) save(defaultUser); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void initTables() throws SQLException { |
||||||
|
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery(); |
||||||
|
int availableVersion = 1; |
||||||
|
int currentVersion; |
||||||
|
if (rs.next()) { |
||||||
|
currentVersion = rs.getInt("value"); |
||||||
|
rs.close(); |
||||||
|
} else { |
||||||
|
rs.close(); |
||||||
|
conn.prepareStatement(CREATE_STORE_VERSION).execute(); |
||||||
|
currentVersion = 0; |
||||||
|
} |
||||||
|
|
||||||
|
conn.setAutoCommit(false); |
||||||
|
var stmt = conn.prepareStatement(SET_STORE_VERSION); |
||||||
|
while (currentVersion < availableVersion) { |
||||||
|
try { |
||||||
|
switch (currentVersion) { |
||||||
|
case 0: |
||||||
|
createStoreTables(); |
||||||
|
break; |
||||||
|
} |
||||||
|
stmt.setInt(1, ++currentVersion); |
||||||
|
stmt.execute(); |
||||||
|
conn.commit(); |
||||||
|
} catch (Exception e) { |
||||||
|
conn.rollback(); |
||||||
|
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
conn.setAutoCommit(true); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public List<User> list() { |
||||||
|
try { |
||||||
|
List<User> result = new ArrayList<>(); |
||||||
|
var rs = conn.prepareStatement(LIST_USERS).executeQuery(); |
||||||
|
while (rs.next()) result.add(userFrom(rs)); |
||||||
|
rs.close(); |
||||||
|
for (User user : result) addPermissions(user); |
||||||
|
return result; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<User> load(String id) { |
||||||
|
try { |
||||||
|
User user = null; |
||||||
|
var stmt = conn.prepareStatement(LOAD_USER); |
||||||
|
stmt.setString(1, id); |
||||||
|
var rs = stmt.executeQuery(); |
||||||
|
if (rs.next()) user = userFrom(rs); |
||||||
|
rs.close(); |
||||||
|
return nullable(user).map(this::addPermissions); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Result<User> login(String username, String password) { |
||||||
|
if (username == null || username.isBlank()) return Error.message(ERROR_NO_USERNAME); |
||||||
|
var optLock = getLock(username); |
||||||
|
if (optLock.isPresent()) { |
||||||
|
var lock = optLock.get(); |
||||||
|
LOG.log(WARNING, "{0} is locked after {1} failed logins. Lock will be released at {2}", username, lock.attempts(), lock.releaseTime()); |
||||||
|
return Error.message(ERROR_LOCKED, ATTEMPTS, lock.attempts(), RELEASE, lock.releaseTime()); |
||||||
|
} |
||||||
|
for (var user : find(username)) { |
||||||
|
if (passwordMatches(password, user)) { |
||||||
|
this.unlock(username); |
||||||
|
return Payload.of(user); |
||||||
|
} |
||||||
|
} |
||||||
|
var lock = lock(username); |
||||||
|
LOG.log(WARNING, "Login failed for {0} → locking account until {1}", username, lock.releaseTime()); |
||||||
|
return Error.message(ERROR_LOGIN_FAILED, RELEASE, lock.releaseTime()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean passwordMatches(String password, User user) { |
||||||
|
return hasher.matches(password, user.hashedPassword()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SqliteUserService save(User user) { |
||||||
|
try { |
||||||
|
conn.setAutoCommit(false); |
||||||
|
var stmt = conn.prepareStatement(SAVE_USER); |
||||||
|
stmt.setString(1, user.uuid()); |
||||||
|
stmt.setString(2, user.hashedPassword()); |
||||||
|
stmt.setString(3, user.email()); |
||||||
|
stmt.setLong(4, user.sessionDuration().toMinutes()); |
||||||
|
stmt.setString(5, user.username()); |
||||||
|
stmt.setString(6, user.realName()); |
||||||
|
stmt.setString(7, user.hashedPassword()); |
||||||
|
stmt.setString(8, user.email()); |
||||||
|
stmt.setLong(9, user.sessionDuration().toMinutes()); |
||||||
|
stmt.setString(10, user.username()); |
||||||
|
stmt.setString(11, user.realName()); |
||||||
|
stmt.execute(); |
||||||
|
dropPermissionsOf(user.uuid()); |
||||||
|
|
||||||
|
stmt = conn.prepareStatement(INSERT_PERMISSIONS); |
||||||
|
stmt.setString(1, user.uuid()); |
||||||
|
for (Permission perm : Permission.values()) { |
||||||
|
if (user.hasPermission(perm)) { |
||||||
|
stmt.setString(2, perm.toString()); |
||||||
|
stmt.execute(); |
||||||
|
} |
||||||
|
} |
||||||
|
conn.commit(); |
||||||
|
return this; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SqliteUserService updatePassword(User user, String plaintextPassword) { |
||||||
|
return save(user.hashedPassword(hasher.hash(plaintextPassword, uuid()))); |
||||||
|
} |
||||||
|
|
||||||
|
private User userFrom(ResultSet rs) throws SQLException { |
||||||
|
var uuid = rs.getString("uuid"); |
||||||
|
var pass = rs.getString("password"); |
||||||
|
var user = rs.getString("username"); |
||||||
|
var name = rs.getString("realname"); |
||||||
|
var mail = rs.getString("email"); |
||||||
|
var mins = rs.getLong("session_duration"); |
||||||
|
return new User(user, pass, name, mail, uuid).sessionDuration(Duration.ofMinutes(mins)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.utils.Strings.uuid; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.AuthServiceTest; |
||||||
|
import de.srsoftware.oidc.api.AuthorizationService; |
||||||
|
import java.io.File; |
||||||
|
import java.sql.SQLException; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
|
||||||
|
public class SqliteAuthServiceTest extends AuthServiceTest { |
||||||
|
private AuthorizationService authorizationService; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected AuthorizationService authorizationService() { |
||||||
|
return authorizationService; |
||||||
|
} |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setup() throws SQLException { |
||||||
|
var dbFile = new File("/tmp/" + uuid() + ".sqlite"); |
||||||
|
var conn = new ConnectionProvider().get(dbFile); |
||||||
|
authorizationService = new SqliteAuthService(conn); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.utils.Strings.uuid; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.ClientService; |
||||||
|
import de.srsoftware.oidc.api.ClientServiceTest; |
||||||
|
import java.io.File; |
||||||
|
import java.sql.SQLException; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
|
||||||
|
public class SqliteClientServiceTest extends ClientServiceTest { |
||||||
|
private ClientService clientService; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ClientService clientService() { |
||||||
|
return clientService; |
||||||
|
} |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setup() throws SQLException { |
||||||
|
var dbFile = new File("/tmp/" + uuid() + ".sqlite"); |
||||||
|
var conn = new ConnectionProvider().get(dbFile); |
||||||
|
clientService = new SqliteClientService(conn); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.utils.Strings.uuid; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.KeyStorage; |
||||||
|
import de.srsoftware.oidc.api.KeyStoreTest; |
||||||
|
import java.io.File; |
||||||
|
import java.sql.SQLException; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
|
||||||
|
public class SqliteKeyStoreTest extends KeyStoreTest { |
||||||
|
private KeyStorage keyStore; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected KeyStorage keyStore() { |
||||||
|
return keyStore; |
||||||
|
} |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setup() throws SQLException { |
||||||
|
var dbFile = new File("/tmp/" + uuid() + ".sqlite"); |
||||||
|
var conn = new ConnectionProvider().get(dbFile); |
||||||
|
keyStore = new SqliteKeyStore(conn); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import static de.srsoftware.utils.Strings.uuid; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.MailConfig; |
||||||
|
import de.srsoftware.oidc.api.MailConfigTest; |
||||||
|
import java.io.File; |
||||||
|
import java.sql.SQLException; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
|
||||||
|
public class SqliteMailConfigTest extends MailConfigTest { |
||||||
|
private SqliteMailConfig mailConfig; |
||||||
|
private File dbFile; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected MailConfig mailConfig() { |
||||||
|
return mailConfig; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void reOpen() { |
||||||
|
try { |
||||||
|
var conn = new ConnectionProvider().get(dbFile); |
||||||
|
mailConfig = new SqliteMailConfig(conn); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setup() throws SQLException { |
||||||
|
dbFile = new File("/tmp/" + uuid() + ".sqlite"); |
||||||
|
var conn = new ConnectionProvider().get(dbFile); |
||||||
|
mailConfig = new SqliteMailConfig(conn); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.SessionService; |
||||||
|
import de.srsoftware.oidc.api.SessionServiceTest; |
||||||
|
import java.io.File; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.util.UUID; |
||||||
|
import org.junit.jupiter.api.AfterEach; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
|
||||||
|
public class SqliteSessionServiceTest extends SessionServiceTest { |
||||||
|
private File storage = new File("/tmp/" + UUID.randomUUID()); |
||||||
|
private SessionService sessionService = null; |
||||||
|
|
||||||
|
@AfterEach |
||||||
|
public void tearDown() { |
||||||
|
if (storage.exists()) storage.delete(); |
||||||
|
} |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setup() throws SQLException { |
||||||
|
tearDown(); |
||||||
|
sessionService = new SqliteSessionService(new ConnectionProvider().get(storage)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected SessionService sessionService() { |
||||||
|
return sessionService; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
/* © SRSoftware 2024 */ |
||||||
|
package de.srsoftware.oidc.datastore.sqlite; |
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.UserService; |
||||||
|
import de.srsoftware.oidc.api.UserServiceTest; |
||||||
|
import java.io.File; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.util.UUID; |
||||||
|
import org.junit.jupiter.api.AfterEach; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
|
||||||
|
public class SqliteUserServiceTest extends UserServiceTest { |
||||||
|
private File storage = new File("/tmp/" + UUID.randomUUID()); |
||||||
|
private UserService userService; |
||||||
|
|
||||||
|
|
||||||
|
@AfterEach |
||||||
|
public void tearDown() { |
||||||
|
if (storage.exists()) storage.delete(); |
||||||
|
} |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setup() throws SQLException { |
||||||
|
tearDown(); |
||||||
|
userService = new SqliteUserService(new ConnectionProvider().get(storage), hasher()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected UserService userService() { |
||||||
|
return userService; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue