Skip to content
Permalink
Browse files

Issue #9 resource auth (#259)

* #9 DB and generic structure for Resource.auth

* #9 first stab an generic Resource Auth via Plugins

* #9 finalized generic Resource Auth via Plugins

* #9 finalized generic Resource Auth via Plugins

* #9 finalized generic Resource Auth via Plugins - fix test

* #9 finalized generic Resource Auth via Plugins - msgs and translations
  • Loading branch information...
justb4 committed Jul 3, 2019
1 parent a463d2a commit 3abff6a374e47b6e6b097932903598e124753c35
@@ -47,6 +47,7 @@
from enums import RESOURCE_TYPES
from models import Resource, Run, ProbeVars, CheckVars, Tag, User, Recipient
from factory import Factory
from resourceauth import ResourceAuth
from util import send_email, geocode, format_checked_datetime, \
format_run_status, format_obj_value
import views
@@ -669,6 +670,8 @@ def update(resource_identifier):
elif key == 'notify_webhooks':
resource.set_recipients('webhook',
[v for v in value if v.strip()])
elif key == 'auth':
resource.auth = value
elif getattr(resource, key) != resource_identifier_dict[key]:
# Update other resource attrs, mainly 'name'
setattr(resource, key, resource_identifier_dict[key])
@@ -749,6 +752,7 @@ def edit_resource(resource_identifier):
lang=g.current_lang,
resource=resource,
suggestions=suggestions,
auths_avail=ResourceAuth.get_auth_defs(),
probes_avail=probes_avail)


@@ -111,6 +111,9 @@

# Checkers
'GeoHealthCheck.plugins.check.checks',

# Resource Auth Plugins
'GeoHealthCheck.plugins.resourceauth.resourceauths',
]

# Entry for User Plugins: will be added to default core GHC_PLUGINS
@@ -8,6 +8,7 @@ class Factory:
Object, Function class Factory (Pattern).
Based on: http://stackoverflow.com/questions/2226330/
instantiate-a-python-class-from-a-name
Also contains introspection util functions.
"""

@staticmethod
@@ -117,3 +118,10 @@ def get_class_for_method(method):
else:
classes = list(c.__bases__) + classes
return None

@staticmethod
def full_class_name_for_obj(o):
module = o.__class__.__module__
if module is None or module == str.__class__.__module__:
return o.__class__.__name__
return module + '.' + o.__class__.__name__
@@ -0,0 +1,31 @@
"""empty message
Revision ID: f72ff1ac3967
Revises: 90e1c865a561
Create Date: 2019-06-24 09:33:20.664465
"""
from alembic import op
import sqlalchemy as sa
from GeoHealthCheck.migrations import alembic_helpers

# revision identifiers, used by Alembic.
revision = 'f72ff1ac3967'
down_revision = '90e1c865a561'
branch_labels = None
depends_on = None



def upgrade():
if not alembic_helpers.table_has_column('resource', 'auth'):
print('Column auth not present in resource table, will create')
op.add_column(u'resource', sa.Column('auth', sa.Text(),
nullable=True, default=None, server_default=None))
else:
print('Column auth already present in resource table')


def downgrade():
print('Dropping Column auth from resource table')
op.drop_column(u'resource', 'auth')
@@ -42,6 +42,7 @@
from enums import RESOURCE_TYPES
from factory import Factory
from init import App
from resourceauth import ResourceAuth
from wtforms.validators import Email, ValidationError
from owslib.util import bind_url

@@ -369,14 +370,17 @@ class Resource(DB.Model):
backref=DB.backref('username2', lazy='dynamic'))
tags = DB.relationship('Tag', secondary=resource_tags, backref='resource')
run_frequency = DB.Column(DB.Integer, default=60)
_auth = DB.Column('auth', DB.Text, nullable=True, default=None)

def __init__(self, owner, resource_type, title, url, tags):
def __init__(self, owner, resource_type, title, url, tags, auth=None):
self.resource_type = resource_type
self.active = True
self.title = title
self.url = url
self.owner = owner
self.tags = tags
self.auth = auth
self.auth_obj = None
self.latitude, self.longitude = util.geocode(url)

def __repr__(self):
@@ -543,6 +547,39 @@ def dump_recipients(self):
out[c] = self.get_recipients(c)
return out

@property
def auth(self):
return ResourceAuth.decode(self._auth)

@property
def auth_type(self):
if not self.has_auth():
return 'None'
return self.auth['type']

@auth.setter
def auth(self, auth_dict):
if auth_dict is None:
self._auth = None
return

self.auth_obj = ResourceAuth.create(auth_dict)
self._auth = self.auth_obj.encode()

def has_auth(self):
return self._auth is not None

def add_auth_header(self, headers_dict):
if 'Authorization' in headers_dict:
del headers_dict['Authorization']

if not self.has_auth():
return headers_dict

self.auth_obj = ResourceAuth.create(self.auth)

return self.auth_obj.add_auth_header(headers_dict)


class ResourceLock(DB.Model):
"""lock resource for multiprocessing runs"""
@@ -42,6 +42,9 @@ class Plugin(object):
def __init__(self):
self._parameters = {}

def get_class_name(self):
return type(self).__name__

def get_param(self, param_name):
"""
Get actual parameter value. `param_name` should be defined
@@ -1,5 +1,3 @@
import requests

from GeoHealthCheck.probe import Probe
from GeoHealthCheck.result import Result, push_result

