Skip to content

Commit ac9c60a

Browse files
committed
fix: parse MySQL account authentication as SHOW CREATE USER prints it
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 5d07ba4 commit ac9c60a

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
@@ -2761,6 +2761,14 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
27612761
if (!condition) { throw new ParseException(message); }
27622762
}
27632763

2764+
/** MySQL role lists name roles with role_ident, which does not take CURRENT_USER(). */
2765+
private static List<UserAccount> requireRoleNames(List<UserAccount> roles) throws ParseException {
2766+
for (UserAccount role : roles) {
2767+
requireAccessSyntax(!role.isCurrentUserParentheses(), "A role list does not take CURRENT_USER()");
2768+
}
2769+
return roles;
2770+
}
2771+
27642772
private static <E extends Enum<E>> E accessEnum(Class<E> type, String value) throws ParseException {
27652773
try { return Enum.valueOf(type, value.toUpperCase(Locale.ROOT)); }
27662774
catch (IllegalArgumentException ex) { throw new ParseException("Invalid " + type.getSimpleName() + ": " + value); }
@@ -2840,14 +2848,15 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
28402848

28412849
/**
28422850
* Whether ALTER USER uses MySQL account syntax rather than the PostgreSQL alias of ALTER ROLE,
2843-
* which names one unquoted role followed by an action: IF EXISTS, USER(), a quoted name, a
2844-
* host, an account list, no action at all, or a MySQL account clause.
2851+
* which names one unquoted role followed by an action: IF EXISTS, USER(), CURRENT_USER(), a
2852+
* quoted name, a host, an account list, no action at all, or a MySQL account clause.
28452853
*/
28462854
private boolean isMySqlAlterUserAhead() {
28472855
if (getToken(1).kind != K_USER) { return false; }
28482856
Token name = getToken(2);
28492857
if (name.kind == K_IF || name.kind == S_CHAR_LITERAL
2850-
|| name.kind == K_USER && "(".equals(getToken(3).image)
2858+
|| (name.kind == K_USER || "CURRENT_USER".equalsIgnoreCase(name.image))
2859+
&& "(".equals(getToken(3).image)
28512860
|| isMySqlAccountHostAhead(2, false)) {
28522861
return true;
28532862
}
@@ -16735,6 +16744,11 @@ UserAccount MySqlAccountName():
1673516744
|
1673616745
part=MySqlAccountNamePart() { account.setUser(part); }
1673716746
[
16747+
// CURRENT_USER() names the current user like CURRENT_USER; a quoted part has quotes in its image
16748+
LOOKAHEAD({ "CURRENT_USER".equalsIgnoreCase(getToken(0).image)
16749+
&& "(".equals(getToken(1).image) && ")".equals(getToken(2).image) })
16750+
"(" ")" { account.setCurrentUserParentheses(true); }
16751+
|
1673816752
<K_AT_SIGN> part=MySqlAccountNamePart() { account.setHost(part); }
1673916753
|
1674016754
// a quoted user followed by an unquoted @host
@@ -16778,11 +16792,11 @@ RoleSelection MySqlRoleSelection(boolean setRole):
1677816792
{ selection = new RoleSelection(RoleSelection.Kind.ALL); }
1677916793
[
1678016794
LOOKAHEAD({ getToken(1).kind == K_EXCEPT }) <K_EXCEPT> roles=MySqlAccountNameList()
16781-
{ selection.setKind(RoleSelection.Kind.ALL_EXCEPT); selection.setRoles(roles); }
16795+
{ selection.setKind(RoleSelection.Kind.ALL_EXCEPT); selection.setRoles(requireRoleNames(roles)); }
1678216796
]
1678316797
|
1678416798
roles=MySqlAccountNameList()
16785-
{ selection = new RoleSelection(RoleSelection.Kind.ROLES).withRoles(roles); }
16799+
{ selection = new RoleSelection(RoleSelection.Kind.ROLES).withRoles(requireRoleNames(roles)); }
1678616800
)
1678716801
{
1678816802
requireAccessSyntax(setRole || selection.getKind() != RoleSelection.Kind.DEFAULT
@@ -16792,10 +16806,14 @@ RoleSelection MySqlRoleSelection(boolean setRole):
1679216806
}
1679316807
}
1679416808

16809+
/**
16810+
* IDENTIFIED BY or IDENTIFIED WITH. The plugin may be quoted like an account name part, and the
16811+
* hash after AS may be a hexadecimal literal, as SHOW CREATE USER prints both.
16812+
*/
1679516813
UserAuthentication MySqlUserAuthentication():
1679616814
{
1679716815
UserAuthentication authentication = new UserAuthentication();
16798-
String plugin = null;
16816+
StringValue plugin = null;
1679916817
Token credential = null;
1680016818
}
1680116819
{
@@ -16813,9 +16831,9 @@ UserAuthentication MySqlUserAuthentication():
1681316831
}
1681416832
)
1681516833
|
16816-
<K_WITH> plugin=RelObjectName() {
16834+
<K_WITH> plugin=MySqlAccountNamePart() {
1681716835
authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN);
16818-
authentication.setPlugin(plugin);
16836+
authentication.setPluginName(plugin);
1681916837
}
1682016838
[
1682116839
<K_BY>
@@ -16831,10 +16849,13 @@ UserAuthentication MySqlUserAuthentication():
1683116849
}
1683216850
)
1683316851
|
16834-
<K_AS> credential=<S_CHAR_LITERAL> {
16835-
authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN_AS_STRING);
16836-
authentication.setCredential(stringValue(credential));
16837-
}
16852+
<K_AS> { authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN_AS_STRING); }
16853+
(
16854+
credential=<S_CHAR_LITERAL> { authentication.setCredential(stringValue(credential)); }
16855+
|
16856+
credential=<S_HEX>
16857+
{ authentication.setCredentialValue(new HexValue(credential.image)); }
16858+
)
1683816859
]
1683916860
)
1684016861
{ return authentication; }
@@ -17142,7 +17163,7 @@ CreateRole MySqlCreateRole():
1714217163
{
1714317164
ContextualKeyword("ROLE")
1714417165
[ LOOKAHEAD(3) <K_IF> <K_NOT> <K_EXISTS> { role.setIfNotExists(true); } ]
17145-
roles=MySqlAccountNameList() { role.setRoles(roles); }
17166+
roles=MySqlAccountNameList() { role.setRoles(requireRoleNames(roles)); }
1714617167
{ return role; }
1714717168
}
1714817169

