mirror of
https://github.com/versity/versitygw.git
synced 2026-09-22 07:54:14 +00:00
feat: add configurable file permissions for new objects
Replace the hardcoded 0644 defaultFilePerm with a NewFilePerm option on the posix and scoutfs backends, exposed as the --file-perms flag and VGW_FILE_PERMS env var alongside the existing dir-perms option. The mode passed to open() is masked by the process umask, so the O_TMPFILE path now chmods explicitly to match the CreateTemp fallback path and give new objects the configured mode regardless of umask.
This commit is contained in:
+39
-3
@@ -78,6 +78,9 @@ type Posix struct {
|
||||
// newDirPerm is the permission to set on newly created directories
|
||||
newDirPerm fs.FileMode
|
||||
|
||||
// newFilePerm is the permission to set on newly created object files
|
||||
newFilePerm fs.FileMode
|
||||
|
||||
// forceNoTmpFile is a flag to disable the use of O_TMPFILE even
|
||||
// if the filesystem supports it. This is needed for cases where
|
||||
// there are different filesystems mounted below the bucket level.
|
||||
@@ -130,9 +133,22 @@ type Posix struct {
|
||||
dataIntegrityEtag bool
|
||||
}
|
||||
|
||||
func (o *PosixOpts) SetNewDirPerm(perm fs.FileMode) {
|
||||
o.NewDirPerm = perm
|
||||
o.newDirPermSet = true
|
||||
}
|
||||
|
||||
func (o *PosixOpts) SetNewFilePerm(perm fs.FileMode) {
|
||||
o.NewFilePerm = perm
|
||||
o.newFilePermSet = true
|
||||
}
|
||||
|
||||
var _ backend.Backend = &Posix{}
|
||||
|
||||
const (
|
||||
defaultNewDirPerm fs.FileMode = 0755
|
||||
defaultNewFilePerm fs.FileMode = 0644
|
||||
|
||||
MetaTmpDir = ".sgwtmp"
|
||||
MetaTmpMultipartDir = MetaTmpDir + "/multipart"
|
||||
onameAttr = "objname"
|
||||
@@ -190,8 +206,18 @@ type PosixOpts struct {
|
||||
BucketLinks bool
|
||||
//VersioningDir sets the version directory to enable object versioning
|
||||
VersioningDir string
|
||||
// NewDirPerm specifies the permission to set on newly created directories
|
||||
NewDirPerm fs.FileMode
|
||||
// NewDirPerm specifies the permission to set on newly created directories.
|
||||
// When unset, the default is 0755. An explicit zero value (0000) is valid
|
||||
// and is preserved when SetNewDirPerm(0000) is used.
|
||||
NewDirPerm fs.FileMode
|
||||
newDirPermSet bool
|
||||
// NewFilePerm specifies the permission to set on newly created object
|
||||
// files. Defaults to 0644 when unset. Unlike NewDirPerm, this mode is
|
||||
// applied explicitly and is therefore not reduced by the process umask.
|
||||
// An explicit zero value (0000) is valid and is preserved when
|
||||
// SetNewFilePerm(0000) is used.
|
||||
NewFilePerm fs.FileMode
|
||||
newFilePermSet bool
|
||||
// SideCarDir sets the directory to store sidecar metadata
|
||||
SideCarDir string
|
||||
// ForceNoTmpFile disables the use of O_TMPFILE even if the filesystem
|
||||
@@ -284,6 +310,15 @@ func New(rootdir string, meta meta.MetadataStorer, opts PosixOpts) (*Posix, erro
|
||||
fmt.Println("Using sidecar directory for metadata:", sidecardirAbs)
|
||||
}
|
||||
|
||||
newDirPerm := defaultNewDirPerm
|
||||
if opts.newDirPermSet {
|
||||
newDirPerm = opts.NewDirPerm.Perm()
|
||||
}
|
||||
newFilePerm := defaultNewFilePerm
|
||||
if opts.newFilePermSet {
|
||||
newFilePerm = opts.NewFilePerm.Perm()
|
||||
}
|
||||
|
||||
return &Posix{
|
||||
meta: meta,
|
||||
rootfd: f,
|
||||
@@ -294,7 +329,8 @@ func New(rootdir string, meta meta.MetadataStorer, opts PosixOpts) (*Posix, erro
|
||||
chowngid: opts.ChownGID,
|
||||
bucketlinks: opts.BucketLinks,
|
||||
versioningDir: versioningdirAbs,
|
||||
newDirPerm: opts.NewDirPerm,
|
||||
newDirPerm: newDirPerm,
|
||||
newFilePerm: newFilePerm,
|
||||
forceNoTmpFile: opts.ForceNoTmpFile,
|
||||
forceNoCopyFileRange: opts.ForceNoCopyFileRange,
|
||||
enableODirect: opts.EnableODirect,
|
||||
|
||||
@@ -38,24 +38,20 @@ import (
|
||||
const procfddir = "/proc/self/fd"
|
||||
|
||||
type tmpfile struct {
|
||||
f *os.File
|
||||
bucket string
|
||||
objname string
|
||||
isOTmp bool
|
||||
procFDName string
|
||||
useODirect bool
|
||||
size int64
|
||||
doChown bool
|
||||
uid int
|
||||
gid int
|
||||
newDirPerm fs.FileMode
|
||||
f *os.File
|
||||
bucket string
|
||||
objname string
|
||||
isOTmp bool
|
||||
procFDName string
|
||||
useODirect bool
|
||||
size int64
|
||||
doChown bool
|
||||
uid int
|
||||
gid int
|
||||
newDirPerm fs.FileMode
|
||||
newFilePerm fs.FileMode
|
||||
}
|
||||
|
||||
var (
|
||||
// TODO: make this configurable
|
||||
defaultFilePerm uint32 = 0644
|
||||
)
|
||||
|
||||
func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account, dofalloc bool, forceNoTmpFile bool, allowODirect odirectPolicy) (*tmpfile, error) {
|
||||
uid, gid, doChown := p.getChownIDs(acct)
|
||||
|
||||
@@ -76,7 +72,9 @@ func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Accou
|
||||
useODirect = true
|
||||
}
|
||||
|
||||
fd, err := unix.Open(dir, openFlags, defaultFilePerm)
|
||||
filePerm := uint32(p.newFilePerm.Perm())
|
||||
|
||||
fd, err := unix.Open(dir, openFlags, filePerm)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EROFS) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
||||
@@ -85,7 +83,7 @@ func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Accou
|
||||
if p.enableODirect && bool(allowODirect) && isODirectUnsupportedOpenErr(err) {
|
||||
warnODirectUnsupportedOnce("openTmpFile", err)
|
||||
|
||||
fd, err = unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, defaultFilePerm)
|
||||
fd, err = unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, filePerm)
|
||||
if err == nil {
|
||||
useODirect = false
|
||||
} else if errors.Is(err, syscall.EROFS) {
|
||||
@@ -103,18 +101,29 @@ func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Accou
|
||||
// later to link file into namespace
|
||||
f := os.NewFile(uintptr(fd), filepath.Join(procfddir, strconv.Itoa(fd)))
|
||||
|
||||
// The mode passed to open() is masked by the process umask. Set the
|
||||
// configured mode explicitly so new objects get the same permissions
|
||||
// regardless of umask, and regardless of whether this or the CreateTemp
|
||||
// fallback path (which also chmods) created the file.
|
||||
err = f.Chmod(p.newFilePerm)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("set temp file mode: %w", err)
|
||||
}
|
||||
|
||||
tmp := &tmpfile{
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
isOTmp: true,
|
||||
procFDName: strconv.Itoa(fd),
|
||||
useODirect: useODirect,
|
||||
size: size,
|
||||
doChown: doChown,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
newDirPerm: p.newDirPerm,
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
isOTmp: true,
|
||||
procFDName: strconv.Itoa(fd),
|
||||
useODirect: useODirect,
|
||||
size: size,
|
||||
doChown: doChown,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
newDirPerm: p.newDirPerm,
|
||||
newFilePerm: p.newFilePerm,
|
||||
}
|
||||
|
||||
// falloc is best effort, its fine if this fails
|
||||
@@ -158,7 +167,7 @@ func (p *Posix) openMkTemp(dir, bucket, obj string, size int64, dofalloc bool, u
|
||||
return nil, fmt.Errorf("close temp file before O_DIRECT reopen: %w", err)
|
||||
}
|
||||
|
||||
fd, err := unix.Open(name, unix.O_RDWR|unix.O_CLOEXEC|unix.O_DIRECT, defaultFilePerm)
|
||||
fd, err := unix.Open(name, unix.O_RDWR|unix.O_CLOEXEC|unix.O_DIRECT, uint32(p.newFilePerm.Perm()))
|
||||
if err == nil {
|
||||
f = os.NewFile(uintptr(fd), name)
|
||||
useODirect = true
|
||||
@@ -176,14 +185,16 @@ func (p *Posix) openMkTemp(dir, bucket, obj string, size int64, dofalloc bool, u
|
||||
}
|
||||
|
||||
tmp := &tmpfile{
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
useODirect: useODirect,
|
||||
size: size,
|
||||
doChown: doChown,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
useODirect: useODirect,
|
||||
size: size,
|
||||
doChown: doChown,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
newDirPerm: p.newDirPerm,
|
||||
newFilePerm: p.newFilePerm,
|
||||
}
|
||||
// falloc is best effort, its fine if this fails
|
||||
if size > 0 && dofalloc {
|
||||
@@ -326,7 +337,7 @@ func (tmp *tmpfile) fallbackLink() error {
|
||||
tempname := tmp.f.Name()
|
||||
|
||||
// reset default file mode because CreateTemp uses 0600
|
||||
tmp.f.Chmod(fs.FileMode(defaultFilePerm))
|
||||
tmp.f.Chmod(tmp.newFilePerm)
|
||||
|
||||
err := tmp.f.Close()
|
||||
if err != nil {
|
||||
@@ -359,7 +370,7 @@ func (tmp *tmpfile) fallbackLink() error {
|
||||
// if this fails fallback to copy
|
||||
backoffMs := initialBackoffMs
|
||||
for range maxDirRecreateRetries {
|
||||
err = backend.MoveFile(tempname, objPath, fs.FileMode(defaultFilePerm))
|
||||
err = backend.MoveFile(tempname, objPath, tmp.newFilePerm)
|
||||
if !errors.Is(err, syscall.ENOENT) {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -40,14 +40,15 @@ type tmpfile struct {
|
||||
bucket string
|
||||
objname string
|
||||
// Retained for compatibility with shared tmpfile methods in otmpfile_common.
|
||||
isOTmp bool
|
||||
procFDName string
|
||||
useODirect bool
|
||||
size int64
|
||||
newDirPerm fs.FileMode
|
||||
uid int
|
||||
gid int
|
||||
doChown bool
|
||||
isOTmp bool
|
||||
procFDName string
|
||||
useODirect bool
|
||||
size int64
|
||||
newDirPerm fs.FileMode
|
||||
newFilePerm fs.FileMode
|
||||
uid int
|
||||
gid int
|
||||
doChown bool
|
||||
}
|
||||
|
||||
func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account, _ bool, _ bool, allowODirect odirectPolicy) (*tmpfile, error) {
|
||||
@@ -85,32 +86,28 @@ func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Accou
|
||||
}
|
||||
|
||||
return &tmpfile{
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
isOTmp: false,
|
||||
procFDName: "",
|
||||
useODirect: false,
|
||||
size: size,
|
||||
newDirPerm: p.newDirPerm,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
doChown: doChown,
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
isOTmp: false,
|
||||
procFDName: "",
|
||||
useODirect: false,
|
||||
size: size,
|
||||
newDirPerm: p.newDirPerm,
|
||||
newFilePerm: p.newFilePerm,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
doChown: doChown,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
// TODO: make this configurable
|
||||
defaultFilePerm fs.FileMode = 0644
|
||||
)
|
||||
|
||||
func (tmp *tmpfile) link() error {
|
||||
tempname := tmp.f.Name()
|
||||
|
||||
objPath := filepath.Join(tmp.bucket, tmp.objname)
|
||||
|
||||
// reset default file mode because CreateTemp uses 0600
|
||||
tmp.f.Chmod(defaultFilePerm)
|
||||
tmp.f.Chmod(tmp.newFilePerm)
|
||||
|
||||
err := tmp.f.Close()
|
||||
if err != nil {
|
||||
@@ -119,7 +116,7 @@ func (tmp *tmpfile) link() error {
|
||||
|
||||
backoffMs := initialBackoffMs
|
||||
for {
|
||||
err = backend.MoveFile(tempname, objPath, defaultFilePerm)
|
||||
err = backend.MoveFile(tempname, objPath, tmp.newFilePerm)
|
||||
if !os.IsNotExist(err) {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -32,8 +32,16 @@ type ScoutfsOpts struct {
|
||||
BucketLinks bool
|
||||
//VersioningDir sets the version directory to enable object versioning
|
||||
VersioningDir string
|
||||
// NewDirPerm specifies the permission to set on newly created directories
|
||||
NewDirPerm fs.FileMode
|
||||
// NewDirPerm specifies the permission to set on newly created directories.
|
||||
// An explicit zero value (0000) is valid and is preserved when set via
|
||||
// SetNewDirPerm.
|
||||
NewDirPerm fs.FileMode
|
||||
newDirPermSet bool
|
||||
// NewFilePerm specifies the permission to set on newly created object
|
||||
// files. Defaults to 0644 when unset; an explicit zero value (0000) is
|
||||
// valid and is preserved when set via SetNewFilePerm.
|
||||
NewFilePerm fs.FileMode
|
||||
newFilePermSet bool
|
||||
// GlacierMode enables glacier emulation for offline files
|
||||
GlacierMode bool
|
||||
// DisableNoArchive prevents setting noarchive on temporary files
|
||||
@@ -61,4 +69,14 @@ type ScoutfsOpts struct {
|
||||
DataIntegrityEtag bool
|
||||
}
|
||||
|
||||
func (o *ScoutfsOpts) SetNewDirPerm(perm fs.FileMode) {
|
||||
o.NewDirPerm = perm
|
||||
o.newDirPermSet = true
|
||||
}
|
||||
|
||||
func (o *ScoutfsOpts) SetNewFilePerm(perm fs.FileMode) {
|
||||
o.NewFilePerm = perm
|
||||
o.newFilePermSet = true
|
||||
}
|
||||
|
||||
var _ backend.Backend = &ScoutFS{}
|
||||
|
||||
@@ -74,18 +74,25 @@ type ScoutFS struct {
|
||||
func New(rootdir string, opts ScoutfsOpts) (*ScoutFS, error) {
|
||||
metastore := meta.XattrMeta{}
|
||||
|
||||
p, err := posix.New(rootdir, metastore, posix.PosixOpts{
|
||||
posixOpts := posix.PosixOpts{
|
||||
ChownUID: opts.ChownUID,
|
||||
ChownGID: opts.ChownGID,
|
||||
BucketLinks: opts.BucketLinks,
|
||||
NewDirPerm: opts.NewDirPerm,
|
||||
VersioningDir: opts.VersioningDir,
|
||||
ValidateBucketNames: opts.ValidateBucketNames,
|
||||
Concurrency: opts.Concurrency,
|
||||
CopyObjectThreshold: opts.CopyObjectThreshold,
|
||||
DefaultEtag: opts.DefaultEtag,
|
||||
DataIntegrityEtag: opts.DataIntegrityEtag,
|
||||
})
|
||||
}
|
||||
if opts.newDirPermSet {
|
||||
posixOpts.SetNewDirPerm(opts.NewDirPerm)
|
||||
}
|
||||
if opts.newFilePermSet {
|
||||
posixOpts.SetNewFilePerm(opts.NewFilePerm)
|
||||
}
|
||||
|
||||
p, err := posix.New(rootdir, metastore, posixOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -24,11 +24,17 @@ import (
|
||||
"github.com/versity/versitygw/backend/posix"
|
||||
)
|
||||
|
||||
// maxFilePerms is the highest accepted value for the file-perms option. The
|
||||
// posix backend only applies permission bits to new objects, so anything
|
||||
// above this would be silently dropped.
|
||||
const maxFilePerms = 0777
|
||||
|
||||
var (
|
||||
chownuid, chowngid bool
|
||||
bucketlinks bool
|
||||
versioningDir string
|
||||
dirPerms uint
|
||||
filePerms uint
|
||||
sidecar string
|
||||
nometa bool
|
||||
forceNoTmpFile bool
|
||||
@@ -90,6 +96,14 @@ will be translated into the file /mnt/fs/gwroot/mybucket/a/b/c/myobject`,
|
||||
DefaultText: "0755",
|
||||
Value: 0755,
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "file-perms",
|
||||
Usage: "default file permissions for new objects",
|
||||
EnvVars: []string{"VGW_FILE_PERMS"},
|
||||
Destination: &filePerms,
|
||||
DefaultText: "0644",
|
||||
Value: 0644,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sidecar",
|
||||
Usage: "use provided sidecar directory to store metadata",
|
||||
@@ -161,6 +175,10 @@ func runPosix(ctx *cli.Context) error {
|
||||
return fmt.Errorf("invalid directory permissions: %d", dirPerms)
|
||||
}
|
||||
|
||||
if filePerms > maxFilePerms {
|
||||
return fmt.Errorf("invalid file permissions: %o, must be within 0000-0777", filePerms)
|
||||
}
|
||||
|
||||
if nometa && sidecar != "" {
|
||||
return fmt.Errorf("cannot use both nometa and sidecar metadata")
|
||||
}
|
||||
@@ -174,7 +192,6 @@ func runPosix(ctx *cli.Context) error {
|
||||
ChownGID: chowngid,
|
||||
BucketLinks: bucketlinks,
|
||||
VersioningDir: versioningDir,
|
||||
NewDirPerm: fs.FileMode(dirPerms),
|
||||
ForceNoTmpFile: forceNoTmpFile,
|
||||
ForceNoCopyFileRange: forceNoCopyFileRange,
|
||||
EnableODirect: enableODirect,
|
||||
@@ -185,6 +202,8 @@ func runPosix(ctx *cli.Context) error {
|
||||
DefaultEtag: defaultEtag,
|
||||
DataIntegrityEtag: dataIntegrityEtag,
|
||||
}
|
||||
opts.SetNewDirPerm(fs.FileMode(dirPerms))
|
||||
opts.SetNewFilePerm(fs.FileMode(filePerms))
|
||||
|
||||
var ms meta.MetadataStorer
|
||||
switch {
|
||||
|
||||
@@ -95,6 +95,14 @@ move interfaces as well as support for tiered filesystems.`,
|
||||
DefaultText: "0755",
|
||||
Value: 0755,
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "file-perms",
|
||||
Usage: "default file permissions for new objects",
|
||||
EnvVars: []string{"VGW_FILE_PERMS"},
|
||||
Destination: &filePerms,
|
||||
DefaultText: "0644",
|
||||
Value: 0644,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "disable-noarchive",
|
||||
Usage: "disable setting noarchive for multipart part uploads",
|
||||
@@ -133,6 +141,10 @@ func runScoutfs(ctx *cli.Context) error {
|
||||
return fmt.Errorf("invalid directory permissions: %d", dirPerms)
|
||||
}
|
||||
|
||||
if filePerms > maxFilePerms {
|
||||
return fmt.Errorf("invalid file permissions: %o, must be within 0000-0777", filePerms)
|
||||
}
|
||||
|
||||
if actionsConcurrency <= 0 {
|
||||
return fmt.Errorf("concurrency must be positive, got %d", actionsConcurrency)
|
||||
}
|
||||
@@ -142,7 +154,6 @@ func runScoutfs(ctx *cli.Context) error {
|
||||
opts.ChownUID = chownuid
|
||||
opts.ChownGID = chowngid
|
||||
opts.BucketLinks = bucketlinks
|
||||
opts.NewDirPerm = fs.FileMode(dirPerms)
|
||||
opts.DisableNoArchive = disableNoArchive
|
||||
opts.VersioningDir = versioningDir
|
||||
opts.ValidateBucketNames = DisableStrictBucketNames
|
||||
@@ -151,6 +162,8 @@ func runScoutfs(ctx *cli.Context) error {
|
||||
opts.CopyObjectThreshold = CopyObjectThreshold
|
||||
opts.DefaultEtag = defaultEtag
|
||||
opts.DataIntegrityEtag = dataIntegrityEtag
|
||||
opts.SetNewDirPerm(fs.FileMode(dirPerms))
|
||||
opts.SetNewFilePerm(fs.FileMode(filePerms))
|
||||
|
||||
be, err := scoutfs.New(ctx.Args().Get(0), opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -596,6 +596,13 @@ ROOT_SECRET_ACCESS_KEY=
|
||||
# as any parent directories automatically created with object uploads.
|
||||
#VGW_DIR_PERMS=0755
|
||||
|
||||
# The default permissions mode when creating new object files is 0644. Use
|
||||
# VGW_FILE_PERMS option to set a different mode for any new file that the
|
||||
# gateway creates for an uploaded object. Only permission bits (0000-0777) are
|
||||
# accepted. Unlike VGW_DIR_PERMS, this mode is applied explicitly and is not
|
||||
# reduced by the process umask.
|
||||
#VGW_FILE_PERMS=0644
|
||||
|
||||
# To enable object versions, the VGW_VERSIONING_DIR option must be set to the
|
||||
# directory that will be used to store the object versions. The version
|
||||
# directory must NOT be a subdirectory of the VGW_BACKEND_ARG directory.
|
||||
@@ -722,6 +729,13 @@ ROOT_SECRET_ACCESS_KEY=
|
||||
# as any parent directories automatically created with object uploads.
|
||||
#VGW_DIR_PERMS=0755
|
||||
|
||||
# The default permissions mode when creating new object files is 0644. Use
|
||||
# VGW_FILE_PERMS option to set a different mode for any new file that the
|
||||
# gateway creates for an uploaded object. Only permission bits (0000-0777) are
|
||||
# accepted. Unlike VGW_DIR_PERMS, this mode is applied explicitly and is not
|
||||
# reduced by the process umask.
|
||||
#VGW_FILE_PERMS=0644
|
||||
|
||||
# To enable object versions, the VGW_VERSIONING_DIR option must be set to the
|
||||
# directory that will be used to store the object versions. The version
|
||||
# directory must NOT be a subdirectory of the VGW_BACKEND_ARG directory.
|
||||
|
||||
Reference in New Issue
Block a user