mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
CE-937 Add concept of nullValueBehaviorKeyName to QSecurityKeyType - so that a session can override the null-value-behavior for a table's lock (e.g., allow super-users access to null records, where normal users cannot).
This commit is contained in:
@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
@ -230,7 +231,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
{
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
inputRecord.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."));
|
||||
}
|
||||
@ -298,7 +299,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// handle null values - error if the NullValueBehavior is DENY //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
|
||||
|
@ -50,6 +50,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
@ -85,6 +86,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
@ -208,14 +210,23 @@ public class QInstanceValidator
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyType.getName()), "Missing name for a securityKeyType"))
|
||||
{
|
||||
assertCondition(Objects.equals(name, securityKeyType.getName()), "Inconsistent naming for securityKeyType: " + name + "/" + securityKeyType.getName() + ".");
|
||||
assertCondition(!usedNames.contains(name), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + name);
|
||||
|
||||
String duplicateNameMessagePrefix = "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: ";
|
||||
assertCondition(!usedNames.contains(name), duplicateNameMessagePrefix + name);
|
||||
usedNames.add(name);
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
|
||||
{
|
||||
assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + securityKeyType.getAllAccessKeyName());
|
||||
assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), duplicateNameMessagePrefix + securityKeyType.getAllAccessKeyName());
|
||||
usedNames.add(securityKeyType.getAllAccessKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
|
||||
{
|
||||
assertCondition(!usedNames.contains(securityKeyType.getNullValueBehaviorKeyName()), duplicateNameMessagePrefix + securityKeyType.getNullValueBehaviorKeyName());
|
||||
usedNames.add(securityKeyType.getNullValueBehaviorKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getPossibleValueSourceName()))
|
||||
{
|
||||
assertCondition(qInstance.getPossibleValueSource(securityKeyType.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName in securityKeyType: " + name);
|
||||
|
@ -1052,10 +1052,16 @@ public class QInstance
|
||||
for(QSecurityKeyType securityKeyType : CollectionUtils.nonNullMap(getSecurityKeyTypes()).values())
|
||||
{
|
||||
rs.add(securityKeyType.getName());
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
|
||||
{
|
||||
rs.add(securityKeyType.getAllAccessKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
|
||||
{
|
||||
rs.add(securityKeyType.getNullValueBehaviorKeyName());
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.security;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock.NullValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility for working with security key, nullValueBehaviors.
|
||||
*******************************************************************************/
|
||||
public class NullValueBehaviorUtil
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(NullValueBehaviorUtil.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Look at a RecordSecurityLock, but also the active session - and if the session
|
||||
** has a null-value-behavior key for the lock's key-type, then allow that behavior
|
||||
** to override the lock's default.
|
||||
*******************************************************************************/
|
||||
public static NullValueBehavior getEffectiveNullValueBehavior(RecordSecurityLock recordSecurityLock)
|
||||
{
|
||||
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
|
||||
if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
|
||||
{
|
||||
List<Serializable> nullValueSessionValueList = QContext.getQSession().getSecurityKeyValues(securityKeyType.getNullValueBehaviorKeyName());
|
||||
if(CollectionUtils.nullSafeHasContents(nullValueSessionValueList))
|
||||
{
|
||||
NullValueBehavior nullValueBehavior = NullValueBehavior.tryToGetFromString(ValueUtils.getValueAsString(nullValueSessionValueList.get(0)));
|
||||
if(nullValueBehavior != null)
|
||||
{
|
||||
return nullValueBehavior;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Unexpected value in nullValueBehavior security key. Will use recordSecurityLock's nullValueBehavior",
|
||||
logPair("nullValueBehaviorKeyName", securityKeyType.getNullValueBehaviorKeyName()),
|
||||
logPair("value", nullValueSessionValueList.get(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (recordSecurityLock.getNullValueBehavior());
|
||||
}
|
||||
|
||||
}
|
@ -34,6 +34,7 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface
|
||||
{
|
||||
private String name;
|
||||
private String allAccessKeyName;
|
||||
private String nullValueBehaviorKeyName;
|
||||
private String possibleValueSourceName;
|
||||
|
||||
|
||||
@ -149,4 +150,35 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface
|
||||
qInstance.addSecurityKeyType(this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for nullValueBehaviorKeyName
|
||||
*******************************************************************************/
|
||||
public String getNullValueBehaviorKeyName()
|
||||
{
|
||||
return (this.nullValueBehaviorKeyName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for nullValueBehaviorKeyName
|
||||
*******************************************************************************/
|
||||
public void setNullValueBehaviorKeyName(String nullValueBehaviorKeyName)
|
||||
{
|
||||
this.nullValueBehaviorKeyName = nullValueBehaviorKeyName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for nullValueBehaviorKeyName
|
||||
*******************************************************************************/
|
||||
public QSecurityKeyType withNullValueBehaviorKeyName(String nullValueBehaviorKeyName)
|
||||
{
|
||||
this.nullValueBehaviorKeyName = nullValueBehaviorKeyName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -22,7 +22,9 @@
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.security;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -67,7 +69,34 @@ public class RecordSecurityLock
|
||||
{
|
||||
ALLOW,
|
||||
ALLOW_WRITE_ONLY, // not common - but see Audit, where you can do a thing that inserts them into a generic table, even though you can't later read them yourself...
|
||||
DENY
|
||||
DENY;
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// for use in tryToGetFromString, where we'll lowercase the input //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
private static final Map<String, NullValueBehavior> stringMapping = new HashMap<>();
|
||||
|
||||
static
|
||||
{
|
||||
stringMapping.put("allow", ALLOW);
|
||||
stringMapping.put("allow_write_only", ALLOW_WRITE_ONLY);
|
||||
stringMapping.put("allowwriteonly", ALLOW_WRITE_ONLY);
|
||||
stringMapping.put("deny", DENY);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static NullValueBehavior tryToGetFromString(String string)
|
||||
{
|
||||
if(string == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
return stringMapping.get(string.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -75,6 +75,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep;
|
||||
@ -1622,19 +1623,30 @@ class QInstanceValidatorTest extends BaseTest
|
||||
assertValidationFailureReasons((qInstance ->
|
||||
{
|
||||
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("clientId"));
|
||||
}), "More than one SecurityKeyType with name (or allAccessKeyName) of: clientId");
|
||||
}), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: clientId");
|
||||
|
||||
assertValidationFailureReasonsAllowingExtraReasons((qInstance ->
|
||||
{
|
||||
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("clientId").withNullValueBehaviorKeyName("clientId"));
|
||||
}), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: clientId");
|
||||
|
||||
assertValidationFailureReasons((qInstance ->
|
||||
{
|
||||
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("allAccess"));
|
||||
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("warehouseId").withAllAccessKeyName("allAccess"));
|
||||
}), "More than one SecurityKeyType with name (or allAccessKeyName) of: allAccess");
|
||||
}), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: allAccess");
|
||||
|
||||
assertValidationFailureReasons((qInstance ->
|
||||
{
|
||||
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withNullValueBehaviorKeyName("nullBehavior"));
|
||||
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("warehouseId").withNullValueBehaviorKeyName("nullBehavior"));
|
||||
}), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: nullBehavior");
|
||||
|
||||
assertValidationFailureReasons((qInstance ->
|
||||
{
|
||||
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("allAccess"));
|
||||
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("allAccess"));
|
||||
}), "More than one SecurityKeyType with name (or allAccessKeyName) of: allAccess");
|
||||
}), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: allAccess");
|
||||
|
||||
assertValidationFailureReasons((qInstance -> qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withPossibleValueSourceName("nonPVS"))),
|
||||
"Unrecognized possibleValueSourceName in securityKeyType");
|
||||
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.security;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for NullValueBehaviorUtil
|
||||
*******************************************************************************/
|
||||
class NullValueBehaviorUtilTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test()
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if session doesn't have a null-value key, then always get back the lock's behavior //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values())
|
||||
{
|
||||
assertEquals(lockNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock()
|
||||
.withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE)
|
||||
.withNullValueBehavior(lockNullValueBehavior)));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// if session DOES have a null-value key, then always gete it back //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock.NullValueBehavior sessionNullValueBehavior : RecordSecurityLock.NullValueBehavior.values())
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValues(Map.of(TestUtils.SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR, List.of(sessionNullValueBehavior.toString())));
|
||||
|
||||
for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values())
|
||||
{
|
||||
assertEquals(sessionNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock()
|
||||
.withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE)
|
||||
.withNullValueBehavior(lockNullValueBehavior)));
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// if session has an invalid key, always get back lock's behavior //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values())
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValues(Map.of(TestUtils.SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR, List.of("xyz")));
|
||||
|
||||
assertEquals(lockNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock()
|
||||
.withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE)
|
||||
.withNullValueBehavior(lockNullValueBehavior)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -173,6 +173,7 @@ public class TestUtils
|
||||
|
||||
public static final String SECURITY_KEY_TYPE_STORE = "store";
|
||||
public static final String SECURITY_KEY_TYPE_STORE_ALL_ACCESS = "storeAllAccess";
|
||||
public static final String SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR = "storeNullBehavior";
|
||||
public static final String SECURITY_KEY_TYPE_INTERNAL_OR_EXTERNAL = "internalOrExternal";
|
||||
|
||||
|
||||
@ -471,6 +472,7 @@ public class TestUtils
|
||||
return new QSecurityKeyType()
|
||||
.withName(SECURITY_KEY_TYPE_STORE)
|
||||
.withAllAccessKeyName(SECURITY_KEY_TYPE_STORE_ALL_ACCESS)
|
||||
.withNullValueBehaviorKeyName(SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR)
|
||||
.withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STORE);
|
||||
}
|
||||
|
||||
@ -1255,7 +1257,6 @@ public class TestUtils
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
Reference in New Issue
Block a user