@@ -17181,7 +17202,8 @@ SetDefaultRoleStatement SetDefaultRoleStatement():
1718117202
{ statement.setRoles(roles); }
1718217203
[
1718317204
( <K_TO> | <K_FOR> { statement.setForKeyword(true); } )
17184-
accounts=MySqlAccountNameList() { statement.setAccounts(accounts); }
17205+
// MySQL reads these accounts as a role list
17206+
accounts=MySqlAccountNameList() { statement.setAccounts(requireRoleNames(accounts)); }
1718517207
]
1718617208
{ return statement; }
1718717209
}
@@ -19917,6 +19939,11 @@ void DropTargets(Drop drop):
1991719939
LOOKAHEAD({ drop.getObjectType().hasAccountTargets() })
1991819940
account=MySqlAccountName() { drop.addAccounts(account); }
1991919941
( "," account=MySqlAccountName() { drop.addAccounts(account); } )*
19942+
{
19943+
if (drop.getObjectType() == Drop.ObjectType.ROLE) {
19944+
requireRoleNames(drop.getAccounts());
19945+
}
19946+
}
1992019947
|
1992119948
name=DropObjectName() { drop.addNames(name); }
1992219949
( "," name=DropObjectName() { drop.addNames(name); } )*
@@ -23059,7 +23086,7 @@ PrivilegeClause AccessClause(boolean revoke, boolean defaults):
2305923086
( "," privilege=AccessPrivilege() { clause.getPrivileges().add(privilege); } )*
2306023087
<K_ON> target=AccessTarget(defaults) { clause.setTarget(target); }
2306123088
|
23062-
roles=MySqlAccountNameList() { clause.setRoleItems(roles); clause.setTarget(null); }
23089+
roles=MySqlAccountNameList() { clause.setRoleItems(requireRoleNames(roles)); clause.setTarget(null); }
2306323090
)
2306423091
( <K_TO> { requireAccessSyntax(!revoke, "REVOKE requires FROM"); } | <K_FROM> { requireAccessSyntax(revoke, "GRANT requires TO"); } )
2306523092
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)