New: Displaying folder-based permissions in UI rather than file-based permissions and with selectable sane presets

Fixed: Preserve setgid when applying unix permissions
pull/1807/head
Taloth Saldono 4 years ago committed by Qstick
parent 40df88e37c
commit e2a0b63256

@ -5,6 +5,10 @@
align-items: center;
}
.editableContainer {
width: 100%;
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
@ -22,6 +26,16 @@
margin-left: 12px;
}
.dropdownArrowContainerEditable {
position: absolute;
top: 0;
right: 0;
padding-right: 17px;
width: 30%;
height: 35px;
text-align: right;
}
.dropdownArrowContainerDisabled {
composes: dropdownArrowContainer;

@ -17,6 +17,7 @@ import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import HintedSelectInputOption from './HintedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput';
import styles from './EnhancedSelectInput.css';
function isArrowKey(keyCode) {
@ -169,11 +170,21 @@ class EnhancedSelectInput extends Component {
}
}
onFocus = () => {
if (this.state.isOpen) {
this._removeListener();
this.setState({ isOpen: false });
}
}
onBlur = () => {
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
const origIndex = getSelectedIndex(this.props);
if (origIndex !== this.state.selectedIndex) {
this.setState({ selectedIndex: origIndex });
if (!this.props.isEditable) {
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
const origIndex = getSelectedIndex(this.props);
if (origIndex !== this.state.selectedIndex) {
this.setState({ selectedIndex: origIndex });
}
}
}
@ -297,16 +308,19 @@ class EnhancedSelectInput extends Component {
const {
className,
disabledClassName,
name,
value,
values,
isDisabled,
isEditable,
isFetching,
hasError,
hasWarning,
valueOptions,
selectedValueOptions,
selectedValueComponent: SelectedValueComponent,
optionComponent: OptionComponent
optionComponent: OptionComponent,
onChange
} = this.props;
const {
@ -332,52 +346,94 @@ class EnhancedSelectInput extends Component {
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</div>
</Link>
{
isEditable ?
<div
className={styles.editableContainer}
>
<TextInput
className={className}
name={name}
value={value}
readOnly={isDisabled}
hasError={hasError}
hasWarning={hasWarning}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={onChange}
/>
<Link
className={classNames(
styles.dropdownArrowContainerEditable,
isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer)
}
onPress={this.onPress}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</Link>
</div> :
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</div>
</Link>
}
</Measure>
</div>
)}
@ -502,6 +558,7 @@ EnhancedSelectInput.propTypes = {
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isEditable: PropTypes.bool.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
valueOptions: PropTypes.object.isRequired,
@ -517,6 +574,7 @@ EnhancedSelectInput.defaultProps = {
disabledClassName: styles.isDisabled,
isDisabled: false,
isFetching: false,
isEditable: false,
valueOptions: {},
selectedValueOptions: {},
selectedValueComponent: HintedSelectInputSelectedValue,

@ -25,6 +25,7 @@ import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import TagInputConnector from './TagInputConnector';
import TextInput from './TextInput';
import TextTagInputConnector from './TextTagInputConnector';
import UMaskInput from './UMaskInput';
import styles from './FormInputGroup.css';
function getComponent(type) {
@ -92,6 +93,9 @@ function getComponent(type) {
case inputTypes.TEXT_TAG:
return TextTagInputConnector;
case inputTypes.UMASK:
return UMaskInput;
default:
return TextInput;
}
@ -199,7 +203,7 @@ function FormInputGroup(props) {
}
{
!checkInput && helpTextWarning &&
(!checkInput || helpText) && helpTextWarning &&
<FormInputHelpText
text={helpTextWarning}
isWarning={true}

@ -0,0 +1,53 @@
.inputWrapper {
display: flex;
}
.inputFolder {
composes: input from '~Components/Form/Input.css';
max-width: 100px;
}
.inputUnitWrapper {
position: relative;
width: 100%;
}
.inputUnit {
composes: inputUnit from '~Components/Form/FormInputGroup.css';
right: 40px;
font-family: $monoSpaceFontFamily;
}
.unit {
font-family: $monoSpaceFontFamily;
}
.details {
margin-top: 5px;
margin-left: 17px;
line-height: 20px;
> div {
display: flex;
label {
flex: 0 0 50px;
}
.value {
width: 50px;
text-align: right;
}
.unit {
width: 90px;
text-align: right;
}
}
}
.readOnly {
background-color: #eee;
}

@ -0,0 +1,133 @@
/* eslint-disable no-bitwise */
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import EnhancedSelectInput from './EnhancedSelectInput';
import styles from './UMaskInput.css';
const umaskOptions = [
{
key: '755',
value: '755 - Owner write, Everyone else read',
hint: 'drwxr-xr-x'
},
{
key: '775',
value: '775 - Owner & Group write, Other read',
hint: 'drwxrwxr-x'
},
{
key: '770',
value: '770 - Owner & Group write',
hint: 'drwxrwx---'
},
{
key: '750',
value: '750 - Owner write, Group read',
hint: 'drwxr-x---'
},
{
key: '777',
value: '777 - Everyone write',
hint: 'drwxrwxrwx'
}
];
function formatPermissions(permissions) {
const hasSticky = permissions & 0o1000;
const hasSetGID = permissions & 0o2000;
const hasSetUID = permissions & 0o4000;
let result = '';
for (let i = 0; i < 9; i++) {
const bit = (permissions & (1 << i)) !== 0;
let digit = bit ? 'xwr'[i % 3] : '-';
if (i === 6 && hasSetUID) {
digit = bit ? 's' : 'S';
} else if (i === 3 && hasSetGID) {
digit = bit ? 's' : 'S';
} else if (i === 0 && hasSticky) {
digit = bit ? 't' : 'T';
}
result = digit + result;
}
return result;
}
class UMaskInput extends Component {
//
// Render
render() {
const {
name,
value,
onChange
} = this.props;
const valueNum = parseInt(value, 8);
const umaskNum = 0o777 & ~valueNum;
const umask = umaskNum.toString(8).padStart(4, '0');
const folderNum = 0o777 & ~umaskNum;
const folder = folderNum.toString(8).padStart(3, '0');
const fileNum = 0o666 & ~umaskNum;
const file = fileNum.toString(8).padStart(3, '0');
const unit = formatPermissions(folderNum);
const values = umaskOptions.map((v) => {
return { ...v, hint: <span className={styles.unit}>{v.hint}</span> };
});
return (
<div>
<div className={styles.inputWrapper}>
<div className={styles.inputUnitWrapper}>
<EnhancedSelectInput
name={name}
value={value}
values={values}
isEditable={true}
onChange={onChange}
/>
<div className={styles.inputUnit}>
d{unit}
</div>
</div>
</div>
<div className={styles.details}>
<div>
<label>UMask</label>
<div className={styles.value}>{umask}</div>
</div>
<div>
<label>Folder</label>
<div className={styles.value}>{folder}</div>
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
</div>
<div>
<label>File</label>
<div className={styles.value}>{file}</div>
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
</div>
</div>
</div>
);
}
}
UMaskInput.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func
};
export default UMaskInput;

@ -20,6 +20,7 @@ export const DYNAMIC_SELECT = 'dynamicSelect';
export const TAG = 'tag';
export const TEXT = 'text';
export const TEXT_TAG = 'textTag';
export const UMASK = 'umask';
export const all = [
AUTO_COMPLETE,
@ -43,5 +44,6 @@ export const all = [
SERIES_TYPE_SELECT,
TAG,
TEXT,
TEXT_TAG
TEXT_TAG,
UMASK
];

@ -381,17 +381,32 @@ class MediaManagement extends Component {
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>File chmod mode</FormLabel>
<FormLabel>chmod Folder</FormLabel>
<FormInputGroup
type={inputTypes.UMASK}
name="chmodFolder"
helpText="Octal, applied during import/rename to media folders and files (without execute bits)"
helpTextWarning="This only works if the user running Lidarr is the owner of the file. It's better to ensure the download client sets the permissions properly."
onChange={onInputChange}
{...settings.chmodFolder}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>chown Group</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="fileChmod"
helpTexts={[
'Octal, applied to media files when imported/renamed by Lidarr',
'The same mode is applied to movie/sub folders with the execute bit added, e.g., 0644 becomes 0755'
]}
name="chownGroup"
helpText="Group name or gid. Use gid for remote file systems."
helpTextWarning="This only works if the user running Lidarr is the owner of the file. It's better to ensure the download client uses the same group as Lidarr."
values={fileDateOptions}
onChange={onInputChange}
{...settings.fileChmod}
{...settings.chownGroup}
/>
</FormGroup>
</FieldSet>

@ -8,11 +8,11 @@ namespace Lidarr.Api.V1.Config
{
public class MediaManagementConfigModule : LidarrConfigModule<MediaManagementConfigResource>
{
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FileChmodValidator fileChmodValidator)
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator)
: base(configService)
{
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.FileChmod).SetValidator(fileChmodValidator).When(c => !string.IsNullOrEmpty(c.FileChmod) && (OsInfo.IsLinux || OsInfo.IsOsx));
SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && (OsInfo.IsLinux || OsInfo.IsOsx));
SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
}

@ -19,7 +19,8 @@ namespace Lidarr.Api.V1.Config
public AllowFingerprinting AllowFingerprinting { get; set; }
public bool SetPermissionsLinux { get; set; }
public string FileChmod { get; set; }
public string ChmodFolder { get; set; }
public string ChownGroup { get; set; }
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
public int MinimumFreeSpaceWhenImporting { get; set; }
@ -46,7 +47,8 @@ namespace Lidarr.Api.V1.Config
AllowFingerprinting = model.AllowFingerprinting,
SetPermissionsLinux = model.SetPermissionsLinux,
FileChmod = model.FileChmod,
ChmodFolder = model.ChmodFolder,
ChownGroup = model.ChownGroup,
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
MinimumFreeSpaceWhenImporting = model.MinimumFreeSpaceWhenImporting,

@ -37,7 +37,7 @@ namespace NzbDrone.Common.Disk
public abstract long? GetAvailableSpace(string path);
public abstract void InheritFolderPermissions(string filename);
public abstract void SetEveryonePermissions(string filename);
public abstract void SetPermissions(string path, string mask);
public abstract void SetPermissions(string path, string mask, string group);
public abstract void CopyPermissions(string sourcePath, string targetPath);
public abstract long? GetTotalSize(string path);
@ -534,7 +534,7 @@ namespace NzbDrone.Common.Disk
}
}
public virtual bool IsValidFilePermissionMask(string mask)
public virtual bool IsValidFolderPermissionMask(string mask)
{
throw new NotSupportedException();
}

@ -12,7 +12,7 @@ namespace NzbDrone.Common.Disk
long? GetAvailableSpace(string path);
void InheritFolderPermissions(string filename);
void SetEveryonePermissions(string filename);
void SetPermissions(string path, string mask);
void SetPermissions(string path, string mask, string group);
void CopyPermissions(string sourcePath, string targetPath);
long? GetTotalSize(string path);
DateTime FolderGetCreationTime(string path);
@ -58,6 +58,6 @@ namespace NzbDrone.Common.Disk
List<IFileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
void RemoveEmptySubfolders(string path);
void SaveStream(Stream stream, string path);
bool IsValidFilePermissionMask(string mask);
bool IsValidFolderPermissionMask(string mask);
}
}

@ -255,11 +255,18 @@ namespace NzbDrone.Core.Configuration
set { SetValue("SetPermissionsLinux", value); }
}
public string FileChmod
public string ChmodFolder
{
get { return GetValue("FileChmod", "0644"); }
get { return GetValue("ChmodFolder", "755"); }
set { SetValue("FileChmod", value); }
set { SetValue("ChmodFolder", value); }
}
public string ChownGroup
{
get { return GetValue("ChownGroup", ""); }
set { SetValue("ChownGroup", value); }
}
public string MetadataSource

@ -42,7 +42,8 @@ namespace NzbDrone.Core.Configuration
//Permissions (Media Management)
bool SetPermissionsLinux { get; set; }
string FileChmod { get; set; }
string ChmodFolder { get; set; }
string ChownGroup { get; set; }
//Indexers
int Retention { get; set; }

@ -1,4 +1,7 @@
using System;
using System.Data;
using FluentMigrator;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
@ -9,6 +12,45 @@ namespace NzbDrone.Core.Datastore.Migration
protected override void MainDbUpgrade()
{
Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser')");
Execute.WithConnection(ConvertFileChmodToFolderChmod);
}
private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tran)
{
using (IDbCommand getFileChmodCmd = conn.CreateCommand())
{
getFileChmodCmd.Transaction = tran;
getFileChmodCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'filechmod'";
var fileChmod = getFileChmodCmd.ExecuteScalar() as string;
if (fileChmod != null)
{
if (fileChmod.IsNotNullOrWhiteSpace())
{
// Convert without using mono libraries. We take the 'r' bits and shifting them to the 'x' position, preserving everything else.
var fileChmodNum = Convert.ToInt32(fileChmod, 8);
var folderChmodNum = fileChmodNum | ((fileChmodNum & 0x124) >> 2);
var folderChmod = Convert.ToString(folderChmodNum, 8).PadLeft(3, '0');
using (IDbCommand insertCmd = conn.CreateCommand())
{
insertCmd.Transaction = tran;
insertCmd.CommandText = "INSERT INTO Config (Key, Value) VALUES ('chmodfolder', ?)";
insertCmd.AddParameter(folderChmod);
insertCmd.ExecuteNonQuery();
}
}
using (IDbCommand deleteCmd = conn.CreateCommand())
{
deleteCmd.Transaction = tran;
deleteCmd.CommandText = "DELETE FROM Config WHERE Key = 'filechmod'";
deleteCmd.ExecuteNonQuery();
}
}
}
}
}
}

