Skip to content

Commit 19a3585

Browse files
authored
fix: parse MySQL account authentication as SHOW CREATE USER prints it (#2863)
SHOW CREATE USER prints the authentication plugin as a string literal and, with print_identified_with_as_hex enabled, a hash that contains unprintable characters as a hexadecimal literal. MySQL also accepts CURRENT_USER() wherever a user account is named. The plugin was read only as an identifier, AS took only a string literal, and an account name had no parentheses after CURRENT_USER; ALTER USER CURRENT_USER() also went to the PostgreSQL ALTER ROLE alias. The plugin reuses MySqlAccountNamePart, so it keeps its quote character in UserAuthentication.getPluginName(). The hash after AS may be a HexValue, returned by getCredentialValue(). UserAccount records CURRENT_USER() in user lists; role lists still reject it, as MySQL reads them with role_ident. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 23d3173 commit 19a3585

6 files changed

Lines changed: 227 additions & 26 deletions

File tree

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@
1212
import java.io.Serializable;
1313
import net.sf.jsqlparser.expression.StringValue;
1414

15-
/** A user and optional host together with its authentication clause. */
15+
/**
16+
* A user and optional host together with its authentication clause. The current user may be named
17+
* {@code CURRENT_USER} or {@code CURRENT_USER()}.
18+
*/
1619
public class UserAccount implements Serializable {
1720

1821
private StringValue user;
1922
private StringValue host;
2023
private UserAuthentication authentication;
24+
private boolean currentUserParentheses;
2125

2226
public StringValue getUser() {
2327
return user;
@@ -35,6 +39,15 @@ public void setHost(StringValue host) {
3539
this.host = host;
3640
}
3741

42+
/** Whether the user is written {@code CURRENT_USER()}. */
43+
public boolean isCurrentUserParentheses() {
44+
return currentUserParentheses;
45+
}
46+
47+
public void setCurrentUserParentheses(boolean currentUserParentheses) {
48+
this.currentUserParentheses = currentUserParentheses;
49+
}
50+
3851
public UserAuthentication getAuthentication() {
3952
return authentication;
4053
}
@@ -46,6 +59,9 @@ public void setAuthentication(UserAuthentication authentication) {
4659
@Override
4760
public String toString() {
4861
StringBuilder builder = new StringBuilder().append(user);
62+
if (currentUserParentheses) {
63+
builder.append("()");
64+
}
4965
if (host != null) {
5066
builder.append("@").append(host);
5167
}
@@ -65,6 +81,11 @@ public UserAccount withHost(StringValue host) {
6581
return this;
6682
}
6783

84+
public UserAccount withCurrentUserParentheses(boolean currentUserParentheses) {
85+
setCurrentUserParentheses(currentUserParentheses);
86+
return this;
87+
}
88+
6889
public UserAccount withAuthentication(UserAuthentication authentication) {
6990
setAuthentication(authentication);
7091
return this;

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

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
package net.sf.jsqlparser.statement.create.user;
1111

1212
import java.io.Serializable;
13+
import net.sf.jsqlparser.expression.Expression;
14+
import net.sf.jsqlparser.expression.HexValue;
1315
import net.sf.jsqlparser.expression.StringValue;
1416

1517
/**
1618
* Structured authentication clause of a MySQL user account. {@code ALTER USER} adds
1719
* {@code REPLACE 'current'} and {@code RETAIN CURRENT PASSWORD} to a password assignment, and
18-
* {@code DISCARD OLD PASSWORD} as a mode of its own.
20+
* {@code DISCARD OLD PASSWORD} as a mode of its own. The plugin name keeps its quote character, as
21+
* 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.
1923
*/
2024
public class UserAuthentication implements Serializable {
2125

@@ -30,8 +34,8 @@ public boolean isPasswordAssignment() {
3034
}
3135

3236
private Mode mode;
33-
private String plugin;
34-
private StringValue credential;
37+
private StringValue pluginName;
38+
private Expression credential;
3539
private StringValue currentCredential;
3640
private boolean retainCurrentPassword;
3741

@@ -43,22 +47,52 @@ public void setMode(Mode mode) {
4347
this.mode = mode;
4448
}
4549

50+
/** The plugin name without its quotes, or null without {@code IDENTIFIED WITH}. */
4651
public String getPlugin() {
47-
return plugin;
52+
return pluginName == null ? null : pluginName.getValue();
4853
}
4954

55+
/** Sets an unquoted plugin name. */
5056
public void setPlugin(String plugin) {
51-
this.plugin = plugin;
57+
this.pluginName =
58+
plugin == null ? null : new StringValue().withValue(plugin).setQuoteStr("");
5259
}
5360

61+
/** The plugin name with its quote character, which is empty for an unquoted name. */
62+
public StringValue getPluginName() {
63+
return pluginName;
64+
}
65+
66+
public void setPluginName(StringValue pluginName) {
67+
this.pluginName = pluginName;
68+
}
69+
70+
/**
71+
* The password or hash as a string literal, or null when there is none or the hash of
72+
* {@code AS} is a hexadecimal literal; {@link #getCredentialValue()} returns either.
73+
*/
5474
public StringValue getCredential() {
55-
return credential;
75+
return credential instanceof StringValue ? (StringValue) credential : null;
5676
}
5777

5878
public void setCredential(StringValue credential) {
5979
this.credential = credential;
6080
}
6181

82+
/** The password or hash as written: a {@link StringValue}, or a {@link HexValue} after AS. */
83+
public Expression getCredentialValue() {
84+
return credential;
85+
}
86+
87+
public void setCredentialValue(Expression credential) {
88+
if (credential != null && !(credential instanceof StringValue)
89+
&& !(credential instanceof HexValue)) {
90+
throw new IllegalArgumentException(
91+
"A credential is a string or hexadecimal literal: " + credential);
92+
}
93+
this.credential = credential;
94+
}
95+
6296
/** The current password of {@code REPLACE 'current'}, or null when omitted. */
6397
public StringValue getCurrentCredential() {
6498
return currentCredential;
@@ -96,13 +130,13 @@ private String modeToString() {
96130
case BY_RANDOM_PASSWORD:
97131
return "IDENTIFIED BY RANDOM PASSWORD";
98132
case WITH_PLUGIN:
99-
return "IDENTIFIED WITH " + plugin;
133+
return "IDENTIFIED WITH " + pluginName;
100134
case WITH_PLUGIN_BY_PASSWORD:
101-
return "IDENTIFIED WITH " + plugin + " BY " + credential;
135+
return "IDENTIFIED WITH " + pluginName + " BY " + credential;
102136
case WITH_PLUGIN_BY_RANDOM_PASSWORD:
103-
return "IDENTIFIED WITH " + plugin + " BY RANDOM PASSWORD";
137+
return "IDENTIFIED WITH " + pluginName + " BY RANDOM PASSWORD";
104138
case WITH_PLUGIN_AS_STRING:
105-
return "IDENTIFIED WITH " + plugin + " AS " + credential;
139+
return "IDENTIFIED WITH " + pluginName + " AS " + credential;
106140
case DISCARD_OLD_PASSWORD:
107141
return "DISCARD OLD PASSWORD";
108142
default:
@@ -120,11 +154,21 @@ public UserAuthentication withPlugin(String plugin) {
120154
return this;
121155
}
122156

157+
public UserAuthentication withPluginName(StringValue pluginName) {
158+
setPluginName(pluginName);
159+
return this;
160+
}
161+
123162
public UserAuthentication withCredential(StringValue credential) {
124163
setCredential(credential);
125164
return this;
126165
}
127166

167+
public UserAuthentication withCredentialValue(Expression credential) {
168+
setCredentialValue(credential);
169+
return this;
170+
}
171+
128172
public UserAuthentication withCurrentCredential(StringValue currentCredential) {
129173
setCurrentCredential(currentCredential);
130174
return this;

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2751,6 +2751,14 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
27512751
if (!condition) { throw new ParseException(message); }
27522752
}
27532753

2754+
/** MySQL role lists name roles with role_ident, which does not take CURRENT_USER(). */
2755+
private static List<UserAccount> requireRoleNames(List<UserAccount> roles) throws ParseException {
2756+
for (UserAccount role : roles) {
2757+
requireAccessSyntax(!role.isCurrentUserParentheses(), "A role list does not take CURRENT_USER()");
2758+
}
2759+
return roles;
2760+
}
2761+
27542762
private static <E extends Enum<E>> E accessEnum(Class<E> type, String value) throws ParseException {
27552763
try { return Enum.valueOf(type, value.toUpperCase(Locale.ROOT)); }
27562764
catch (IllegalArgumentException ex) { throw new ParseException("Invalid " + type.getSimpleName() + ": " + value); }
@@ -2830,14 +2838,15 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
28302838

28312839
/**
28322840
* Whether ALTER USER uses MySQL account syntax rather than the PostgreSQL alias of ALTER ROLE,
2833-
* which names one unquoted role followed by an action: IF EXISTS, USER(), a quoted name, a
2834-
* host, an account list, no action at all, or a MySQL account clause.
2841+
* which names one unquoted role followed by an action: IF EXISTS, USER(), CURRENT_USER(), a
2842+
* quoted name, a host, an account list, no action at all, or a MySQL account clause.
28352843
*/
28362844
private boolean isMySqlAlterUserAhead() {
28372845
if (getToken(1).kind != K_USER) { return false; }
28382846
Token name = getToken(2);
28392847
if (name.kind == K_IF || name.kind == S_CHAR_LITERAL
2840-
|| name.kind == K_USER && "(".equals(getToken(3).image)
2848+
|| (name.kind == K_USER || "CURRENT_USER".equalsIgnoreCase(name.image))
2849+
&& "(".equals(getToken(3).image)
28412850
|| isMySqlAccountHostAhead(2, false)) {
28422851
return true;
28432852
}
@@ -16734,6 +16743,11 @@ UserAccount MySqlAccountName():
1673416743
|
1673516744
part=MySqlAccountNamePart() { account.setUser(part); }
1673616745
[
16746+
// CURRENT_USER() names the current user like CURRENT_USER; a quoted part has quotes in its image
16747+
LOOKAHEAD({ "CURRENT_USER".equalsIgnoreCase(getToken(0).image)
16748+
&& "(".equals(getToken(1).image) && ")".equals(getToken(2).image) })
16749+
"(" ")" { account.setCurrentUserParentheses(true); }
16750+
|
1673716751
<K_AT_SIGN> part=MySqlAccountNamePart() { account.setHost(part); }
1673816752
|
1673916753
// a quoted user followed by an unquoted @host
@@ -16777,11 +16791,11 @@ RoleSelection MySqlRoleSelection(boolean setRole):
1677716791
{ selection = new RoleSelection(RoleSelection.Kind.ALL); }
1677816792
[
1677916793
LOOKAHEAD({ getToken(1).kind == K_EXCEPT }) <K_EXCEPT> roles=MySqlAccountNameList()
16780-
{ selection.setKind(RoleSelection.Kind.ALL_EXCEPT); selection.setRoles(roles); }
16794+
{ selection.setKind(RoleSelection.Kind.ALL_EXCEPT); selection.setRoles(requireRoleNames(roles)); }
1678116795
]
1678216796
|
1678316797
roles=MySqlAccountNameList()
16784-
{ selection = new RoleSelection(RoleSelection.Kind.ROLES).withRoles(roles); }
16798+
{ selection = new RoleSelection(RoleSelection.Kind.ROLES).withRoles(requireRoleNames(roles)); }
1678516799
)
1678616800
{
1678716801
requireAccessSyntax(setRole || selection.getKind() != RoleSelection.Kind.DEFAULT
@@ -16791,10 +16805,14 @@ RoleSelection MySqlRoleSelection(boolean setRole):
1679116805
}
1679216806
}
1679316807

16808+
/**
16809+
* IDENTIFIED BY or IDENTIFIED WITH. The plugin may be quoted like an account name part, and the
16810+
* hash after AS may be a hexadecimal literal, as SHOW CREATE USER prints both.
16811+
*/
1679416812
UserAuthentication MySqlUserAuthentication():
1679516813
{
1679616814
UserAuthentication authentication = new UserAuthentication();
16797-
String plugin = null;
16815+
StringValue plugin = null;
1679816816
Token credential = null;
1679916817
}
1680016818
{
@@ -16812,9 +16830,9 @@ UserAuthentication MySqlUserAuthentication():
1681216830
}
1681316831
)
1681416832
|
16815-
<K_WITH> plugin=RelObjectName() {
16833+
<K_WITH> plugin=MySqlAccountNamePart() {
1681616834
authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN);
16817-
authentication.setPlugin(plugin);
16835+
authentication.setPluginName(plugin);
1681816836
}
1681916837
[
1682016838
<K_BY>
@@ -16830,10 +16848,13 @@ UserAuthentication MySqlUserAuthentication():
1683016848
}
1683116849
)
1683216850
|
16833-
<K_AS> credential=<S_CHAR_LITERAL> {
16834-
authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN_AS_STRING);
16835-
authentication.setCredential(stringValue(credential));
16836-
}
16851+
<K_AS> { authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN_AS_STRING); }
16852+
(
16853+
credential=<S_CHAR_LITERAL> { authentication.setCredential(stringValue(credential)); }
16854+
|
16855+
credential=<S_HEX>
16856+
{ authentication.setCredentialValue(new HexValue(credential.image)); }
16857+
)
1683716858
]
1683816859
)
1683916860
{ return authentication; }
@@ -17141,7 +17162,7 @@ CreateRole MySqlCreateRole():
1714117162
{
1714217163
ContextualKeyword("ROLE")
1714317164
[ LOOKAHEAD(3) <K_IF> <K_NOT> <K_EXISTS> { role.setIfNotExists(true); } ]
17144-
roles=MySqlAccountNameList() { role.setRoles(roles); }
17165+
roles=MySqlAccountNameList() { role.setRoles(requireRoleNames(roles)); }
1714517166
{ return role; }
1714617167
}
1714717168

@@ -17180,7 +17201,8 @@ SetDefaultRoleStatement SetDefaultRoleStatement():
1718017201
{ statement.setRoles(roles); }
1718117202
[
1718217203
( <K_TO> | <K_FOR> { statement.setForKeyword(true); } )
17183-
accounts=MySqlAccountNameList() { statement.setAccounts(accounts); }
17204+
// MySQL reads these accounts as a role list
17205+
accounts=MySqlAccountNameList() { statement.setAccounts(requireRoleNames(accounts)); }
1718417206
]
1718517207
{ return statement; }
1718617208
}
@@ -19916,6 +19938,11 @@ void DropTargets(Drop drop):
1991619938
LOOKAHEAD({ drop.getObjectType().hasAccountTargets() })
1991719939
account=MySqlAccountName() { drop.addAccounts(account); }
1991819940
( "," account=MySqlAccountName() { drop.addAccounts(account); } )*
19941+
{
19942+
if (drop.getObjectType() == Drop.ObjectType.ROLE) {
19943+
requireRoleNames(drop.getAccounts());
19944+
}
19945+
}
1991919946
|
1992019947
name=DropObjectName() { drop.addNames(name); }
1992119948
( "," name=DropObjectName() { drop.addNames(name); } )*
@@ -23058,7 +23085,7 @@ PrivilegeClause AccessClause(boolean revoke, boolean defaults):
2305823085
( "," privilege=AccessPrivilege() { clause.getPrivileges().add(privilege); } )*
2305923086
<K_ON> target=AccessTarget(defaults) { clause.setTarget(target); }
2306023087
|
23061-
roles=MySqlAccountNameList() { clause.setRoleItems(roles); clause.setTarget(null); }
23088+
roles=MySqlAccountNameList() { clause.setRoleItems(requireRoleNames(roles)); clause.setTarget(null); }
2306223089
)
2306323090
( <K_TO> { requireAccessSyntax(!revoke, "REVOKE requires FROM"); } | <K_FROM> { requireAccessSyntax(revoke, "GRANT requires TO"); } )
2306423091
grantees=AccessGrantees() { clause.setGranteeItems(grantees); return clause; }

‎src/test/java/net/sf/jsqlparser/statement/alter/AlterUserTest.java‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,31 @@ void replacesCurrentPassword() throws JSQLParserException {
9292
assertTrue(authentication.isRetainCurrentPassword());
9393
}
9494

95+
@Test
96+
void namesTheCurrentUserWithParentheses() throws JSQLParserException {
97+
AlterUser alterUser = parse("ALTER USER CURRENT_USER() IDENTIFIED BY 'x' "
98+
+ "RETAIN CURRENT PASSWORD");
99+
UserAccount account = alterUser.getAccounts().get(0);
100+
101+
assertEquals("CURRENT_USER", account.getUser().getValue());
102+
assertTrue(account.isCurrentUserParentheses());
103+
assertFalse(alterUser.isUserFunction());
104+
assertFalse(parse("ALTER USER CURRENT_USER IDENTIFIED BY 'x'").getAccounts().get(0)
105+
.isCurrentUserParentheses());
106+
assertEquals("CURRENT_USER()", new UserAccount()
107+
.withUser(new StringValue("CURRENT_USER").setQuoteStr(""))
108+
.withCurrentUserParentheses(true).toString());
109+
}
110+
111+
@ParameterizedTest
112+
@ValueSource(strings = {"ALTER USER 'CURRENT_USER'() IDENTIFIED BY 'x'",
113+
"ALTER USER app() IDENTIFIED BY 'x'",
114+
"ALTER USER CURRENT_USER()@localhost ACCOUNT LOCK"})
115+
void takesParenthesesOnlyAfterAnUnquotedCurrentUser(String sql) {
116+
assertThrows(JSQLParserException.class,
117+
() -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)));
118+
}
119+
95120
@Test
96121
void altersTheCurrentUserWithUserFunction() throws JSQLParserException {
97122
AlterUser alterUser = parse("ALTER USER USER() IDENTIFIED BY 'x'");

0 commit comments

Comments
 (0)