@@ -8,6 +6,8 @@ class ESRIFSDrilldown(Probe):
"""
Probe for ESRI FeatureServer endpoint "drilldown": starting
with top /FeatureServer endpoint: get Layers and get Features on these.
Test e.g. from https://sampleserver6.arcgisonline.com/arcgis/rest/services
(at least sampleserver6 is ArcGIS 10.6.1 supporting Paging).
"""

NAME = 'ESRIFS Drilldown'
@@ -63,8 +63,7 @@ def perform_request(self):
result.start()
layers = []
try:

fs_caps = requests.get(req_tpl['fs_caps']).json()
fs_caps = self.perform_get_request(req_tpl['fs_caps']).json()
for attr in ['currentVersion', 'layers']:
val = fs_caps.get(attr, None)
if val is None:
@@ -93,7 +92,7 @@ def perform_request(self):
layer_ids.append(layer['id'])

for layer_id in layer_ids:
layer_caps.append(requests.get(
layer_caps.append(self.perform_get_request(
req_tpl['layer_caps'] % layer_id).json())

except Exception as err:
@@ -115,7 +114,7 @@ def perform_request(self):
for layer_id in layer_ids:

try:
features = requests.get(
features = self.perform_get_request(
req_tpl['get_features'] % layer_id).json()
obj_id_field_name = features['objectIdFieldName']
features = features['features']
@@ -124,9 +123,10 @@ def perform_request(self):

# At least one Feature: use first and try to get by id
object_id = features[0]['attributes'][obj_id_field_name]
feature = requests.get(req_tpl['get_feature_by_id']
% (layer_id, obj_id_field_name,
str(object_id))).json()
feature = self.perform_get_request(
req_tpl['get_feature_by_id'] % (
layer_id, obj_id_field_name,
str(object_id))).json()

feature = feature['features']
if len(feature) == 0:
@@ -46,7 +46,8 @@ def perform_request(self):
result = Result(True, 'Test Capabilities')
result.start()
try:
wms = WebMapService(self._resource.url)
wms = WebMapService(self._resource.url,
headers=self.get_request_headers())
title = wms.identification.title
self.log('response: title=%s' % title)
except Exception as err:
No changes.
@@ -0,0 +1,164 @@
import base64

from GeoHealthCheck.resourceauth import ResourceAuth


class NoAuth(ResourceAuth):
"""
Checks if header exists and has given header value.
See http://docs.python-requests.org/en/master/user/quickstart
"""

NAME = 'None'
DESCRIPTION = 'Default class for no auth'

PARAM_DEFS = {}
"""Param defs"""

def __init__(self):
ResourceAuth.__init__(self)

def verify(self):
return False

def encode(self):
return None


class BasicAuth(ResourceAuth):
"""
Basic authentication.
"""

NAME = 'Basic'
DESCRIPTION = 'Default class for no auth'

PARAM_DEFS = {
'username': {
'type': 'string',
'description': 'Username',
'default': None,
'required': True,
'range': None
},
'password': {
'type': 'password',
'description': 'Password',
'default': None,
'required': True,
'range': None
}
}
"""Param defs"""

def __init__(self):
ResourceAuth.__init__(self)

def verify(self):
if self.auth_dict is None:
return False

if 'data' not in self.auth_dict:
return False

auth_data = self.auth_dict['data']

if auth_data.get('username', None) is None:
return False

if len(auth_data.get('username', '')) == 0:
return False

if auth_data.get('password', None) is None:
return False

if len(auth_data.get('password', '')) == 0:
return False

return True

def encode_auth_header_val(self):
"""
Get encoded authorization header value from config data.
Authorization scheme-specific.
{
'type': 'Basic',
'data': {
'username': 'the_user',
'password': 'the_password'
}
}
:return: None or http Basic auth header value
"""

# Has auth, encode as HTTP header value
# Basic auth:
# http://mozgovipc.blogspot.nl/2012/06/
# python-http-basic-authentication-with.html
# base64 encode username and password
# write the Authorization header
# like: 'Basic base64encode(username + ':' + password)
auth_creds = self.auth_dict['data']
auth_val = base64.encodestring(
'%s:%s' % (auth_creds['username'], auth_creds['password']))
auth_val = "Basic %s" % auth_val
return auth_val


class BearerTokenAuth(ResourceAuth):
"""
Bearer token auth
"""

NAME = 'Bearer Token'
DESCRIPTION = 'Bearer token auth'

PARAM_DEFS = {
'token': {
'type': 'password',
'description': 'Token string',
'default': None,
'required': True,
'range': None
}
}
"""Param defs"""

def __init__(self):
ResourceAuth.__init__(self)

def verify(self):
if self.auth_dict is None:
return False

if 'data' not in self.auth_dict:
return False

auth_data = self.auth_dict['data']

if auth_data.get('token', None) is None:
return False

if len(auth_data.get('token', '')) == 0:
return False

return True

def encode_auth_header_val(self):
"""
Get encoded authorization header value from config data.
Authorization scheme-specific.
{
'type': 'Bearer Token',
'data': {
'token': 'the_token'
}
}
:return: None or http auth header value
"""

# Bearer Type, see eg. https://tools.ietf.org/html/rfc6750
return "Bearer %s" % self.auth_dict['data']['token']

0 comments on commit 3abff6a

Please sign in to comment.
You can’t perform that action at this time.