@ -54,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles
}
else
{
SetMonoPermissions(path, _configService.FileChmod);
SetMonoPermissions(path);
}
}
@ -62,7 +62,7 @@ namespace NzbDrone.Core.MediaFiles
{
if (OsInfo.IsNotWindows)
{
SetMonoPermissions(path, _configService.FileChmod);
SetMonoPermissions(path);
}
}
@ -75,7 +75,7 @@ namespace NzbDrone.Core.MediaFiles
}
}
private void SetMonoPermissions(string path, string permissions)
private void SetMonoPermissions(string path)
{
if (!_configService.SetPermissionsLinux)
{
@ -84,7 +84,7 @@ namespace NzbDrone.Core.MediaFiles
try
{
_diskProvider.SetPermissions(path, permissions);
_diskProvider.SetPermissions(path, _configService.ChmodFolder, _configService.ChownGroup);
}
catch (Exception ex)
{

@ -149,7 +149,7 @@ namespace NzbDrone.Core.Update
// Set executable flag on update app
if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore))
{
_diskProvider.SetPermissions(_appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime), "0755");
_diskProvider.SetPermissions(_appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime), "0755", null);
}
_logger.Info("Starting update client {0}", _appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime));

@ -3,11 +3,11 @@ using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Validation
{
public class FileChmodValidator : PropertyValidator
public class FolderChmodValidator : PropertyValidator
{
private readonly IDiskProvider _diskProvider;
public FileChmodValidator(IDiskProvider diskProvider)
public FolderChmodValidator(IDiskProvider diskProvider)
: base("Must contain a valid Unix permissions octal")
{
_diskProvider = diskProvider;
@ -20,7 +20,7 @@ namespace NzbDrone.Core.Validation
return false;
}
return _diskProvider.IsValidFilePermissionMask(context.PropertyValue.ToString());
return _diskProvider.IsValidFolderPermissionMask(context.PropertyValue.ToString());
}
}
}

