Skip to content

Commit 91c0bc2

Browse files
authored
feat: parse MySQL multi-factor authentication in CREATE USER and ALTER USER (#2910)
CREATE USER accepts a second and third factor after AND, and INITIAL AUTHENTICATION after a plugin without credentials. ALTER USER accepts ADD, MODIFY and DROP of a factor, once or twice per account, and the registration steps INITIATE REGISTRATION, FINISH REGISTRATION SET CHALLENGE_RESPONSE AS and UNREGISTER for one account or USER(). The factors extend the existing account model: UserAccount keeps the additional authentications and the UserFactorOperation list, and UserAuthentication keeps the initial authentication. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 186fb5d commit 91c0bc2

7 files changed

Lines changed: 929 additions & 17 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/alter/AlterUser.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
* {@link UserOptions} are those of {@link net.sf.jsqlparser.statement.create.user.CreateUser}.
2929
* {@code ALTER USER acct DEFAULT ROLE ...} keeps its roles in
3030
* {@link UserOptions#getDefaultRoles()}. {@code ALTER USER USER() ...} names the current user with
31-
* the unquoted account name {@code USER()}, see {@link #isUserFunction()}.
31+
* the unquoted account name {@code USER()}, see {@link #isUserFunction()}. Multi-factor
32+
* authentication changes ({@code ADD}, {@code MODIFY}, {@code DROP} of {@code 2 FACTOR} or
33+
* {@code 3 FACTOR}) and the registration steps of one account are the
34+
* {@link UserAccount#getFactorOperations()} of the account.
3235
*/
3336
public class AlterUser extends ASTNodeAccessImpl implements Statement {
3437

‎src/main/java/net/sf/jsqlparser/statement/create/user/UserAccount.java‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,27 @@
1010
package net.sf.jsqlparser.statement.create.user;
1111

1212
import java.io.Serializable;
13+
import java.util.ArrayList;
14+
import java.util.Collection;
15+
import java.util.Collections;
16+
import java.util.List;
1317
import net.sf.jsqlparser.expression.StringValue;
1418

1519
/**
1620
* A user and optional host together with its authentication clause. The current user may be named
17-
* {@code CURRENT_USER} or {@code CURRENT_USER()}.
21+
* {@code CURRENT_USER} or {@code CURRENT_USER()}. MySQL multi-factor authentication adds the second
22+
* and third factor of {@code CREATE USER ... IDENTIFIED ... AND IDENTIFIED ...} in
23+
* {@link #getAdditionalAuthentications()}, and the factor changes and registration steps of
24+
* {@code ALTER USER} in {@link #getFactorOperations()}.
1825
*/
1926
public class UserAccount implements Serializable {
2027

2128
private StringValue user;
2229
private StringValue host;
2330
private UserAuthentication authentication;
2431
private boolean currentUserParentheses;
32+
private final List<UserAuthentication> additionalAuthentications = new ArrayList<>();
33+
private final List<UserFactorOperation> factorOperations = new ArrayList<>();
2534

2635
public StringValue getUser() {
2736
return user;
@@ -56,6 +65,38 @@ public void setAuthentication(UserAuthentication authentication) {
5665
this.authentication = authentication;
5766
}
5867

68+
/**
69+
* The authentication of the second and third factor, each written {@code AND IDENTIFIED ...}
70+
* after the first one in {@code CREATE USER}; empty, never null, when none are given.
71+
*/
72+
public List<UserAuthentication> getAdditionalAuthentications() {
73+
return Collections.unmodifiableList(additionalAuthentications);
74+
}
75+
76+
public void setAdditionalAuthentications(
77+
Collection<? extends UserAuthentication> additionalAuthentications) {
78+
this.additionalAuthentications.clear();
79+
if (additionalAuthentications != null) {
80+
this.additionalAuthentications.addAll(additionalAuthentications);
81+
}
82+
}
83+
84+
/**
85+
* The {@code ADD}, {@code MODIFY} and {@code DROP} factor changes, or the single registration
86+
* step, of an {@code ALTER USER} account in source order; empty, never null, when none are
87+
* given.
88+
*/
89+
public List<UserFactorOperation> getFactorOperations() {
90+
return Collections.unmodifiableList(factorOperations);
91+
}
92+
93+
public void setFactorOperations(Collection<? extends UserFactorOperation> factorOperations) {
94+
this.factorOperations.clear();
95+
if (factorOperations != null) {
96+
this.factorOperations.addAll(factorOperations);
97+
}
98+
}
99+
59100
@Override
60101
public String toString() {
61102
StringBuilder builder = new StringBuilder().append(user);
@@ -68,6 +109,12 @@ public String toString() {
68109
if (authentication != null) {
69110
builder.append(" ").append(authentication);
70111
}
112+
for (UserAuthentication additional : additionalAuthentications) {
113+
builder.append(" AND ").append(additional);
114+
}
115+
for (UserFactorOperation operation : factorOperations) {
116+
builder.append(" ").append(operation);
117+
}
71118
return builder.toString();
72119
}
73120

@@ -90,4 +137,27 @@ public UserAccount withAuthentication(UserAuthentication authentication) {
90137
setAuthentication(authentication);
91138
return this;
92139
}
140+
141+
public UserAccount withAdditionalAuthentications(
142+
Collection<? extends UserAuthentication> additionalAuthentications) {
143+
setAdditionalAuthentications(additionalAuthentications);
144+
return this;
145+
}
146+
147+
public UserAccount addAdditionalAuthentications(
148+
UserAuthentication... additionalAuthentications) {
149+
Collections.addAll(this.additionalAuthentications, additionalAuthentications);
150+
return this;
151+
}
152+
153+
public UserAccount withFactorOperations(
154+
Collection<? extends UserFactorOperation> factorOperations) {
155+
setFactorOperations(factorOperations);
156+
return this;
157+
}
158+
159+
public UserAccount addFactorOperations(UserFactorOperation... factorOperations) {
160+
Collections.addAll(this.factorOperations, factorOperations);
161+
return this;
162+
}
93163
}

‎src/main/java/net/sf/jsqlparser/statement/create/user/UserAuthentication.java‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
* {@code REPLACE 'current'} and {@code RETAIN CURRENT PASSWORD} to a password assignment, and
2020
* {@code DISCARD OLD PASSWORD} as a mode of its own. The plugin name keeps its quote character, as
2121
* in {@code IDENTIFIED WITH 'caching_sha2_password'}, and the hash of {@code AS} may be a
22-
* hexadecimal literal, as SHOW CREATE USER prints it.
22+
* hexadecimal literal, as SHOW CREATE USER prints it. {@code CREATE USER} may give a passwordless
23+
* plugin a temporary first factor, {@code IDENTIFIED WITH plugin INITIAL AUTHENTICATION IDENTIFIED
24+
* ...}, kept in {@link #getInitialAuthentication()}.
2325
*/
2426
public class UserAuthentication implements Serializable {
2527

@@ -38,6 +40,7 @@ public boolean isPasswordAssignment() {
3840
private Expression credential;
3941
private StringValue currentCredential;
4042
private boolean retainCurrentPassword;
43+
private UserAuthentication initialAuthentication;
4144

4245
public Mode getMode() {
4346
return mode;
@@ -111,9 +114,26 @@ public void setRetainCurrentPassword(boolean retainCurrentPassword) {
111114
this.retainCurrentPassword = retainCurrentPassword;
112115
}
113116

117+
/**
118+
* The authentication after {@code INITIAL AUTHENTICATION}, which registers the device of a
119+
* passwordless plugin, or null when omitted. MySQL writes it as {@code IDENTIFIED BY 'auth'},
120+
* {@code IDENTIFIED BY RANDOM PASSWORD} or {@code IDENTIFIED WITH plugin AS 'hash'}, after
121+
* {@code IDENTIFIED WITH plugin} only.
122+
*/
123+
public UserAuthentication getInitialAuthentication() {
124+
return initialAuthentication;
125+
}
126+
127+
public void setInitialAuthentication(UserAuthentication initialAuthentication) {
128+
this.initialAuthentication = initialAuthentication;
129+
}
130+
114131
@Override
115132
public String toString() {
116133
StringBuilder builder = new StringBuilder(modeToString());
134+
if (initialAuthentication != null) {
135+
builder.append(" INITIAL AUTHENTICATION ").append(initialAuthentication);
136+
}
117137
if (currentCredential != null) {
118138
builder.append(" REPLACE ").append(currentCredential);
119139
}
@@ -178,4 +198,10 @@ public UserAuthentication withRetainCurrentPassword(boolean retainCurrentPasswor
178198
setRetainCurrentPassword(retainCurrentPassword);
179199
return this;
180200
}
201+
202+
public UserAuthentication withInitialAuthentication(
203+
UserAuthentication initialAuthentication) {
204+
setInitialAuthentication(initialAuthentication);
205+
return this;
206+
}
181207
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.user;
11+
12+
import java.io.Serializable;
13+
import net.sf.jsqlparser.expression.Expression;
14+
import net.sf.jsqlparser.expression.HexValue;
15+
import net.sf.jsqlparser.expression.StringValue;
16+
17+
/**
18+
* One multi-factor authentication change of a MySQL {@code ALTER USER} account:
19+
* {@code ADD factor IDENTIFIED ...}, {@code MODIFY factor IDENTIFIED ...}, {@code DROP factor}, or
20+
* a registration step of a passwordless device, {@code factor INITIATE REGISTRATION},
21+
* {@code factor FINISH REGISTRATION SET CHALLENGE_RESPONSE AS '...'} and {@code factor UNREGISTER}.
22+
* The factor is written {@code 2 FACTOR} or {@code 3 FACTOR}; the server checks the number when it
23+
* executes the statement.
24+
*/
25+
public class UserFactorOperation implements Serializable {
26+
27+
public enum Operation {
28+
ADD, MODIFY, DROP, INITIATE_REGISTRATION, FINISH_REGISTRATION, UNREGISTER;
29+
30+
/** Whether an {@code IDENTIFIED} clause follows the factor, as for ADD and MODIFY. */
31+
public boolean hasAuthentication() {
32+
return this == ADD || this == MODIFY;
33+
}
34+
35+
/** Whether this is a registration step, written after the factor. */
36+
public boolean isRegistration() {
37+
return this == INITIATE_REGISTRATION || this == FINISH_REGISTRATION
38+
|| this == UNREGISTER;
39+
}
40+
}
41+
42+
private Operation operation;
43+
private int factor;
44+
private UserAuthentication authentication;
45+
private Expression challengeResponse;
46+
47+
public UserFactorOperation() {
48+
// for the fluent setters
49+
}
50+
51+
public UserFactorOperation(Operation operation, int factor) {
52+
this.operation = operation;
53+
this.factor = factor;
54+
}
55+
56+
public Operation getOperation() {
57+
return operation;
58+
}
59+
60+
public void setOperation(Operation operation) {
61+
this.operation = operation;
62+
}
63+
64+
/** The factor number of {@code n FACTOR}: 2 or 3 in a statement the server executes. */
65+
public int getFactor() {
66+
return factor;
67+
}
68+
69+
public void setFactor(int factor) {
70+
this.factor = factor;
71+
}
72+
73+
/** The authentication of the added or modified factor, or null for the other operations. */
74+
public UserAuthentication getAuthentication() {
75+
return authentication;
76+
}
77+
78+
public void setAuthentication(UserAuthentication authentication) {
79+
this.authentication = authentication;
80+
}
81+
82+
/**
83+
* The value after {@code SET CHALLENGE_RESPONSE AS} of FINISH REGISTRATION: a
84+
* {@link StringValue} or a {@link HexValue}, or null for the other operations.
85+
*/
86+
public Expression getChallengeResponse() {
87+
return challengeResponse;
88+
}
89+
90+
public void setChallengeResponse(Expression challengeResponse) {
91+
if (challengeResponse != null && !(challengeResponse instanceof StringValue)
92+
&& !(challengeResponse instanceof HexValue)) {
93+
throw new IllegalArgumentException(
94+
"A challenge response is a string or hexadecimal literal: "
95+
+ challengeResponse);
96+
}
97+
this.challengeResponse = challengeResponse;
98+
}
99+
100+
@Override
101+
public String toString() {
102+
String factorClause = factor + " FACTOR";
103+
switch (operation) {
104+
case ADD:
105+
case MODIFY:
106+
return operation + " " + factorClause + " " + authentication;
107+
case DROP:
108+
return "DROP " + factorClause;
109+
case INITIATE_REGISTRATION:
110+
return factorClause + " INITIATE REGISTRATION";
111+
case FINISH_REGISTRATION:
112+
return factorClause + " FINISH REGISTRATION SET CHALLENGE_RESPONSE AS "
113+
+ challengeResponse;
114+
case UNREGISTER:
115+
return factorClause + " UNREGISTER";
116+
default:
117+
throw new IllegalStateException("Unsupported factor operation: " + operation);
118+
}
119+
}
120+
121+
public UserFactorOperation withOperation(Operation operation) {
122+
setOperation(operation);
123+
return this;
124+
}
125+
126+
public UserFactorOperation withFactor(int factor) {
127+
setFactor(factor);
128+
return this;
129+
}
130+
131+
public UserFactorOperation withAuthentication(UserAuthentication authentication) {
132+
setAuthentication(authentication);
133+
return this;
134+
}
135+
136+
public UserFactorOperation withChallengeResponse(Expression challengeResponse) {
137+
setChallengeResponse(challengeResponse);
138+
return this;
139+
}
140+
}

0 commit comments

Comments
 (0)