Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into stable
Browse files Browse the repository at this point in the history
  • Loading branch information
bugy committed Apr 15, 2018
2 parents cf59819 + 7641e03 commit 9e98ec1
Show file tree
Hide file tree
Showing 68 changed files with 3,462 additions and 643 deletions.
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
language: python
python: 3.4
install: pip install typing
before_script:
- cd src
- python3 -m unittest discover -s tests -p "*.py"
- cd ..
script:
- python3 tools/build.py
8 changes: 7 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ https://github.com/whitebird/FontAwesome-cookie
Licensed under http://creativecommons.org/publicdomain/zero/1.0

images/file_download.png (modified)
https://material.io/icons/#ic_file_download
https://material.io/icons/#ic_file_download


images/g-logo-plain.png (modified)
images/g-logo-plain-pressed.png (modified)
https://developers.google.com/identity/branding-guidelines
Licensed under the Creative Commons Attribution 3.0 License
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ GUI is very straightforward and easy-to-use for anyone. Example of the user inte

## Requirements
### Server-side
Python 3.4+ with following modules:
* Tornado
Python 3.4 or higher with the following modules:
* Tornado 4/5
* typing *(for python 3.4 only)*

Some features can require additional modules. Such requirements are specified in a corresponding feature description.

Expand All @@ -46,7 +47,7 @@ Internet connection is not needed. All the files are loaded from the server.

### Developer mode
1. Clone/download the repository
2. Run tools/init.py script (this will download javascript libraries)
2. Run 'tools/init.py --dev' script (this will download javascript libraries)


## Setup and run
Expand Down
3 changes: 2 additions & 1 deletion samples/configs/destroy_world.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"script_path": "./samples/scripts/destroy_world.py",
"description": "This is a very dangerous script, please be careful when running. Don't forget your protective helmet.",
"requires_terminal": false
"requires_terminal": true,
"kill_on_disconnect": false
}
20 changes: 20 additions & 0 deletions samples/configs/parameterized.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@
"two",
"three"
]
},
{
"name": "Very long list",
"description": "List with very long values",
"type": "list",
"param": "--very_long_list",
"values": [
"some quite long line",
"short",
"a bit longer",
"abcdefghijklmopqrstuvwxyz",
"/home/whoever/wherever/temp/stuff/internal/my_important_file-180214.txt",
"abcde-fghijk:lmopqr.stuvwx_yzabcd efghij.klmopq-rstuv%wxyz"
]
},
{
"name": "My file",
"description": "File upload testing",
"type": "file_upload",
"param": "-f"
}
]
}
33 changes: 31 additions & 2 deletions samples/scripts/parameterized.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
#!/bin/sh
#!/bin/bash

echo $@
echo $@

my_file=''

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
-f)
my_file="$2"
shift # past argument
shift # past value
;;
*) # unknown option
POSITIONAL+=("$1") # save it in an array for later
shift # past argument
;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

echo
if [ -z "$my_file" ]; then
echo '-f is empty'
else
echo '-f content:'
cat "$my_file"
fi
34 changes: 34 additions & 0 deletions src/alerts/alerts_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
import threading

LOGGER = logging.getLogger('script_server.alerts_service')


class AlertsService:
def __init__(self, alerts_config):
if alerts_config:
self._destinations = alerts_config.get_destinations()
else:
self._destinations = []

self._running_threads = []

def send_alert(self, title, body, logs=None):
if not self._destinations:
return

def _send():
for destination in self._destinations:
try:
destination.send(title, body, logs)
except:
LOGGER.exception("Couldn't send alert to " + str(destination))

thread = threading.Thread(target=_send, name='AlertThread-' + title)
thread.start()

@staticmethod
def _wait():
for thread in threading.enumerate():
if thread.name.startswith('AlertThread-'):
thread.join()
8 changes: 0 additions & 8 deletions src/alerts/destination_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ def split_addresses(addresses_string):


class EmailDestination(destination_base.Destination):
from_address = None
to_addresses = None
server = None
login = None
password = None
auth_enabled = None
tls = None

def __init__(self, params_dict):
self.from_address = params_dict.get('from')
self.to_addresses = params_dict.get('to')
Expand Down
2 changes: 0 additions & 2 deletions src/alerts/destination_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@


class HttpDestination(destination_base.Destination):
url = None

def __init__(self, params_dict):
self.url = params_dict.get('url')

Expand Down
25 changes: 20 additions & 5 deletions src/auth/auth_base.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import abc


