Skip to content

Commit

Permalink
Merge pull request #38 from YakDriver/textmode
Browse files Browse the repository at this point in the history
Add symbolic representation of changes to make
  • Loading branch information
YakDriver authored May 27, 2020
2 parents 7fe5de2 + f9279cd commit 461d5c0
Show file tree
Hide file tree
Showing 7 changed files with 525 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.2.7
current_version = 0.3.0
commit = False
tag = False
tag_name = {new_version}
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
## Changelog

### 0.3.0

**Commit Delta**: [Change from 0.2.4 release](https://github.com/YakDriver/oschmod/compare/0.2.4...0.3.0)

**Released**: 2020.05.27

**Summary**:

* Adds symbolic representation for mode changes (e.g., `+x`, `u+rwx`, `ugo-x`).

### 0.2.4

**Commit Delta**: [Change from 0.2.2 release](https://github.com/YakDriver/oschmod/compare/0.2.2...0.2.4)
Expand Down
138 changes: 109 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# oschmod
Python chmod that works on Windows and Linux

<p>
<a href="./LICENSE" alt="License">
<img src="https://img.shields.io/github/license/YakDriver/oschmod.svg" /></a>
Expand All @@ -13,37 +10,40 @@ Python chmod that works on Windows and Linux
<img src="https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/YakDriver/oschmod" alt="Mergify"/>
</p>

Use ***oschmod*** to set permissions for files and directories on Windows, Linux and macOS. While Python's standard libraries include a simple command to do this on Linux and macOS (`os.chmod()`), the same command does not work on Windows.
# oschmod

* Read more about [oschmod](https://medium.com/@dirk.avery/securing-files-on-windows-macos-and-linux-7b2b9899992) on Medium
* For more background, have a look at the [oschmod Wiki](https://github.com/YakDriver/oschmod/wiki).
***oschmod*** sets consistent file permissions across Windows, Linux and macOS.

## Usage
## oschmod TL;DR

The problem is that on Linux and macOS, you can easily set distinct permissions for a file's owner, group, and all others. It takes one command and one mode, or, in other words, a number representing bitwise permissions for reading, writing, and executing. On Linux and macOS, you use the `os` module and `os.chmod()`.
***oschmod*** brings `chmod` functionality to **Windows**, macOS, and Linux! If you're not familiar, `chmod` is a handy macOS and Linux-only tool for setting file permissions.

Misleadingly, on Windows, `os.chmod()` does not have the same effect and does not give a warning or error. You think you've protected a file but you have not.
Prior to ***oschmod***, Windows file permissions couldn't be set in the familiar `chmod` way. Tools did not translate `chmod`-style permissions into Windows-style file permissions. Even though Python's `os.chmod()` sets read, write, and execute file permissions, on Windows, `os.chmod()` basically has no effect. Even worse, Python on Windows gives no warnings or errors. If you think you set file permissions on Windows with `os.chmod()`, you're wrong!

For example, on Linux or macOS, to give a file owner read, write, and execute permissions and deny the group and others any permissions (i.e., equivalent of `700`), you can make a single call:
***oschmod*** allows you to set consistent file permissions in a consistent way across platforms.

```python
import os
import stat
os.chmod('myfile', stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
* Read more about [oschmod](https://medium.com/@dirk.avery/securing-files-on-windows-macos-and-linux-7b2b9899992) on Medium
* For more background, have a look at the [oschmod Wiki](https://github.com/YakDriver/oschmod/wiki).

## Installation

```console
$ pip install oschmod
```

Running the same command on Windows does not achieve the same results. The owner isn't given any permissions and the group and others are not denied any permissions. All you can do is restrict anyone from deleting, changing or renaming the file. That's nothing like what `os.chmod()` does.
## GNU Documentation

However, using ***oschmod*** you can use the same command on Windows, macOS or Linux and get the same results:
***oschmod*** changes the file mode bits of each given file according to mode, which can be either a symbolic representation of changes to make, or an octal number representing the bit pattern for the new mode bits.

```python
import oschmod
oschmod.set_mode('myfile', stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
```
The format of a symbolic mode is `[ugoa...][+-=][perms...]` where perms is zero or more letters from the set `rwx`. Multiple symbolic modes can be given, separated by commas.

A combination of the letters `ugoa` controls which users' access to the file will be changed: the user who owns it (`u`), other users in the file's group (`g`), other users not in the file's group (`o`), or all users (`a`). If none of these are given, the effect is as if `a` were given.

*(Modified from the GNU manpage for chmod.)*

## CLI
## Command line interface

The `oschmod` CLI works similarly to `chmod` albeit with fewer options:
***oschmod*** brings the ability to set consistent file permissions using the command line to Windows, macOS, and Linux platforms. If you are familiar with `chmod`, ***oschmod*** works similarly, albeit with fewer options.

```console
$ oschmod -h
Expand All @@ -52,28 +52,108 @@ usage: oschmod [-h] [-R] mode object
Change the mode (permissions) of a file or directory

positional arguments:
mode octal mode of the object
mode octal or symbolic mode of the object
object file or directory

optional arguments:
-h, --help show this help message and exit
-R apply mode recursively
```

For example, to open up a file to the world, you can run this command:
## Command line examples

You can use symbolic (e.g., "u+rw") or octal (e.g., "600) representations of modes. Multiple mode modifications can be made in a single call by separating modifiers with commas.

### Symbolic representation examples

Symbolic representation mode modifiers have three parts:

1. **whom:** To whom does the modification apply? You can include zero or more of `[ugoa]*` where `a` is for all, `u` is for the file owner (i.e., "**u**ser"), `g` is for the file group, and `o` is for others. In other words, `ugo` is equivalent to `a`. Also, if you do not provide a "whom," ***oschmod*** assumes you mean `a` (everyone).
2. **operation:** Which operation should be applied? You must include one and only one operation, `[+-=]{1}`, per modifier (although you can have multiple modifiers). `+` adds permissions, `-` removes permissions, and `=` sets permissions regardless of previous permissions. `+` and `-` modifications often depend on the current permissions.
3. **permission:** Which permission or permissions will be affected? You can include zero or more of `[rwx]*` where `r` is for read, `w` is for write, and `x` is for execute. If you do not include a permission with `+` or `-` (e.g., `u-`), the modifier has no effect. However, if you use no permissions with `=` (e.g., `o=`), all permissions are removed.

**Example 1:** To give everyone execute permissions on a file (all of these are equivalent):

```console
$ oschmod 777 file_name
$ oschmod +x <file name>
$ oschmod a+x <file name>
$ oschmod ugo+x <file name>
```

As another example, you can lock down a file to just the file owner:
**Example 2:** To remove read, write, and execute permissions from the file group and all others (these are equivalent):

```console
$ oschmod 700 file_name
$ oschmod go-rwx <file name>
$ oschmod go= <file name>
```

## Installation
**Example 3:** To give the file owner read and execute permissions, and remove execute permissions from the group and all others:

```console
$ pip install oschmod
$ oschmod u+rx,go-x <file name>
```

**Example 4:** To give everyone all permissions, and then remove execute write from the group, and execute from all others:

```console
$ oschmod a+rwx,g-w,o-x <file name>
```

### Octal representation examples

For more about what octal representations mean, see [this article](https://medium.com/@dirk.avery/securing-files-on-windows-macos-and-linux-7b2b9899992) on Medium.

**Example 5:** To give everyone read, write, and execute permissions on a file:

```console
$ oschmod 777 <file name>
```

**Example 6:** To lock down a file to just give the file owner read, write, and execute permissions and deny all permissions to everyone else:

```console
$ oschmod 700 <file name>
```

## Python usage

You can use ***oschmod*** from Python code. Any of the command line examples above will work very similarly. For example, *Example 4* above, in Python code, would look like this:

```python
import oschmod
oschmod.set_mode("myfile", "a+rwx,g-w,o-x")
```

*Example 5* above, in Python code, could be done in two ways:

```python
import oschmod
oschmod.set_mode("myfile", "777")
oschmod.set_mode("myfile", 0o777)
```

***oschmod*** is compatible with bitwise permissions as defined in the `stat` module. To give a file's owner read, write, and execute permissions and deny the group and others any permissions (i.e., equivalent of `700`):

```python
import oschmod
import stat
oschmod.set_mode('myfile', stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
```

Replacing `os.chmod()` with ***oschmod*** should usually be an easy drop-in replacement. Replacement will allow you to get consistent file permission settings on Windows, macOS, and Linux:

If this is your Python code using `os.chmod()`:

```python
import os
os.chmod('myfile1', 'u+x')
os.chmod('myfile2', 0o777)
```

The replacement using ***oschmod*** is very similar:

```python
import oschmod
oschmod.set_mode('myfile1', 'u+x')
oschmod.set_mode('myfile2', 0o777)
```
81 changes: 75 additions & 6 deletions oschmod/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
"""oschmod module.
Module for working with file permissions.
Module for working with file permissions that are consistent across Windows,
macOS, and Linux.
These bitwise permissions from the stat module can be used with this module:
stat.S_IRWXU # Mask for file owner permissions
Expand All @@ -27,10 +28,10 @@
import os
import platform
import random
import re
import stat
import string


IS_WINDOWS = platform.system() == 'Windows'
HAS_PYWIN32 = False
try:
Expand Down Expand Up @@ -153,7 +154,7 @@
"S_IXOTH"
)

__version__ = "0.2.7"
__version__ = "0.3.0"


def get_mode(path):
Expand All @@ -164,10 +165,26 @@ def get_mode(path):


def set_mode(path, mode):
"""Get bitwise mode (stat) of object (dir or file)."""
"""
Set bitwise mode (stat) of object (dir or file).
Three types of modes can be used:
1. Decimal mode - an integer representation of set bits (eg, 512)
2. Octal mode - a string expressing an octal number (eg, "777")
3. Symbolic representation - a string with modifier symbols (eg, "+x")
"""
new_mode = 0
if isinstance(mode, int):
new_mode = mode
elif isinstance(mode, str):
if '+' in mode or '-' in mode or '=' in mode:
new_mode = get_effective_mode(get_mode(path), mode)
else:
new_mode = int(mode, 8)

if IS_WINDOWS:
return win_set_permissions(path, mode)
return os.chmod(path, mode)
return win_set_permissions(path, new_mode)
return os.chmod(path, new_mode)


def set_mode_recursive(path, mode, dir_mode=None):
Expand Down Expand Up @@ -203,6 +220,51 @@ def set_mode_recursive(path, mode, dir_mode=None):
return set_mode(path, dir_mode)


def _get_effective_mode_multiple(current_mode, modes):
"""Get octal mode, given current mode and symbolic mode modifiers."""
new_mode = current_mode
for mode in modes.split(","):
new_mode = get_effective_mode(new_mode, mode)
return new_mode


def get_effective_mode(current_mode, symbolic):
"""Get octal mode, given current mode and symbolic mode modifier."""
if not isinstance(symbolic, str):
raise AttributeError('symbolic must be a string')

if "," in symbolic:
return _get_effective_mode_multiple(current_mode, symbolic)

result = re.search(r'^\s*([ugoa]*)([-+=])([rwx]*)\s*$', symbolic)
if result is None:
raise AttributeError('bad format of symbolic representation modifier')

whom = result.group(1) or "ugo"
operation = result.group(2)
perm = result.group(3)

if "a" in whom:
whom = "ugo"

# bitwise magic
bit_perm = _get_basic_symbol_to_mode(perm)
mask_mode = ("u" in whom and bit_perm << 6) | \
("g" in whom and bit_perm << 3) | \
("o" in whom and bit_perm << 0)

if operation == "=":
original = ("u" not in whom and current_mode & 448) | \
("g" not in whom and current_mode & 56) | \
("o" not in whom and current_mode & 7)
return mask_mode | original

if operation == "+":
return current_mode | mask_mode

return current_mode & ~mask_mode


def get_object_type(path):
"""Get whether object is file or directory."""
object_type = DIRECTORY
Expand Down Expand Up @@ -302,6 +364,13 @@ def win_get_permissions(path):
return _win_get_permissions(path, get_object_type(path))


def _get_basic_symbol_to_mode(symbol):
"""Calculate numeric value of set of 'rwx'."""
return ("r" in symbol and 1 << 2) | \
("w" in symbol and 1 << 1) | \
("x" in symbol and 1 << 0)


def _win_get_permissions(path, object_type):
"""Get the permissions."""
sec_des = win32security.GetNamedSecurityInfo(
Expand Down
5 changes: 3 additions & 2 deletions oschmod/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ def main():
description='Change the mode (permissions) of a file or directory')
parser.add_argument('-R', action='store_true',
help='apply mode recursively')
parser.add_argument('mode', nargs=1, help='octal mode of the object')
parser.add_argument(
'mode', nargs=1, help='octal or symbolic mode of the object')
parser.add_argument('object', nargs=1, help='file or directory')

args = parser.parse_args()
mode = int(args.mode[0], 8)
mode = args.mode[0]
obj = args.object[0]
if args.R:
oschmod.set_mode_recursive(obj, mode)
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = oschmod
description = Windows and Linux compatible chmod
long_description = file: README.md, CHANGELOG.md
long_description_content_type = text/markdown
version = 0.2.7
version = 0.3.0
author = YakDriver
author_email = [email protected]
url = https://github.com/yakdriver/oschmod
Expand Down
Loading

0 comments on commit 461d5c0

Please sign in to comment.