@ -170,15 +170,15 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
Syscall.stat(tempFile, out var fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0444");
Subject.SetPermissions(tempFile, "644");
Subject.SetPermissions(tempFile, "755", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
Subject.SetPermissions(tempFile, "0644");
Subject.SetPermissions(tempFile, "0755", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
Subject.SetPermissions(tempFile, "1664");
Subject.SetPermissions(tempFile, "1775", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1664");
}
@ -195,56 +195,49 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
Syscall.stat(tempPath, out var fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0555");
Subject.SetPermissions(tempPath, "644");
Subject.SetPermissions(tempPath, "755", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
Subject.SetPermissions(tempPath, "0644");
Subject.SetPermissions(tempPath, "0755", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
Subject.SetPermissions(tempPath, "1664");
Subject.SetPermissions(tempPath, "1775", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1775");
Subject.SetPermissions(tempPath, "775");
Subject.SetPermissions(tempPath, "775", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
Subject.SetPermissions(tempPath, "640");
Subject.SetPermissions(tempPath, "750", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0750");
Subject.SetPermissions(tempPath, "0041");
Subject.SetPermissions(tempPath, "0051", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0051");
// reinstate sane permissions so fokder can be cleaned up
Subject.SetPermissions(tempPath, "775");
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
}
[Test]
public void IsValidFilePermissionMask_should_return_correct()
public void IsValidFolderPermissionMask_should_return_correct()
{
// Files may not be executable
Subject.IsValidFilePermissionMask("0777").Should().BeFalse();
Subject.IsValidFilePermissionMask("0544").Should().BeFalse();
Subject.IsValidFilePermissionMask("0454").Should().BeFalse();
Subject.IsValidFilePermissionMask("0445").Should().BeFalse();
// No special bits should be set
Subject.IsValidFilePermissionMask("1644").Should().BeFalse();
Subject.IsValidFilePermissionMask("2644").Should().BeFalse();
Subject.IsValidFilePermissionMask("4644").Should().BeFalse();
Subject.IsValidFilePermissionMask("7644").Should().BeFalse();
// Files should be readable and writeable by owner
Subject.IsValidFilePermissionMask("0400").Should().BeFalse();
Subject.IsValidFilePermissionMask("0000").Should().BeFalse();
Subject.IsValidFilePermissionMask("0200").Should().BeFalse();
Subject.IsValidFilePermissionMask("0600").Should().BeTrue();
Subject.IsValidFolderPermissionMask("1755").Should().BeFalse();
Subject.IsValidFolderPermissionMask("2755").Should().BeFalse();
Subject.IsValidFolderPermissionMask("4755").Should().BeFalse();
Subject.IsValidFolderPermissionMask("7755").Should().BeFalse();
// Folder should be readable and writeable by owner
Subject.IsValidFolderPermissionMask("0000").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0100").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0200").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0300").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0400").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0500").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0600").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0700").Should().BeTrue();
}
}
}

@ -77,50 +77,66 @@ namespace NzbDrone.Mono.Disk
{
}
public override void SetPermissions(string path, string mask)
public override void SetPermissions(string path, string mask, string group)
{
_logger.Debug("Setting permissions: {0} on {1}", mask, path);
var permissions = NativeConvert.FromOctalPermissionString(mask);
if (_fileSystem.Directory.Exists(path))
if (_fileSystem.File.Exists(path))
{
permissions = GetFolderPermissions(permissions);
permissions = GetFilePermissions(permissions);
}
// Preserve non-access permissions
if (Syscall.stat(path, out var curStat) < 0)
{
var error = Stdlib.GetLastError();
throw new LinuxPermissionsException("Error getting current permissions: " + error);
}
permissions |= curStat.st_mode & ~FilePermissions.ACCESSPERMS;
if (Syscall.chmod(path, permissions) < 0)
{
var error = Stdlib.GetLastError();
throw new LinuxPermissionsException("Error setting permissions: " + error);
}
var groupId = GetGroupId(group);
if (Syscall.chown(path, unchecked((uint)-1), groupId) < 0)
{
var error = Stdlib.GetLastError();
throw new LinuxPermissionsException("Error setting group: " + error);
}
}
private static FilePermissions GetFolderPermissions(FilePermissions permissions)
private static FilePermissions GetFilePermissions(FilePermissions permissions)
{
permissions |= (FilePermissions)((int)(permissions & (FilePermissions.S_IRUSR | FilePermissions.S_IRGRP | FilePermissions.S_IROTH)) >> 2);
permissions &= ~(FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH);
return permissions;
}
public override bool IsValidFilePermissionMask(string mask)
public override bool IsValidFolderPermissionMask(string mask)
{
try
{
var permissions = NativeConvert.FromOctalPermissionString(mask);
if ((permissions & (FilePermissions.S_ISUID | FilePermissions.S_ISGID | FilePermissions.S_ISVTX)) != 0)
{
return false;
}
if ((permissions & (FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH)) != 0)
if ((permissions & ~FilePermissions.ACCESSPERMS) != 0)
{
// Only allow access permissions
return false;
}
if ((permissions & (FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) != (FilePermissions.S_IRUSR | FilePermissions.S_IWUSR))
if ((permissions & FilePermissions.S_IRWXU) != FilePermissions.S_IRWXU)
{
// We expect at least full owner permissions (700)
return false;
}

@ -128,7 +128,7 @@ namespace NzbDrone.Update.UpdateEngine
// Set executable flag on Lidarr app
if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore))
{
_diskProvider.SetPermissions(Path.Combine(installationFolder, "Lidarr"), "0755");
_diskProvider.SetPermissions(Path.Combine(installationFolder, "Lidarr"), "0755", null);
}
}
catch (Exception e)

@ -102,7 +102,7 @@ namespace NzbDrone.Windows.Disk
}
}
public override void SetPermissions(string path, string mask)
public override void SetPermissions(string path, string mask, string group)
{
}

Loading…
Cancel
Save