Skip to content

Commit

Permalink
Merge pull request #258 from tableau/dev
Browse files Browse the repository at this point in the history
v0.4
  • Loading branch information
0golovatyi committed Apr 16, 2019
2 parents 00664c7 + 26ab5ab commit b7e3a0c
Show file tree
Hide file tree
Showing 28 changed files with 469 additions and 134 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -47,6 +47,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
*_trial_temp*

# Translations
*.mo
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Improvements

- Added basic access authentication (all methods except /info)
- tabpy-tools can deploy models to TabPy with authentication on
- Increased unit tests coverage
- Travis CI for merge requests: unit tests executed, code style checking

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Expand Up @@ -82,7 +82,7 @@ or npm [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli).
TOC for markdown file is built with [markdown-toc](https://www.npmjs.com/package/markdown-toc):

```sh
markdownlint -i docs/server-startup.md
markdown-toc -i docs/server-startup.md
```

## TabPy with Swagger
Expand Down
2 changes: 1 addition & 1 deletion VERSION
@@ -1 +1 @@
0.4
0.4
47 changes: 32 additions & 15 deletions docs/server-config.md
@@ -1,9 +1,23 @@
# TabPy Server Configuration Instructions

<!-- markdownlint-disable MD004 -->
<!-- toc -->

- [Configuring HTTP vs HTTPS](#configuring-http-vs-https)
- [Authentication](#authentication)
* [Enabling Authentication](#enabling-authentication)
* [Password File](#password-file)
* [Adding an Account](#adding-an-account)
* [Updating an Account](#updating-an-account)
* [Deleting an Account](#deleting-an-account)

<!-- tocstop -->
<!-- markdownlint-enable MD004 -->

Default settings for TabPy may be viewed in the
tabpy_server/common/default.conf file. This file also contains a
commented example of how to set up your TabPy server to only
serve HTTPS traffic.
tabpy_server/common/default.conf file. This file also contains
commented examples of how to set up your TabPy server to only
serve HTTPS traffic and enable authentication.

Change settings by:

Expand All @@ -17,7 +31,10 @@ Change settings by:
2. Modifying default.conf.
3. Specifying your own config file as a command line parameter.
- i.e. Running the command:
```python tabpy.py --config=path\to\my\config```

```sh
python tabpy.py --config=path\to\my\config
```

The default config file is provided to show you the default values but does not
need to be present to run TabPy.
Expand Down Expand Up @@ -50,8 +67,8 @@ for more details).

### Enabling Authentication

To enable the feature specify `TABPY_PWD_FILE` parameter in TabPy
configuration file with a fully qualified name:
To enable the feature specify the `TABPY_PWD_FILE` parameter in the
TabPy configuration file with a fully qualified name:

```sh
TABPY_PWD_FILE = c:\path\to\password\file.txt
Expand All @@ -60,14 +77,14 @@ TABPY_PWD_FILE = c:\path\to\password\file.txt
### Password File

Password file is a text file containing usernames and hashed passwords
per line separated by space. For username only ASCII characters
supported.
per line separated by single space. For username only ASCII characters
are supported.

**It is highly recommended to restrict access to the password file
with hosting OS mechanisms. Ideally the file should only be accessible
for reading with account TabPy runs as and TabPy admin account.**
for reading with the account under which TabPy runs and TabPy admin account.**

There is `utils/user_management.py` utility to operate with
There is a `utils/user_management.py` utility to operate with
accounts in the password file. Run `utils/user_management.py -h` to
see how to use it.

Expand All @@ -82,20 +99,20 @@ command providing user name, password (optional) and password file:
python utils/user_management.py add -u <username> -p <password> -f <pwdfile>
```

If `-p` agrument is not provided (recommended) password for the user name
will be generated.
If the (recommended) `-p` argument is not provided a password for the user name
will be generated and displayed in the command line.

### Updating an Account

To update password for an account run `utils/user_management.py` utility
To update the password for an account run `utils/user_management.py` utility
with `update` command:

```sh
python utils/user_management.py update -u <username> -p <password> -f <pwdfile>
```

If `-p` agrument is not provided (recommended) password for the user name
will be generated.
If the (recommended) `-p` agrument is not provided a password for the user name
will be generated and displayed in the command line.

### Deleting an Account

Expand Down
21 changes: 21 additions & 0 deletions docs/tabpy-tools.md
Expand Up @@ -6,6 +6,7 @@ on TabPy server.
<!-- toc -->

- [Connecting to TabPy](#connecting-to-tabpy)
- [Authentication](#authentication)
- [Deploying a Function](#deploying-a-function)
- [Providing Schema Metadata](#providing-schema-metadata)
- [Querying an Endpoint](#querying-an-endpoint)
Expand All @@ -30,6 +31,26 @@ The URL and port are where the Tableau-Python-Server process has been started -
more info can be found in the
[server section](server-startup.md#Command-Line-Arguments) of the documentation.

## Authentication

When TabPy is configured with authentication feature on, client code
has to specify the credentials to use during model deployment with
`set_credentials` call for a client:

```python

client.set_credentials('username', 'P@ssw0rd')

```

Credentials only need to be set once for all futher client operations.

In case credentials are not provided when required deployment will
fail with "Unauthorized" code (401).

For how to configure and enable authentication feature for TabPy see
[TabPy Server Configuration Instructions](server-config.md).

## Deploying a Function

A persisted endpoint is backed by a Python method. For example:
Expand Down
2 changes: 1 addition & 1 deletion startup.cmd
Expand Up @@ -49,7 +49,7 @@ ECHO Parsing parameters...
SET PYTHONPATH=%TABPY_ROOT%\tabpy-server;%TABPY_ROOT%\tabpy-tools;%PYTHONPATH%
SET STARTUP_CMD=python tabpy-server\tabpy_server\tabpy.py
IF [%1] NEQ [] (
ECHO Using config file at %TABPY_ROOT%\tabpy-server\tabpy_server\%1
ECHO Using config file at %1
SET STARTUP_CMD=%STARTUP_CMD% --config=%1
)

Expand Down
7 changes: 2 additions & 5 deletions tabpy-server/server_tests/test_config.py
Expand Up @@ -35,7 +35,8 @@ def test_no_config_file(self, mock_os, mock_file_exists,

getenv_calls = [call('TABPY_PORT', 9004),
call('TABPY_QUERY_OBJECT_PATH', '/tmp/query_objects'),
call('TABPY_STATE_PATH', './')]
call('TABPY_STATE_PATH',
'./tabpy-server/tabpy_server')]
mock_os.getenv.assert_has_calls(getenv_calls, any_order=True)
self.assertEqual(len(mock_file_exists.mock_calls), 2)
self.assertEqual(len(mock_psws.mock_calls), 1)
Expand Down Expand Up @@ -113,15 +114,11 @@ def raise_attribute_error():
def __init__(self, *args, **kwargs):
super(TestTransferProtocolValidation, self).__init__(*args, **kwargs)
self.fp = None
self.cwd = os.getcwd()
self.tabpy_cwd = os.path.join(self.cwd, 'tabpy-server', 'tabpy_server')

def setUp(self):
os.chdir(self.tabpy_cwd)
self.fp = NamedTemporaryFile(mode='w+t', delete=False)

def tearDown(self):
os.chdir(self.cwd)
os.remove(self.fp.name)
self.fp = None

Expand Down
57 changes: 57 additions & 0 deletions tabpy-server/server_tests/test_evaluation_plane_handler.py
Expand Up @@ -67,6 +67,20 @@ def setUpClass(cls):
'"script":"res=[]\\nfor i in range(len(_arg1)):\\n '\
'res.append(_arg1[i] * _arg2[i])\\nreturn res"}'

cls.script_not_present =\
'{"data":{"_arg1":[2,3],"_arg2":[3,-1]},'\
'"":"res=[]\\nfor i in range(len(_arg1)):\\n '\
'res.append(_arg1[i] * _arg2[i])\\nreturn res"}'

cls.args_not_present =\
'{"script":"res=[]\\nfor i in range(len(_arg1)):\\n '\
'res.append(_arg1[i] * _arg2[i])\\nreturn res"}'

cls.args_not_sequential =\
'{"data":{"_arg1":[2,3],"_arg3":[3,-1]},'\
'"script":"res=[]\\nfor i in range(len(_arg1)):\\n '\
'res.append(_arg1[i] * _arg3[i])\\nreturn res"}'

@classmethod
def tearDownClass(cls):
cls.patcher.stop()
Expand Down Expand Up @@ -111,3 +125,46 @@ def test_valid_creds_pass(self):
decode('utf-8'))
})
self.assertEqual(200, response.code)

def test_null_request(self):
response = self.fetch('')
self.assertEqual(599, response.code)

def test_script_not_present(self):
response = self.fetch(
'/evaluate',
method='POST',
body=self.script_not_present,
headers={
'Authorization': 'Basic {}'.
format(
base64.b64encode('username:password'.encode('utf-8')).
decode('utf-8'))
})
self.assertEqual(400, response.code)

def test_arguments_not_present(self):
response = self.fetch(
'/evaluate',
method='POST',
body=self.args_not_present,
headers={
'Authorization': 'Basic {}'.
format(
base64.b64encode('username:password'.encode('utf-8')).
decode('utf-8'))
})
self.assertEqual(500, response.code)

def test_arguments_not_sequential(self):
response = self.fetch(
'/evaluate',
method='POST',
body=self.args_not_sequential,
headers={
'Authorization': 'Basic {}'.
format(
base64.b64encode('username:password'.encode('utf-8')).
decode('utf-8'))
})
self.assertEqual(400, response.code)
43 changes: 37 additions & 6 deletions tabpy-server/server_tests/test_pwd_file.py
Expand Up @@ -13,17 +13,12 @@

class TestPasswordFile(unittest.TestCase):
def setUp(self):
self.cwd = pathlib.Path.cwd()
self.tabpy_cwd = self.cwd / 'tabpy-server' / 'tabpy_server'
os.chdir(self.tabpy_cwd)

self.config_file = NamedTemporaryFile(mode='w', delete=False)
self.config_file.close()
self.pwd_file = NamedTemporaryFile(mode='w', delete=False)
self.pwd_file.close()

def tearDown(self):
os.chdir(self.cwd)
os.remove(self.config_file.name)
self.config_file = None
os.remove(self.pwd_file.name)
Expand Down Expand Up @@ -85,7 +80,43 @@ def test_given_one_password_in_pwd_file_expect_one_credentials_entry(self):
self.assertIn(login, app.credentials)
self.assertEqual(app.credentials[login], pwd)

def test_given_one_login_many_times_in_pwd_file_expect_app_fails(self):
def test_given_username_but_no_password_expect_parsing_fails(self):
self._set_file(self.config_file.name,
"[TabPy]\n"
"TABPY_PWD_FILE = {}".format(self.pwd_file.name))

login = 'user_name_123'
pwd = ''
self._set_file(self.pwd_file.name,
"# passwords\n"
"\n"
"{} {}".format(login, pwd))

with self.assertRaises(RuntimeError) as cm:
app = TabPyApp(self.config_file.name)
ex = cm.exception
self.assertEqual('Failed to read password file {}'.format(
self.pwd_file.name), ex.args[0])

def test_given_duplicate_usernames_expect_parsing_fails(self):
self._set_file(self.config_file.name,
"[TabPy]\n"
"TABPY_PWD_FILE = {}".format(self.pwd_file.name))

login = 'user_name_123'
pwd = 'hashedpw'
self._set_file(self.pwd_file.name,
"# passwords\n"
"\n"
"{} {}\n{} {}".format(login, pwd, login, pwd))

with self.assertRaises(RuntimeError) as cm:
app = TabPyApp(self.config_file.name)
ex = cm.exception
self.assertEqual('Failed to read password file {}'.format(
self.pwd_file.name), ex.args[0])

def test_given_one_line_with_too_many_params_expect_app_fails(self):
self._set_file(self.config_file.name,
"[TabPy]\n"
"TABPY_PWD_FILE = {}".format(self.pwd_file.name))
Expand Down

0 comments on commit b7e3a0c

Please sign in to comment.