PasswordPolicy.java
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.mysql.privilege;
import org.apache.doris.analysis.PasswordOptions;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.common.AuthenticationException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.io.Text;
import org.apache.doris.common.io.Writable;
import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.common.util.Util;
import org.apache.doris.persist.gson.GsonUtils;
import org.apache.doris.qe.GlobalVariable;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Queues;
import com.google.gson.annotations.SerializedName;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Password policy for a specific user.
* Current support:
* PASSWORD EXPIRE
* PASSWORD HISTORY
* FAILED_LOGIN_ATTEMPTS
* PASSWORD_LOCK_TIME
*/
public class PasswordPolicy implements Writable {
private static final Logger LOG = LogManager.getLogger(PasswordPolicy.class);
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final String EXPIRATION_SECONDS = "password_policy.expiration_seconds";
private static final String PASSWORD_CREATION_TIME = "password_policy.password_creation_time";
private static final String HISTORY_NUM = "password_policy.history_num";
private static final String HISTORY_PASSWORDS = "password_policy.history_passwords";
private static final String NUM_FAILED_LOGIN = "password_policy.num_failed_login";
private static final String PASSWORD_LOCK_SECONDS = "password_policy.password_lock_seconds";
private static final String FAILED_LOGIN_COUNTER = "password_policy.failed_login_counter";
private static final String LOCK_TIME = "password_policy.lock_time";
@SerializedName(value = "expirePolicy")
private ExpirePolicy expirePolicy = new ExpirePolicy();
@SerializedName(value = "historyPolicy")
private HistoryPolicy historyPolicy = new HistoryPolicy();
@SerializedName(value = "failedLoginPolicy")
private FailedLoginPolicy failedLoginPolicy = new FailedLoginPolicy();
public PasswordPolicy() {
}
public static PasswordPolicy createDefault() {
return new PasswordPolicy();
}
public void checkAccountLockedAndPasswordExpiration(UserIdentity curUser) throws AuthenticationException {
lock.readLock().lock();
try {
if (expirePolicy.isExpire()) {
throw new AuthenticationException(ErrorCode.ERR_MUST_CHANGE_PASSWORD_LOGIN);
}
if (failedLoginPolicy.isLocked()) {
throw new AuthenticationException(
ErrorCode.ERR_USER_ACCESS_DENIED_FOR_USER_ACCOUNT_BLOCKED_BY_PASSWORD_LOCK,
curUser.getQualifiedUser(), curUser.getHost(),
failedLoginPolicy.passwordLockSeconds,
failedLoginPolicy.leftSeconds(),
failedLoginPolicy.failedLoginCounter);
}
} finally {
lock.readLock().unlock();
}
}
public boolean onFailedLogin() {
lock.writeLock().lock();
try {
return failedLoginPolicy.onFailedLogin();
} finally {
lock.writeLock().unlock();
}
}
public boolean checkPasswordHistory(byte[] password) {
lock.readLock().lock();
try {
return historyPolicy.isPasswordAllowed(password);
} finally {
lock.readLock().unlock();
}
}
public void update(byte[] password, PasswordOptions passwordOptions) {
lock.writeLock().lock();
try {
expirePolicy.update(passwordOptions.getExpirePolicySecond());
historyPolicy.update(password, passwordOptions.getHistoryPolicy());
failedLoginPolicy.updateNumFailedLogin(passwordOptions.getLoginAttempts());
failedLoginPolicy.updatePasswordLockSeconds(passwordOptions.getPasswordLockSecond());
} finally {
lock.writeLock().unlock();
}
}
public void updatePassword(byte[] password) {
lock.writeLock().lock();
try {
historyPolicy.addPassword(password);
} finally {
lock.writeLock().unlock();
}
}
public ExpirePolicy getExpirePolicy() {
return expirePolicy;
}
@Override
public void write(DataOutput out) throws IOException {
Text.writeString(out, GsonUtils.GSON.toJson(this));
}
public static PasswordPolicy read(DataInput in) throws IOException {
return GsonUtils.GSON.fromJson(Text.readString(in), PasswordPolicy.class);
}
public List<List<String>> getInfo() {
lock.readLock().lock();
try {
List<List<String>> rows = Lists.newArrayList();
expirePolicy.getInfo(rows);
historyPolicy.getInfo(rows);
failedLoginPolicy.getInfo(rows);
return rows;
} finally {
lock.readLock().unlock();
}
}
public void unlockAccount() {
lock.writeLock().lock();
try {
failedLoginPolicy.unlock();
} finally {
lock.writeLock().unlock();
}
}
/**
* Password expire policy.
* If a password is expired, user can no longer login with this password
*/
public static class ExpirePolicy implements Writable {
// -1: default, will use session variable: default_password_lifetime
// 0: never
// > 0: seconds
public static final int DEFAULT = -1;
public static final int NEVER = 0;
@SerializedName(value = "expirationSecond")
public long expirationSecond = NEVER;
@SerializedName(value = "passwordCreateTime")
public long passwordCreateTime = 0;
public boolean isExpire() {
return leftSeconds() <= 0;
}
public long leftSeconds() {
long tmp = expirationSecond;
if (tmp == -1) {
tmp = GlobalVariable.defaultPasswordLifetime * 86400;
}
if (tmp == 0) {
return Long.MAX_VALUE;
}
return tmp - (System.currentTimeMillis() - passwordCreateTime) / 1000;
}
public void update(long expirationSecond) {
if (expirationSecond == PasswordOptions.UNSET) {
return;
}
this.expirationSecond = expirationSecond;
this.passwordCreateTime = System.currentTimeMillis();
}
public void setPasswordCreateTime() {
this.passwordCreateTime = System.currentTimeMillis();
}
private String expirationSecondsToString() {
if (expirationSecond == -1) {
return "DEFAULT";
} else if (expirationSecond == 0) {
return "NEVER";
} else {
return String.valueOf(expirationSecond);
}
}
public void getInfo(List<List<String>> rows) {
List<String> row1 = Lists.newArrayList();
row1.add(EXPIRATION_SECONDS);
row1.add(expirationSecondsToString());
List<String> row2 = Lists.newArrayList();
row2.add(PASSWORD_CREATION_TIME);
row2.add(passwordCreateTime == 0 ? "" : TimeUtils.longToTimeString(passwordCreateTime));
rows.add(row1);
rows.add(row2);
}
@Override
public void write(DataOutput out) throws IOException {
Text.writeString(out, GsonUtils.GSON.toJson(this));
}
public static ExpirePolicy read(DataInput in) throws IOException {
return GsonUtils.GSON.fromJson(Text.readString(in), ExpirePolicy.class);
}
}
/**
* Password history num.
* The password saved in this policy can not be reused.
*/
public static class HistoryPolicy implements Writable {
public static final int MAX_HISTORY_SIZE = 10;
public static final int DEFAULT = -1;
public static final int NO_RESTRICTION = 0;
@SerializedName(value = "historyPasswords")
public Queue<byte[]> historyPasswords = Queues.newArrayDeque();
// -1: default, will use session variable: password_history
// 0: no reuse restriction
// > 0: number of history password that can not be reused
@SerializedName(value = "historyNum")
public int historyNum = NO_RESTRICTION;
public boolean isPasswordAllowed(byte[] password) {
if (historyNum == NO_RESTRICTION) {
return true;
}
Iterator<byte[]> iter = historyPasswords.iterator();
int tmp = historyNum;
if (tmp == DEFAULT) {
tmp = GlobalVariable.passwordHistory;
}
// if number of password saved in historyPasswords is more than historyNum,
// we need to move forward to pass those history password that do not need to check.
int forward = historyPasswords.size() <= tmp ? 0 : (historyPasswords.size() - tmp);
while (forward-- > 0) {
iter.next();
}
while (iter.hasNext()) {
byte[] history = iter.next();
if (Arrays.equals(history, password)) {
return false;
}
}
return true;
}
public void addPassword(byte[] password) {
if (historyPasswords.size() == MAX_HISTORY_SIZE) {
historyPasswords.poll();
}
historyPasswords.add(password);
}
public void update(byte[] password, int historyNum) {
if (historyNum != PasswordOptions.UNSET) {
this.historyNum = historyNum;
this.historyPasswords.clear();
}
if (password != null) {
this.historyPasswords.add(password);
}
}
private String historyNumToString() {
switch (historyNum) {
case -1:
return "DEFAULT";
case 0:
return "NO_RESTRICTION";
default:
return String.valueOf(historyNum);
}
}
public void getInfo(List<List<String>> rows) {
List<String> row1 = Lists.newArrayList();
row1.add(HISTORY_NUM);
row1.add(historyNumToString());
List<String> row2 = Lists.newArrayList();
row2.add(HISTORY_PASSWORDS);
List<String> hexPasswords = Lists.newArrayList();
Iterator<byte[]> iter = historyPasswords.iterator();
// if number of password saved in historyPasswords is more than historyNum,
// we need to move forward to pass those history password that do not need to check.
int tmp = historyNum;
if (tmp == DEFAULT) {
tmp = GlobalVariable.passwordHistory;
}
int forward = historyPasswords.size() <= tmp ? 0 : (historyPasswords.size() - tmp);
while (forward-- > 0) {
iter.next();
}
while (iter.hasNext()) {
byte[] history = iter.next();
String hex = Util.bytesToHex(history);
hexPasswords.add("*" + hex.substring(0, Math.min(3, hex.length())) + "...");
}
row2.add(Joiner.on(",").join(hexPasswords));
rows.add(row1);
rows.add(row2);
}
@Override
public void write(DataOutput out) throws IOException {
Text.writeString(out, GsonUtils.GSON.toJson(this));
}
public static HistoryPolicy read(DataInput in) throws IOException {
return GsonUtils.GSON.fromJson(Text.readString(in), HistoryPolicy.class);
}
}
/**
* If a user is failed to login for a certain time,
* the account will be locked for a certain period.
*/
public static class FailedLoginPolicy implements Writable {
public static final int DISABLED = 0;
public static final int UNBOUNDED = -1;
public static final int LOCK_ACCOUNT = -1;
public static final int UNLOCK_ACCOUNT = 1;
// 0: disabled
// > 0: num of failed login
@SerializedName(value = "numFailedLogin")
public int numFailedLogin = DISABLED;
// -1: unbounded
// 0: disabled
// > 0: lock time (seconds)
@SerializedName(value = "passwordLockSeconds")
public long passwordLockSeconds = DISABLED;
// num of current failed login
// This field will not persist, so that each FE is independent.
// And after a FE restart, it will be reset to 0.
// Use atomic because it will be visited and updated by multi threads.
public AtomicLong failedLoginCounter = new AtomicLong(0);
// time when the account being locked.
// Same as failedLoginCounter, not persist
public AtomicLong lockTime = new AtomicLong(0);
// Return true if the account is being locked.
// Return false if nothing happen.
public boolean onFailedLogin() {
if (numFailedLogin == DISABLED || passwordLockSeconds == DISABLED) {
// This policy is disabled, nothing happen
return false;
}
if (failedLoginCounter.get() >= numFailedLogin) {
return true;
}
if (failedLoginCounter.incrementAndGet() >= numFailedLogin) {
lockTime.set(System.currentTimeMillis());
return true;
}
return false;
}
public boolean isLocked() {
return leftSeconds() > 0;
}
public long leftSeconds() {
if (numFailedLogin == DISABLED || passwordLockSeconds == DISABLED || lockTime.get() == 0) {
// This policy is disabled or not locked, return
return 0;
}
if (lockTime.get() > 0 && passwordLockSeconds == UNBOUNDED) {
// unbounded lock
// Returns 9999 seconds every time instead of 9999 seconds countdown
return 9999;
}
return Math.max(0, passwordLockSeconds - ((System.currentTimeMillis() - lockTime.get()) / 1000));
}
public void updateNumFailedLogin(int numFailedLogin) {
if (numFailedLogin == PasswordOptions.UNSET) {
return;
}
this.numFailedLogin = numFailedLogin;
unlock();
}
public void updatePasswordLockSeconds(long passwordLockSeconds) {
if (passwordLockSeconds == PasswordOptions.UNSET) {
return;
}
this.passwordLockSeconds = passwordLockSeconds;
unlock();
}
public void unlock() {
this.failedLoginCounter.set(0);
this.lockTime.set(0);
}
@Override
public void write(DataOutput out) throws IOException {
Text.writeString(out, GsonUtils.GSON.toJson(this));
}
public static FailedLoginPolicy read(DataInput in) throws IOException {
return GsonUtils.GSON.fromJson(Text.readString(in), FailedLoginPolicy.class);
}
private String passwordLockSecondsToString() {
if (passwordLockSeconds == -1) {
return "UNBOUNDED";
} else if (passwordLockSeconds == 0) {
return "DISABLED";
} else {
return String.valueOf(passwordLockSeconds);
}
}
private String numFailedLoginToString() {
switch (numFailedLogin) {
case 0:
return "DISABLED";
default:
return String.valueOf(numFailedLogin);
}
}
public void getInfo(List<List<String>> rows) {
List<String> row1 = Lists.newArrayList();
row1.add(NUM_FAILED_LOGIN);
row1.add(numFailedLoginToString());
List<String> row2 = Lists.newArrayList();
row2.add(PASSWORD_LOCK_SECONDS);
row2.add(passwordLockSecondsToString());
List<String> row3 = Lists.newArrayList();
row3.add(FAILED_LOGIN_COUNTER);
row3.add(String.valueOf(failedLoginCounter.get()));
List<String> row4 = Lists.newArrayList();
row4.add(LOCK_TIME);
row4.add(lockTime.get() == 0 ? "" : TimeUtils.longToTimeString(lockTime.get()));
rows.add(row1);
rows.add(row2);
rows.add(row3);
rows.add(row4);
}
}
}