* commit '69fd61cb9a487e5993098c93113d86c4fb32c304': Add StrongAuthTracker
This commit is contained in:
@@ -90,6 +90,7 @@ LOCAL_SRC_FILES += \
|
||||
core/java/android/app/IWallpaperManager.aidl \
|
||||
core/java/android/app/IWallpaperManagerCallback.aidl \
|
||||
core/java/android/app/admin/IDevicePolicyManager.aidl \
|
||||
core/java/android/app/trust/IStrongAuthTracker.aidl \
|
||||
core/java/android/app/trust/ITrustManager.aidl \
|
||||
core/java/android/app/trust/ITrustListener.aidl \
|
||||
core/java/android/app/backup/IBackupManager.aidl \
|
||||
|
||||
26
core/java/android/app/trust/IStrongAuthTracker.aidl
Normal file
26
core/java/android/app/trust/IStrongAuthTracker.aidl
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
**
|
||||
** Copyright 2015, The Android Open Source Project
|
||||
**
|
||||
** Licensed 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 android.app.trust;
|
||||
|
||||
/**
|
||||
* Private API to be notified about strong auth changes
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
oneway interface IStrongAuthTracker {
|
||||
void onStrongAuthRequiredChanged(int strongAuthRequired, int userId);
|
||||
}
|
||||
@@ -26,11 +26,9 @@ import android.app.trust.ITrustListener;
|
||||
interface ITrustManager {
|
||||
void reportUnlockAttempt(boolean successful, int userId);
|
||||
void reportEnabledTrustAgentsChanged(int userId);
|
||||
void reportRequireCredentialEntry(int userId);
|
||||
void registerTrustListener(in ITrustListener trustListener);
|
||||
void unregisterTrustListener(in ITrustListener trustListener);
|
||||
void reportKeyguardShowingChanged();
|
||||
boolean isDeviceLocked(int userId);
|
||||
boolean isDeviceSecure(int userId);
|
||||
boolean hasUserAuthenticatedSinceBoot(int userId);
|
||||
}
|
||||
|
||||
@@ -16,13 +16,19 @@
|
||||
|
||||
package android.app.trust;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* See {@link com.android.server.trust.TrustManagerService}
|
||||
@@ -72,21 +78,6 @@ public class TrustManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that trust is disabled until credentials have been entered for user {@param userId}.
|
||||
*
|
||||
* Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
|
||||
*
|
||||
* @param userId either an explicit user id or {@link android.os.UserHandle#USER_ALL}
|
||||
*/
|
||||
public void reportRequireCredentialEntry(int userId) {
|
||||
try {
|
||||
mService.reportRequireCredentialEntry(userId);
|
||||
} catch (RemoteException e) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that the visibility of the keyguard has changed.
|
||||
*
|
||||
@@ -147,23 +138,6 @@ public class TrustManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified user has been authenticated since the last boot.
|
||||
*
|
||||
* @param userId the user id of the user to check for
|
||||
* @return true if the user has authenticated since boot, false otherwise
|
||||
*
|
||||
* Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
|
||||
*/
|
||||
public boolean hasUserAuthenticatedSinceBoot(int userId) {
|
||||
try {
|
||||
return mService.hasUserAuthenticatedSinceBoot(userId);
|
||||
} catch (RemoteException e) {
|
||||
onError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void onError(Exception e) {
|
||||
Log.e(TAG, "Error while calling TrustManagerService", e);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.app.trust.IStrongAuthTracker;
|
||||
import com.android.internal.widget.VerifyCredentialResponse;
|
||||
|
||||
/** {@hide} */
|
||||
@@ -35,4 +36,7 @@ interface ILockSettings {
|
||||
boolean checkVoldPassword(int userId);
|
||||
boolean havePattern(int userId);
|
||||
boolean havePassword(int userId);
|
||||
void registerStrongAuthTracker(in IStrongAuthTracker tracker);
|
||||
void unregisterStrongAuthTracker(in IStrongAuthTracker tracker);
|
||||
void requireStrongAuth(int strongAuthReason, int userId);
|
||||
}
|
||||
|
||||
@@ -16,18 +16,19 @@
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.IntDef;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManagerNative;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.app.trust.IStrongAuthTracker;
|
||||
import android.app.trust.TrustManager;
|
||||
import android.bluetooth.BluetoothClass;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.SystemClock;
|
||||
@@ -38,9 +39,12 @@ import android.os.storage.StorageManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import com.google.android.collect.Lists;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -228,7 +232,7 @@ public class LockPatternUtils {
|
||||
public void reportFailedPasswordAttempt(int userId) {
|
||||
getDevicePolicyManager().reportFailedPasswordAttempt(userId);
|
||||
getTrustManager().reportUnlockAttempt(false /* authenticated */, userId);
|
||||
getTrustManager().reportRequireCredentialEntry(userId);
|
||||
requireCredentialEntry(userId);
|
||||
}
|
||||
|
||||
public void reportSuccessfulPasswordAttempt(int userId) {
|
||||
@@ -1163,10 +1167,32 @@ public class LockPatternUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see android.app.trust.TrustManager#reportRequireCredentialEntry(int)
|
||||
* Disable trust until credentials have been entered for user {@param userId}.
|
||||
*
|
||||
* Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
|
||||
*
|
||||
* @param userId either an explicit user id or {@link android.os.UserHandle#USER_ALL}
|
||||
*/
|
||||
public void requireCredentialEntry(int userId) {
|
||||
getTrustManager().reportRequireCredentialEntry(userId);
|
||||
requireStrongAuth(StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests strong authentication for user {@param userId}.
|
||||
*
|
||||
* Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
|
||||
*
|
||||
* @param strongAuthReason a combination of {@link StrongAuthTracker.StrongAuthFlags} indicating
|
||||
* the reason for and the strength of the requested authentication.
|
||||
* @param userId either an explicit user id or {@link android.os.UserHandle#USER_ALL}
|
||||
*/
|
||||
public void requireStrongAuth(@StrongAuthTracker.StrongAuthFlags int strongAuthReason,
|
||||
int userId) {
|
||||
try {
|
||||
getLockSettings().requireStrongAuth(strongAuthReason, userId);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Error while requesting strong auth: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAfterChangingPassword(int userHandle) {
|
||||
@@ -1198,4 +1224,148 @@ public class LockPatternUtils {
|
||||
private boolean shouldEncryptWithCredentials(boolean defaultValue) {
|
||||
return isCredentialRequiredToDecrypt(defaultValue) && !isDoNotAskCredentialsOnBootSet();
|
||||
}
|
||||
|
||||
|
||||
public void registerStrongAuthTracker(final StrongAuthTracker strongAuthTracker) {
|
||||
try {
|
||||
getLockSettings().registerStrongAuthTracker(strongAuthTracker.mStub);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException("Could not register StrongAuthTracker");
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterStrongAuthTracker(final StrongAuthTracker strongAuthTracker) {
|
||||
try {
|
||||
getLockSettings().unregisterStrongAuthTracker(strongAuthTracker.mStub);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Could not unregister StrongAuthTracker", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the global strong authentication state.
|
||||
*/
|
||||
public static class StrongAuthTracker {
|
||||
|
||||
@IntDef(flag = true,
|
||||
value = { STRONG_AUTH_NOT_REQUIRED,
|
||||
STRONG_AUTH_REQUIRED_AFTER_BOOT,
|
||||
STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW,
|
||||
SOME_AUTH_REQUIRED_AFTER_USER_REQUEST})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface StrongAuthFlags {}
|
||||
|
||||
/**
|
||||
* Strong authentication is not required.
|
||||
*/
|
||||
public static final int STRONG_AUTH_NOT_REQUIRED = 0x0;
|
||||
|
||||
/**
|
||||
* Strong authentication is required because the user has not authenticated since boot.
|
||||
*/
|
||||
public static final int STRONG_AUTH_REQUIRED_AFTER_BOOT = 0x1;
|
||||
|
||||
/**
|
||||
* Strong authentication is required because a device admin has requested it.
|
||||
*/
|
||||
public static final int STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW = 0x2;
|
||||
|
||||
/**
|
||||
* Some authentication is required because the user has temporarily disabled trust.
|
||||
*/
|
||||
public static final int SOME_AUTH_REQUIRED_AFTER_USER_REQUEST = 0x4;
|
||||
|
||||
public static final int DEFAULT = STRONG_AUTH_REQUIRED_AFTER_BOOT;
|
||||
private static final int ALLOWING_FINGERPRINT = SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
|
||||
|
||||
final SparseIntArray mStrongAuthRequiredForUser = new SparseIntArray();
|
||||
|
||||
private final H mHandler;
|
||||
|
||||
public StrongAuthTracker() {
|
||||
this(Looper.myLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param looper the looper on whose thread calls to {@link #onStrongAuthRequiredChanged}
|
||||
* will be scheduled.
|
||||
*/
|
||||
public StrongAuthTracker(Looper looper) {
|
||||
mHandler = new H(looper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link #STRONG_AUTH_NOT_REQUIRED} if strong authentication is not required,
|
||||
* otherwise returns a combination of {@link StrongAuthFlags} indicating why strong
|
||||
* authentication is required.
|
||||
*
|
||||
* @param userId the user for whom the state is queried.
|
||||
*/
|
||||
public @StrongAuthFlags int getStrongAuthForUser(int userId) {
|
||||
return mStrongAuthRequiredForUser.get(userId, DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if unlocking with trust alone is allowed for {@param userId} by the current
|
||||
* strong authentication requirements.
|
||||
*/
|
||||
public boolean isTrustAllowedForUser(int userId) {
|
||||
return getStrongAuthForUser(userId) == STRONG_AUTH_NOT_REQUIRED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if unlocking with fingerprint alone is allowed for {@param userId} by the
|
||||
* current strong authentication requirements.
|
||||
*/
|
||||
public boolean isFingerprintAllowedForUser(int userId) {
|
||||
return (getStrongAuthForUser(userId) & ~ALLOWING_FINGERPRINT) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the strong authentication requirements for {@param userId} changed.
|
||||
*/
|
||||
public void onStrongAuthRequiredChanged(int userId) {
|
||||
}
|
||||
|
||||
void handleStrongAuthRequiredChanged(@StrongAuthFlags int strongAuthFlags,
|
||||
int userId) {
|
||||
|
||||
int oldValue = getStrongAuthForUser(userId);
|
||||
if (strongAuthFlags != oldValue) {
|
||||
if (strongAuthFlags == DEFAULT) {
|
||||
mStrongAuthRequiredForUser.delete(userId);
|
||||
} else {
|
||||
mStrongAuthRequiredForUser.put(userId, strongAuthFlags);
|
||||
}
|
||||
onStrongAuthRequiredChanged(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final IStrongAuthTracker.Stub mStub = new IStrongAuthTracker.Stub() {
|
||||
@Override
|
||||
public void onStrongAuthRequiredChanged(@StrongAuthFlags int strongAuthFlags,
|
||||
int userId) {
|
||||
mHandler.obtainMessage(H.MSG_ON_STRONG_AUTH_REQUIRED_CHANGED,
|
||||
strongAuthFlags, userId).sendToTarget();
|
||||
}
|
||||
};
|
||||
|
||||
private class H extends Handler {
|
||||
static final int MSG_ON_STRONG_AUTH_REQUIRED_CHANGED = 1;
|
||||
|
||||
public H(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_ON_STRONG_AUTH_REQUIRED_CHANGED:
|
||||
handleStrongAuthRequiredChanged(msg.arg1, msg.arg2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import com.android.internal.telephony.IccCardConstants;
|
||||
import com.android.internal.telephony.IccCardConstants.State;
|
||||
import com.android.internal.telephony.PhoneConstants;
|
||||
import com.android.internal.telephony.TelephonyIntents;
|
||||
import com.android.internal.widget.LockPatternUtils;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
@@ -170,7 +171,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
|
||||
private boolean mFingerprintAlreadyAuthenticated;
|
||||
private boolean mBouncer;
|
||||
private boolean mBootCompleted;
|
||||
private boolean mUserHasAuthenticatedSinceBoot;
|
||||
|
||||
// Device provisioning state
|
||||
private boolean mDeviceProvisioned;
|
||||
@@ -183,6 +183,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
|
||||
|
||||
/** Tracks whether strong authentication hasn't been used since quite some time per user. */
|
||||
private ArraySet<Integer> mStrongAuthTimedOut = new ArraySet<>();
|
||||
private final StrongAuthTracker mStrongAuthTracker = new StrongAuthTracker();
|
||||
|
||||
private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>>
|
||||
mCallbacks = Lists.newArrayList();
|
||||
@@ -539,7 +540,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
|
||||
}
|
||||
|
||||
public boolean isUnlockingWithFingerprintAllowed() {
|
||||
return mUserHasAuthenticatedSinceBoot && !hasFingerprintUnlockTimedOut(sCurrentUser);
|
||||
return mStrongAuthTracker.isUnlockingWithFingerprintAllowed()
|
||||
&& !hasFingerprintUnlockTimedOut(sCurrentUser);
|
||||
}
|
||||
|
||||
public StrongAuthTracker getStrongAuthTracker() {
|
||||
return mStrongAuthTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -827,6 +833,25 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
|
||||
}
|
||||
}
|
||||
|
||||
public class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker {
|
||||
|
||||
public boolean isUnlockingWithFingerprintAllowed() {
|
||||
int userId = getCurrentUser();
|
||||
return isFingerprintAllowedForUser(userId);
|
||||
}
|
||||
|
||||
public boolean hasUserAuthenticatedSinceBoot() {
|
||||
int userId = getCurrentUser();
|
||||
return (getStrongAuthForUser(userId)
|
||||
& STRONG_AUTH_REQUIRED_AFTER_BOOT) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStrongAuthRequiredChanged(int userId) {
|
||||
// do something?
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyguardUpdateMonitor getInstance(Context context) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new KeyguardUpdateMonitor(context);
|
||||
@@ -973,6 +998,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
|
||||
PERMISSION_SELF, null /* handler */);
|
||||
mTrustManager = (TrustManager) context.getSystemService(Context.TRUST_SERVICE);
|
||||
mTrustManager.registerTrustListener(this);
|
||||
new LockPatternUtils(context).registerStrongAuthTracker(mStrongAuthTracker);
|
||||
|
||||
mFpm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
|
||||
updateFingerprintListeningState();
|
||||
@@ -1001,8 +1027,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
|
||||
if (DEBUG) Log.v(TAG, "startListeningForFingerprint()");
|
||||
int userId = ActivityManager.getCurrentUser();
|
||||
if (isUnlockWithFingerprintPossible(userId)) {
|
||||
mUserHasAuthenticatedSinceBoot = mTrustManager.hasUserAuthenticatedSinceBoot(
|
||||
ActivityManager.getCurrentUser());
|
||||
if (mFingerprintCancelSignal != null) {
|
||||
mFingerprintCancelSignal.cancel();
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ public class KeyguardViewMediator extends SystemUI {
|
||||
int currentUser = ActivityManager.getCurrentUser();
|
||||
if ((mUpdateMonitor.getUserTrustIsManaged(currentUser)
|
||||
|| mUpdateMonitor.isUnlockWithFingerprintPossible(currentUser))
|
||||
&& !mTrustManager.hasUserAuthenticatedSinceBoot(currentUser)) {
|
||||
&& !mUpdateMonitor.getStrongAuthTracker().hasUserAuthenticatedSinceBoot()) {
|
||||
return KeyguardSecurityView.PROMPT_REASON_RESTART;
|
||||
} else if (mUpdateMonitor.isUnlockWithFingerprintPossible(currentUser)
|
||||
&& mUpdateMonitor.hasFingerprintUnlockTimedOut(currentUser)) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.server;
|
||||
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.app.backup.BackupManager;
|
||||
import android.app.trust.IStrongAuthTracker;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
@@ -70,6 +71,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
private final Context mContext;
|
||||
|
||||
private final LockSettingsStorage mStorage;
|
||||
private final LockSettingsStrongAuth mStrongAuth = new LockSettingsStrongAuth();
|
||||
|
||||
private LockPatternUtils mLockPatternUtils;
|
||||
private boolean mFirstCallToVold;
|
||||
@@ -93,6 +95,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
filter.addAction(Intent.ACTION_USER_ADDED);
|
||||
filter.addAction(Intent.ACTION_USER_STARTING);
|
||||
filter.addAction(Intent.ACTION_USER_REMOVED);
|
||||
filter.addAction(Intent.ACTION_USER_PRESENT);
|
||||
mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
|
||||
|
||||
mStorage = new LockSettingsStorage(context, new LockSettingsStorage.Callback() {
|
||||
@@ -122,6 +125,8 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
} else if (Intent.ACTION_USER_STARTING.equals(intent.getAction())) {
|
||||
final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
|
||||
mStorage.prefetchUser(userHandle);
|
||||
} else if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {
|
||||
mStrongAuth.reportUnlock(getSendingUserId());
|
||||
} else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
|
||||
final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
|
||||
if (userHandle > 0) {
|
||||
@@ -713,6 +718,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
|
||||
private void removeUser(int userId) {
|
||||
mStorage.removeUser(userId);
|
||||
mStrongAuth.removeUser(userId);
|
||||
|
||||
final KeyStore ks = KeyStore.getInstance();
|
||||
ks.onUserRemoved(userId);
|
||||
@@ -727,6 +733,24 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStrongAuthTracker(IStrongAuthTracker tracker) {
|
||||
checkPasswordReadPermission(UserHandle.USER_ALL);
|
||||
mStrongAuth.registerStrongAuthTracker(tracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterStrongAuthTracker(IStrongAuthTracker tracker) {
|
||||
checkPasswordReadPermission(UserHandle.USER_ALL);
|
||||
mStrongAuth.unregisterStrongAuthTracker(tracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requireStrongAuth(int strongAuthReason, int userId) {
|
||||
checkWritePermission(userId);
|
||||
mStrongAuth.requireStrongAuth(strongAuthReason, userId);
|
||||
}
|
||||
|
||||
private static final String[] VALID_SETTINGS = new String[] {
|
||||
LockPatternUtils.LOCKOUT_PERMANENT_KEY,
|
||||
LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE,
|
||||
@@ -797,5 +821,4 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
Slog.e(TAG, "Unable to acquire GateKeeperService");
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed 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 com.android.server;
|
||||
|
||||
import com.android.internal.widget.LockPatternUtils;
|
||||
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker;
|
||||
|
||||
import android.app.trust.IStrongAuthTracker;
|
||||
import android.os.DeadObjectException;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
|
||||
|
||||
/**
|
||||
* Keeps track of requests for strong authentication.
|
||||
*/
|
||||
public class LockSettingsStrongAuth {
|
||||
|
||||
private static final String TAG = "LockSettings";
|
||||
|
||||
private static final int MSG_REQUIRE_STRONG_AUTH = 1;
|
||||
private static final int MSG_REGISTER_TRACKER = 2;
|
||||
private static final int MSG_UNREGISTER_TRACKER = 3;
|
||||
private static final int MSG_REMOVE_USER = 4;
|
||||
|
||||
private final ArrayList<IStrongAuthTracker> mStrongAuthTrackers = new ArrayList<>();
|
||||
private final SparseIntArray mStrongAuthForUser = new SparseIntArray();
|
||||
|
||||
private void handleAddStrongAuthTracker(IStrongAuthTracker tracker) {
|
||||
for (int i = 0; i < mStrongAuthTrackers.size(); i++) {
|
||||
if (mStrongAuthTrackers.get(i).asBinder() == tracker.asBinder()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
mStrongAuthTrackers.add(tracker);
|
||||
|
||||
for (int i = 0; i < mStrongAuthForUser.size(); i++) {
|
||||
int key = mStrongAuthForUser.keyAt(i);
|
||||
int value = mStrongAuthForUser.valueAt(i);
|
||||
try {
|
||||
tracker.onStrongAuthRequiredChanged(value, key);
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "Exception while adding StrongAuthTracker.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRemoveStrongAuthTracker(IStrongAuthTracker tracker) {
|
||||
for (int i = 0; i < mStrongAuthTrackers.size(); i++) {
|
||||
if (mStrongAuthTrackers.get(i).asBinder() == tracker.asBinder()) {
|
||||
mStrongAuthTrackers.remove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRequireStrongAuth(int strongAuthReason, int userId) {
|
||||
if (userId == UserHandle.USER_ALL) {
|
||||
for (int i = 0; i < mStrongAuthForUser.size(); i++) {
|
||||
int key = mStrongAuthForUser.keyAt(i);
|
||||
handleRequireStrongAuthOneUser(strongAuthReason, key);
|
||||
}
|
||||
} else {
|
||||
handleRequireStrongAuthOneUser(strongAuthReason, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRequireStrongAuthOneUser(int strongAuthReason, int userId) {
|
||||
int oldValue = mStrongAuthForUser.get(userId, LockPatternUtils.StrongAuthTracker.DEFAULT);
|
||||
int newValue = strongAuthReason == STRONG_AUTH_NOT_REQUIRED
|
||||
? STRONG_AUTH_NOT_REQUIRED
|
||||
: (oldValue | strongAuthReason);
|
||||
if (oldValue != newValue) {
|
||||
mStrongAuthForUser.put(userId, newValue);
|
||||
notifyStrongAuthTrackers(newValue, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRemoveUser(int userId) {
|
||||
int index = mStrongAuthForUser.indexOfKey(userId);
|
||||
if (index >= 0) {
|
||||
mStrongAuthForUser.removeAt(index);
|
||||
notifyStrongAuthTrackers(StrongAuthTracker.DEFAULT, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyStrongAuthTrackers(int strongAuthReason, int userId) {
|
||||
for (int i = 0; i < mStrongAuthTrackers.size(); i++) {
|
||||
try {
|
||||
mStrongAuthTrackers.get(i).onStrongAuthRequiredChanged(strongAuthReason, userId);
|
||||
} catch (DeadObjectException e) {
|
||||
Slog.d(TAG, "Removing dead StrongAuthTracker.");
|
||||
mStrongAuthTrackers.remove(i);
|
||||
i--;
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "Exception while notifying StrongAuthTracker.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerStrongAuthTracker(IStrongAuthTracker tracker) {
|
||||
mHandler.obtainMessage(MSG_REGISTER_TRACKER, tracker).sendToTarget();
|
||||
}
|
||||
|
||||
public void unregisterStrongAuthTracker(IStrongAuthTracker tracker) {
|
||||
mHandler.obtainMessage(MSG_UNREGISTER_TRACKER, tracker).sendToTarget();
|
||||
}
|
||||
|
||||
public void removeUser(int userId) {
|
||||
mHandler.obtainMessage(MSG_REMOVE_USER, userId, 0).sendToTarget();
|
||||
}
|
||||
|
||||
public void requireStrongAuth(int strongAuthReason, int userId) {
|
||||
if (userId == UserHandle.USER_ALL || userId >= UserHandle.USER_OWNER) {
|
||||
mHandler.obtainMessage(MSG_REQUIRE_STRONG_AUTH, strongAuthReason,
|
||||
userId).sendToTarget();
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"userId must be an explicit user id or USER_ALL");
|
||||
}
|
||||
}
|
||||
|
||||
public void reportUnlock(int userId) {
|
||||
requireStrongAuth(STRONG_AUTH_NOT_REQUIRED, userId);
|
||||
}
|
||||
|
||||
private final Handler mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_REGISTER_TRACKER:
|
||||
handleAddStrongAuthTracker((IStrongAuthTracker) msg.obj);
|
||||
break;
|
||||
case MSG_UNREGISTER_TRACKER:
|
||||
handleRemoveStrongAuthTracker((IStrongAuthTracker) msg.obj);
|
||||
break;
|
||||
case MSG_REQUIRE_STRONG_AUTH:
|
||||
handleRequireStrongAuth(msg.arg1, msg.arg2);
|
||||
break;
|
||||
case MSG_REMOVE_USER:
|
||||
handleRemoveUser(msg.arg1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package com.android.server.trust;
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.content.PackageMonitor;
|
||||
import com.android.internal.widget.LockPatternUtils;
|
||||
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker;
|
||||
import com.android.server.SystemService;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
@@ -59,6 +60,7 @@ import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.util.SparseIntArray;
|
||||
import android.util.Xml;
|
||||
import android.view.IWindowManager;
|
||||
import android.view.WindowManagerGlobal;
|
||||
@@ -96,16 +98,15 @@ public class TrustManagerService extends SystemService {
|
||||
private static final int MSG_UNREGISTER_LISTENER = 2;
|
||||
private static final int MSG_DISPATCH_UNLOCK_ATTEMPT = 3;
|
||||
private static final int MSG_ENABLED_AGENTS_CHANGED = 4;
|
||||
private static final int MSG_REQUIRE_CREDENTIAL_ENTRY = 5;
|
||||
private static final int MSG_KEYGUARD_SHOWING_CHANGED = 6;
|
||||
private static final int MSG_START_USER = 7;
|
||||
private static final int MSG_CLEANUP_USER = 8;
|
||||
private static final int MSG_SWITCH_USER = 9;
|
||||
|
||||
private final ArraySet<AgentInfo> mActiveAgents = new ArraySet<AgentInfo>();
|
||||
private final ArrayList<ITrustListener> mTrustListeners = new ArrayList<ITrustListener>();
|
||||
private final ArraySet<AgentInfo> mActiveAgents = new ArraySet<>();
|
||||
private final ArrayList<ITrustListener> mTrustListeners = new ArrayList<>();
|
||||
private final Receiver mReceiver = new Receiver();
|
||||
private final SparseBooleanArray mUserHasAuthenticated = new SparseBooleanArray();
|
||||
|
||||
/* package */ final TrustArchive mArchive = new TrustArchive();
|
||||
private final Context mContext;
|
||||
private final LockPatternUtils mLockPatternUtils;
|
||||
@@ -118,9 +119,6 @@ public class TrustManagerService extends SystemService {
|
||||
@GuardedBy("mDeviceLockedForUser")
|
||||
private final SparseBooleanArray mDeviceLockedForUser = new SparseBooleanArray();
|
||||
|
||||
@GuardedBy("mUserHasAuthenticatedSinceBoot")
|
||||
private final SparseBooleanArray mUserHasAuthenticatedSinceBoot = new SparseBooleanArray();
|
||||
|
||||
private boolean mTrustAgentsCanRun = false;
|
||||
private int mCurrentUser = UserHandle.USER_OWNER;
|
||||
|
||||
@@ -146,6 +144,7 @@ public class TrustManagerService extends SystemService {
|
||||
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
|
||||
mPackageMonitor.register(mContext, mHandler.getLooper(), UserHandle.ALL, true);
|
||||
mReceiver.register(mContext);
|
||||
mLockPatternUtils.registerStrongAuthTracker(mStrongAuthTracker);
|
||||
} else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
|
||||
mTrustAgentsCanRun = true;
|
||||
refreshAgentList(UserHandle.USER_ALL);
|
||||
@@ -230,7 +229,7 @@ public class TrustManagerService extends SystemService {
|
||||
if (!userInfo.supportsSwitchTo()) continue;
|
||||
if (!mActivityManager.isUserRunning(userInfo.id)) continue;
|
||||
if (!lockPatternUtils.isSecure(userInfo.id)) continue;
|
||||
if (!getUserHasAuthenticated(userInfo.id)) continue;
|
||||
if (!mStrongAuthTracker.isTrustAllowedForUser(userInfo.id)) continue;
|
||||
DevicePolicyManager dpm = lockPatternUtils.getDevicePolicyManager();
|
||||
int disabledFeatures = dpm.getKeyguardDisabledFeatures(null, userInfo.id);
|
||||
final boolean disableTrustAgents =
|
||||
@@ -509,7 +508,7 @@ public class TrustManagerService extends SystemService {
|
||||
// Agent dispatch and aggregation
|
||||
|
||||
private boolean aggregateIsTrusted(int userId) {
|
||||
if (!getUserHasAuthenticated(userId)) {
|
||||
if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < mActiveAgents.size(); i++) {
|
||||
@@ -524,7 +523,7 @@ public class TrustManagerService extends SystemService {
|
||||
}
|
||||
|
||||
private boolean aggregateIsTrustManaged(int userId) {
|
||||
if (!getUserHasAuthenticated(userId)) {
|
||||
if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < mActiveAgents.size(); i++) {
|
||||
@@ -545,54 +544,6 @@ public class TrustManagerService extends SystemService {
|
||||
info.agent.onUnlockAttempt(successful);
|
||||
}
|
||||
}
|
||||
|
||||
if (successful) {
|
||||
updateUserHasAuthenticated(userId);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUserHasAuthenticated(int userId) {
|
||||
boolean changed = setUserHasAuthenticated(userId);
|
||||
if (changed) {
|
||||
refreshAgentList(userId);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getUserHasAuthenticated(int userId) {
|
||||
return mUserHasAuthenticated.get(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the value has changed
|
||||
*/
|
||||
private boolean setUserHasAuthenticated(int userId) {
|
||||
if (!mUserHasAuthenticated.get(userId)) {
|
||||
mUserHasAuthenticated.put(userId, true);
|
||||
synchronized (mUserHasAuthenticatedSinceBoot) {
|
||||
mUserHasAuthenticatedSinceBoot.put(userId, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void clearUserHasAuthenticated(int userId) {
|
||||
if (userId == UserHandle.USER_ALL) {
|
||||
mUserHasAuthenticated.clear();
|
||||
} else {
|
||||
mUserHasAuthenticated.put(userId, false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getUserHasAuthenticatedSinceBoot(int userId) {
|
||||
synchronized (mUserHasAuthenticatedSinceBoot) {
|
||||
return mUserHasAuthenticatedSinceBoot.get(userId);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireCredentialEntry(int userId) {
|
||||
clearUserHasAuthenticated(userId);
|
||||
refreshAgentList(userId);
|
||||
}
|
||||
|
||||
// Listeners
|
||||
@@ -680,17 +631,6 @@ public class TrustManagerService extends SystemService {
|
||||
mHandler.sendEmptyMessage(MSG_ENABLED_AGENTS_CHANGED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportRequireCredentialEntry(int userId) throws RemoteException {
|
||||
enforceReportPermission();
|
||||
if (userId == UserHandle.USER_ALL || userId >= UserHandle.USER_OWNER) {
|
||||
mHandler.obtainMessage(MSG_REQUIRE_CREDENTIAL_ENTRY, userId, 0).sendToTarget();
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"userId must be an explicit user id or USER_ALL");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportKeyguardShowingChanged() throws RemoteException {
|
||||
enforceReportPermission();
|
||||
@@ -734,18 +674,6 @@ public class TrustManagerService extends SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasUserAuthenticatedSinceBoot(int userId) throws RemoteException {
|
||||
mContext.enforceCallingOrSelfPermission(
|
||||
Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE, null);
|
||||
long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
return getUserHasAuthenticatedSinceBoot(userId);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
private void enforceReportPermission() {
|
||||
mContext.enforceCallingOrSelfPermission(
|
||||
Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE, "reporting trust events");
|
||||
@@ -794,9 +722,8 @@ public class TrustManagerService extends SystemService {
|
||||
fout.print(": trusted=" + dumpBool(aggregateIsTrusted(user.id)));
|
||||
fout.print(", trustManaged=" + dumpBool(aggregateIsTrustManaged(user.id)));
|
||||
fout.print(", deviceLocked=" + dumpBool(isDeviceLockedInner(user.id)));
|
||||
fout.print(", hasAuthenticated=" + dumpBool(getUserHasAuthenticated(user.id)));
|
||||
fout.print(", hasAuthenticatedSinceBoot="
|
||||
+ dumpBool(getUserHasAuthenticatedSinceBoot(user.id)));
|
||||
fout.print(", strongAuthRequired=" + dumpHex(
|
||||
mStrongAuthTracker.getStrongAuthForUser(user.id)));
|
||||
fout.println();
|
||||
fout.println(" Enabled agents:");
|
||||
boolean duplicateSimpleNames = false;
|
||||
@@ -831,6 +758,10 @@ public class TrustManagerService extends SystemService {
|
||||
private String dumpBool(boolean b) {
|
||||
return b ? "1" : "0";
|
||||
}
|
||||
|
||||
private String dumpHex(int i) {
|
||||
return "0x" + Integer.toHexString(i);
|
||||
}
|
||||
};
|
||||
|
||||
private int resolveProfileParent(int userId) {
|
||||
@@ -864,9 +795,6 @@ public class TrustManagerService extends SystemService {
|
||||
// This is also called when the security mode of a user changes.
|
||||
refreshDeviceLockedForUser(UserHandle.USER_ALL);
|
||||
break;
|
||||
case MSG_REQUIRE_CREDENTIAL_ENTRY:
|
||||
requireCredentialEntry(msg.arg1);
|
||||
break;
|
||||
case MSG_KEYGUARD_SHOWING_CHANGED:
|
||||
refreshDeviceLockedForUser(mCurrentUser);
|
||||
break;
|
||||
@@ -900,6 +828,13 @@ public class TrustManagerService extends SystemService {
|
||||
}
|
||||
};
|
||||
|
||||
private final StrongAuthTracker mStrongAuthTracker = new StrongAuthTracker() {
|
||||
@Override
|
||||
public void onStrongAuthRequiredChanged(int userId) {
|
||||
refreshAgentList(userId);
|
||||
}
|
||||
};
|
||||
|
||||
private class Receiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
@@ -908,8 +843,6 @@ public class TrustManagerService extends SystemService {
|
||||
if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(action)) {
|
||||
refreshAgentList(getSendingUserId());
|
||||
updateDevicePolicyFeatures();
|
||||
} else if (Intent.ACTION_USER_PRESENT.equals(action)) {
|
||||
updateUserHasAuthenticated(getSendingUserId());
|
||||
} else if (Intent.ACTION_USER_ADDED.equals(action)) {
|
||||
int userId = getUserId(intent);
|
||||
if (userId > 0) {
|
||||
@@ -918,7 +851,6 @@ public class TrustManagerService extends SystemService {
|
||||
} else if (Intent.ACTION_USER_REMOVED.equals(action)) {
|
||||
int userId = getUserId(intent);
|
||||
if (userId > 0) {
|
||||
mUserHasAuthenticated.delete(userId);
|
||||
synchronized (mUserIsTrusted) {
|
||||
mUserIsTrusted.delete(userId);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
|
||||
import static android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE;
|
||||
import static android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA;
|
||||
import static android.content.pm.PackageManager.GET_UNINSTALLED_PACKAGES;
|
||||
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
|
||||
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
|
||||
import static org.xmlpull.v1.XmlPullParser.END_TAG;
|
||||
import static org.xmlpull.v1.XmlPullParser.TEXT;
|
||||
@@ -44,6 +45,7 @@ import android.app.admin.DevicePolicyManagerInternal;
|
||||
import android.app.admin.IDevicePolicyManager;
|
||||
import android.app.admin.SystemUpdatePolicy;
|
||||
import android.app.backup.IBackupManager;
|
||||
import android.app.trust.TrustManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
@@ -2957,7 +2959,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
|
||||
}
|
||||
boolean requireEntry = (flags & DevicePolicyManager.RESET_PASSWORD_REQUIRE_ENTRY) != 0;
|
||||
if (requireEntry) {
|
||||
utils.requireCredentialEntry(UserHandle.USER_ALL);
|
||||
utils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW,
|
||||
UserHandle.USER_ALL);
|
||||
}
|
||||
synchronized (this) {
|
||||
int newOwner = requireEntry ? callingUid : -1;
|
||||
@@ -3089,7 +3092,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
|
||||
mPowerManager.goToSleep(SystemClock.uptimeMillis(),
|
||||
PowerManager.GO_TO_SLEEP_REASON_DEVICE_ADMIN, 0);
|
||||
// Ensure the device is locked
|
||||
new LockPatternUtils(mContext).requireCredentialEntry(UserHandle.USER_ALL);
|
||||
new LockPatternUtils(mContext).requireStrongAuth(
|
||||
STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW, UserHandle.USER_ALL);
|
||||
getWindowManager().lockNow(null);
|
||||
} catch (RemoteException e) {
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user