Skip to content

Commit aa64c44

Browse files
Add Support JdbcPublicKeyCredentialUserEntityRepository
Closes gh-16224
1 parent eca12c6 commit aa64c44

File tree

6 files changed

+483
-1
lines changed

6 files changed

+483
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.aot.hint;
18+
19+
import org.springframework.aot.hint.RuntimeHints;
20+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
21+
import org.springframework.jdbc.core.JdbcOperations;
22+
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
23+
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
24+
25+
/**
26+
*
27+
* A JDBC implementation of an {@link PublicKeyCredentialUserEntityRepository} that uses a
28+
* {@link JdbcOperations} for {@link PublicKeyCredentialUserEntity} persistence.
29+
*
30+
* @author Max Batischev
31+
* @since 6.5
32+
*/
33+
class PublicKeyCredentialUserEntityRuntimeHints implements RuntimeHintsRegistrar {
34+
35+
@Override
36+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
37+
hints.resources().registerPattern("org/springframework/security/user-entities-schema.sql");
38+
}
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.webauthn.management;
18+
19+
import java.sql.ResultSet;
20+
import java.sql.SQLException;
21+
import java.sql.Types;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.function.Function;
25+
26+
import org.springframework.dao.DuplicateKeyException;
27+
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
28+
import org.springframework.jdbc.core.JdbcOperations;
29+
import org.springframework.jdbc.core.PreparedStatementSetter;
30+
import org.springframework.jdbc.core.RowMapper;
31+
import org.springframework.jdbc.core.SqlParameterValue;
32+
import org.springframework.security.web.webauthn.api.Bytes;
33+
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
34+
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
35+
import org.springframework.util.Assert;
36+
37+
/**
38+
* A JDBC implementation of an {@link PublicKeyCredentialUserEntityRepository} that uses a
39+
* {@link JdbcOperations} for {@link PublicKeyCredentialUserEntity} persistence.
40+
*
41+
* <b>NOTE:</b> This {@code PublicKeyCredentialUserEntityRepository} depends on the table
42+
* definition described in
43+
* "classpath:org/springframework/security/user-entities-schema.sql" and therefore MUST be
44+
* defined in the database schema.
45+
*
46+
* @author Max Batischev
47+
* @since 6.5
48+
* @see PublicKeyCredentialUserEntityRepository
49+
* @see PublicKeyCredentialUserEntity
50+
* @see JdbcOperations
51+
* @see RowMapper
52+
*/
53+
public final class JdbcPublicKeyCredentialUserEntityRepository implements PublicKeyCredentialUserEntityRepository {
54+
55+
private RowMapper<PublicKeyCredentialUserEntity> userEntityRowMapper = new UserEntityRecordRowMapper();
56+
57+
private Function<PublicKeyCredentialUserEntity, List<SqlParameterValue>> userEntityParametersMapper = new UserEntityParametersMapper();
58+
59+
private final JdbcOperations jdbcOperations;
60+
61+
private static final String TABLE_NAME = "user_entities";
62+
63+
// @formatter:off
64+
private static final String COLUMN_NAMES = "id, "
65+
+ "name, "
66+
+ "display_name ";
67+
// @formatter:on
68+
69+
// @formatter:off
70+
private static final String SAVE_USER_SQL = "INSERT INTO " + TABLE_NAME
71+
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?)";
72+
// @formatter:on
73+
74+
private static final String ID_FILTER = "id = ? ";
75+
76+
private static final String USER_NAME_FILTER = "name = ? ";
77+
78+
// @formatter:off
79+
private static final String FIND_USER_BY_ID_SQL = "SELECT " + COLUMN_NAMES
80+
+ " FROM " + TABLE_NAME
81+
+ " WHERE " + ID_FILTER;
82+
// @formatter:on
83+
84+
// @formatter:off
85+
private static final String FIND_USER_BY_NAME_SQL = "SELECT " + COLUMN_NAMES
86+
+ " FROM " + TABLE_NAME
87+
+ " WHERE " + USER_NAME_FILTER;
88+
// @formatter:on
89+
90+
private static final String DELETE_USER_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + ID_FILTER;
91+
92+
// @formatter:off
93+
private static final String UPDATE_USER_SQL = "UPDATE " + TABLE_NAME
94+
+ " SET name = ?, display_name = ? "
95+
+ " WHERE " + ID_FILTER;
96+
// @formatter:on
97+
98+
/**
99+
* Constructs a {@code JdbcPublicKeyCredentialUserEntityRepository} using the provided
100+
* parameters.
101+
* @param jdbcOperations the JDBC operations
102+
*/
103+
public JdbcPublicKeyCredentialUserEntityRepository(JdbcOperations jdbcOperations) {
104+
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
105+
this.jdbcOperations = jdbcOperations;
106+
}
107+
108+
@Override
109+
public PublicKeyCredentialUserEntity findById(Bytes id) {
110+
Assert.notNull(id, "id cannot be null");
111+
List<PublicKeyCredentialUserEntity> result = this.jdbcOperations.query(FIND_USER_BY_ID_SQL,
112+
this.userEntityRowMapper, id.toBase64UrlString());
113+
return !result.isEmpty() ? result.get(0) : null;
114+
}
115+
116+
@Override
117+
public PublicKeyCredentialUserEntity findByUsername(String username) {
118+
Assert.hasText(username, "name cannot be null or empty");
119+
List<PublicKeyCredentialUserEntity> result = this.jdbcOperations.query(FIND_USER_BY_NAME_SQL,
120+
this.userEntityRowMapper, username);
121+
return !result.isEmpty() ? result.get(0) : null;
122+
}
123+
124+
@Override
125+
public void save(PublicKeyCredentialUserEntity userEntity) {
126+
Assert.notNull(userEntity, "userEntity cannot be null");
127+
boolean existsUserEntity = null != this.findById(userEntity.getId());
128+
if (existsUserEntity) {
129+
updateUserEntity(userEntity);
130+
}
131+
else {
132+
try {
133+
insertUserEntity(userEntity);
134+
}
135+
catch (DuplicateKeyException ex) {
136+
updateUserEntity(userEntity);
137+
}
138+
}
139+
}
140+
141+
private void insertUserEntity(PublicKeyCredentialUserEntity userEntity) {
142+
List<SqlParameterValue> parameters = this.userEntityParametersMapper.apply(userEntity);
143+
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
144+
this.jdbcOperations.update(SAVE_USER_SQL, pss);
145+
}
146+
147+
private void updateUserEntity(PublicKeyCredentialUserEntity userEntity) {
148+
List<SqlParameterValue> parameters = this.userEntityParametersMapper.apply(userEntity);
149+
SqlParameterValue userEntityId = parameters.remove(0);
150+
parameters.add(userEntityId);
151+
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
152+
this.jdbcOperations.update(UPDATE_USER_SQL, pss);
153+
}
154+
155+
@Override
156+
public void delete(Bytes id) {
157+
Assert.notNull(id, "id cannot be null");
158+
SqlParameterValue[] parameters = new SqlParameterValue[] {
159+
new SqlParameterValue(Types.VARCHAR, id.toBase64UrlString()), };
160+
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
161+
this.jdbcOperations.update(DELETE_USER_SQL, pss);
162+
}
163+
164+
private static class UserEntityParametersMapper
165+
implements Function<PublicKeyCredentialUserEntity, List<SqlParameterValue>> {
166+
167+
@Override
168+
public List<SqlParameterValue> apply(PublicKeyCredentialUserEntity userEntity) {
169+
List<SqlParameterValue> parameters = new ArrayList<>();
170+
171+
parameters.add(new SqlParameterValue(Types.VARCHAR, userEntity.getId().toBase64UrlString()));
172+
parameters.add(new SqlParameterValue(Types.VARCHAR, userEntity.getName()));
173+
parameters.add(new SqlParameterValue(Types.VARCHAR, userEntity.getDisplayName()));
174+
175+
return parameters;
176+
}
177+
178+
}
179+
180+
private static class UserEntityRecordRowMapper implements RowMapper<PublicKeyCredentialUserEntity> {
181+
182+
@Override
183+
public PublicKeyCredentialUserEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
184+
Bytes id = Bytes.fromBase64(new String(rs.getString("id").getBytes()));
185+
String name = rs.getString("name");
186+
String displayName = rs.getString("display_name");
187+
188+
return ImmutablePublicKeyCredentialUserEntity.builder().id(id).name(name).displayName(displayName).build();
189+
}
190+
191+
}
192+
193+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
org.springframework.aot.hint.RuntimeHintsRegistrar=\
22
org.springframework.security.web.aot.hint.WebMvcSecurityRuntimeHints,\
3-
org.springframework.security.web.aot.hint.UserCredentialRuntimeHints
3+
org.springframework.security.web.aot.hint.UserCredentialRuntimeHints,\
4+
org.springframework.security.web.aot.hint.PublicKeyCredentialUserEntityRuntimeHints
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
create table user_entities
2+
(
3+
id varchar(1000) not null,
4+
name varchar(100) not null,
5+
display_name varchar(200),
6+
primary key (id)
7+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.aot.hint;
18+
19+
import java.util.stream.Stream;
20+
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.MethodSource;
24+
25+
import org.springframework.aot.hint.RuntimeHints;
26+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
27+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
28+
import org.springframework.core.io.support.SpringFactoriesLoader;
29+
import org.springframework.util.ClassUtils;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
33+
/**
34+
* Tests for {@link PublicKeyCredentialUserEntityRuntimeHints}
35+
*
36+
* @author Max Batischev
37+
*/
38+
public class PublicKeyCredentialUserEntityRuntimeHintsTests {
39+
40+
private final RuntimeHints hints = new RuntimeHints();
41+
42+
@BeforeEach
43+
void setup() {
44+
SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories")
45+
.load(RuntimeHintsRegistrar.class)
46+
.forEach((registrar) -> registrar.registerHints(this.hints, ClassUtils.getDefaultClassLoader()));
47+
}
48+
49+
@ParameterizedTest
50+
@MethodSource("getUserEntitiesSqlFiles")
51+
void userEntitiesSqlFilesHasHints(String schemaFile) {
52+
assertThat(RuntimeHintsPredicates.resource().forResource(schemaFile)).accepts(this.hints);
53+
}
54+
55+
private static Stream<String> getUserEntitiesSqlFiles() {
56+
return Stream.of("org/springframework/security/user-entities-schema.sql");
57+
}
58+
59+
}

0 commit comments

Comments
 (0)