class Authorizer(metaclass=abc.ABCMeta):
class Authenticator(metaclass=abc.ABCMeta):
def __init__(self) -> None:
self.client_visible_config = {}
self.auth_type = None

@abc.abstractmethod
def authenticate(self, username, password):
def authenticate(self, request_handler):
pass

def get_client_visible_config(self):
return self.client_visible_config


class AuthRejectedError(Exception):
"""Credentials, provided by user, were rejected by the authentication mechanism (user is unknown to the server)"""
message = None

def __init__(self, message=None):
self.message = message

Expand All @@ -21,7 +26,17 @@ def get_message(self):
class AuthFailureError(Exception):
"""Server-side error, which shows, that authentication process failed because of some internal error.
These kind of errors are not related to user credentials"""
message = None

def __init__(self, message=None):
self.message = message

def get_message(self):
return self.message


class AuthBadRequestException(Exception):
"""Server-side exception, when the data provided by user has invalid format or some data is missing.
Usually it means wrong behaviour on client-side"""

def __init__(self, message=None):
self.message = message
Expand Down
112 changes: 112 additions & 0 deletions src/auth/auth_google_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import logging
import urllib.parse as urllib_parse

import tornado.auth
from tornado import gen, httpclient, escape

from auth import auth_base
from auth.auth_base import AuthFailureError, AuthBadRequestException
from model import model_helper
from utils.tornado_utils import normalize_url

LOGGER = logging.getLogger('script_server.GoogleOauthAuthorizer')


# noinspection PyProtectedMember
class GoogleOauthAuthenticator(auth_base.Authenticator):
def __init__(self, params_dict):
super().__init__()

self.client_id = model_helper.read_obligatory(params_dict, 'client_id', ' for Google OAuth')

secret_value = model_helper.read_obligatory(params_dict, 'secret', ' for Google OAuth')
self.secret = model_helper.unwrap_conf_value(secret_value)

self.states = {}

self.client_visible_config['client_id'] = self.client_id
self.client_visible_config['oauth_url'] = tornado.auth.GoogleOAuth2Mixin._OAUTH_AUTHORIZE_URL
self.client_visible_config['oauth_scope'] = 'email'

def authenticate(self, request_handler):
code = request_handler.get_argument('code', False)

if not code:
LOGGER.error('Code is not specified')
raise AuthBadRequestException('Missing authorization information. Please contact your administrator')

return self.read_user(code, request_handler)

@gen.coroutine
def read_user(self, code, request_handler):
access_token = yield self.get_access_token(code, request_handler)

oauth_mixin = tornado.auth.GoogleOAuth2Mixin()
user_future = oauth_mixin.oauth2_request(
tornado.auth.GoogleOAuth2Mixin._OAUTH_USERINFO_URL,
access_token=access_token)
user_response = yield user_future

if user_response.get('email'):
return user_response.get('email')

error_message = 'No email field in user response. The response: ' + str(user_response)
LOGGER.error(error_message)
raise AuthFailureError(error_message)

@gen.coroutine
def get_access_token(self, code, request_handler):
body = urllib_parse.urlencode({
'redirect_uri': get_path_for_redirect(request_handler),
'code': code,
'client_id': self.client_id,
'client_secret': self.secret,
'grant_type': 'authorization_code',
})
http_client = httpclient.AsyncHTTPClient()
response = yield http_client.fetch(
tornado.auth.GoogleOAuth2Mixin._OAUTH_ACCESS_TOKEN_URL,
method='POST',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
body=body,
raise_error=False)

response_values = {}
if response.body:
response_values = escape.json_decode(response.body)

if response.error:
if response_values.get('error_description'):
error_text = response_values.get('error_description')
elif response_values.get('error'):
error_text = response_values.get('error')
else:
error_text = str(response.error)

error_message = 'Failed to load access_token: ' + error_text
LOGGER.error(error_message)
raise AuthFailureError(error_message)

response_values = escape.json_decode(response.body)
access_token = response_values.get('access_token')

if not access_token:
message = 'No access token in response: ' + str(response.body)
LOGGER.error(message)
raise AuthFailureError(message)

return access_token


def get_path_for_redirect(request_handler):
referer = request_handler.request.headers.get('Referer')
if not referer:
LOGGER.error('No referer')
raise AuthFailureError('Missing request header. Please contact system administrator')

parse_result = urllib_parse.urlparse(referer)
protocol = parse_result[0]
host = parse_result[1]
path = parse_result[2]

return urllib_parse.urlunparse((protocol, host, path, '', '', ''))
Loading

0 comments on commit 9e98ec1

Please sign in to comment.