Commit 003982a2 authored by Sabino's avatar Sabino
parents 5fb7f040 34e860c5
......@@ -7,3 +7,4 @@ humanfriendly
psycopg2>=2.6.1
pyzmq>=15.2.0
typing
python-oauth2
......@@ -24,6 +24,7 @@ from zoe_api.rest_api.info import InfoAPI
from zoe_api.rest_api.service import ServiceAPI, ServiceLogsAPI
from zoe_api.rest_api.discovery import DiscoveryAPI
from zoe_api.rest_api.statistics import SchedulerStatsAPI
from zoe_api.rest_api.oauth import OAuthGetAPI, OAuthRevokeAPI
from zoe_lib.version import ZOE_API_VERSION
......@@ -48,7 +49,10 @@ def api_init(api_endpoint) -> List[tornado.web.URLSpec]:
tornado.web.url(API_PATH + r'/discovery/by_group/([0-9]+)/([a-z0-9A-Z\-]+)', DiscoveryAPI, route_args),
tornado.web.url(API_PATH + r'/statistics/scheduler', SchedulerStatsAPI, route_args)
tornado.web.url(API_PATH + r'/statistics/scheduler', SchedulerStatsAPI, route_args),
tornado.web.url(API_PATH + r'/oauth/token', OAuthGetAPI, route_args),
tornado.web.url(API_PATH + r'/oauth/revoke/([a-z0-9A-Z\-]+)', OAuthRevokeAPI, route_args)
]
return api_routes
# Copyright (c) 2016, Daniele Venzano
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The Execution API endpoints."""
from tornado.web import RequestHandler
import tornado.escape
import logging
import zoe_lib.config as config
from zoe_api.rest_api.utils import catch_exceptions, get_auth
from zoe_api.rest_api.oauth_utils import auth_controller, mongo, get_username_password
import zoe_api.exceptions
from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import
import oauth2.grant
import json
import requests
import pymongo
log = logging.getLogger(__name__)
"""
Example of using:
-Two kind of request token:
(1) with an access token and a refresh token
(2) with only an access token
*To request a new token of type:
(1):
Input: curl -u 'admin:admin' http://localhost:5001/api/0.6/oauth/token -X POST -H 'Content-Type: application/json' -d '{"client_id": "admin", "client_secret": "admin", "grant_type": "password", "username": "admin", "password": "admin", "scope": ""}'
Output: {"token_type": "Bearer", "access_token": "3ddbe9ba-6a21-4e4d-993b-70556390c5d3", "refresh_token": "9bab190f-e211-42aa-917e-20ce987e355e", "expires_in": 36000}
(2):
Input: curl -u 'admin:admin' http://localhost:5001/api/0.6/oauth/token -X POST -H 'Content-Type: application/json' -d '{"client_id": "admin", "client_secret": "admin", "grant_type": "client_credentials", "scope": ""}'
Output: {"token_type": "Bearer", "access_token": "e6ab7c66-777b-4b64-91e0-2f501d28fe6e", "expires_in": 3600}
*To refresh a token, only apply for type-1 request token
Input: curl -u 'admin:admin' http://localhost:5001/api/0.6/oauth/token -X POST -H 'Content-Type: application/json' -d '{"client_id": "admin", "client_secret": "admin", "grant_type": "refresh_token", "refresh_token": "9bab190f-e211-42aa-917e-20ce987e355e", "username": "admin", "password": "admin", "scope": ""}'
Output: {"token_type": "Bearer", "access_token": "378f8d5f-2eb5-4181-b632-ad23c4534d32", "expires_in": 36000}
*To revoke a token, apply for two types of token requested
curl -i --verbose -u 'admin:admin' -X DELETE http://localhost:5001/api/0.6/oauth/revoke/e6ab7c66-777b-4b64-91e0-2f501d28fe6e
*To authenticate with other rest api services, using a header with: "Authorization: Bearer access_token"
curl -H 'Authorization: Bearer 378f8d5f-2eb5-4181-b632-ad23c4534d32' http://localhost:5001/api/0.6/execution
"""
class OAuthGetAPI(RequestHandler):
"""The OAuthGetAPI endpoint."""
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
self.auth_controller = auth_controller
self.mongo = mongo
@catch_exceptions
def post(self):
"""REQUEST/REFRESH token"""
username, password = get_username_password(self)
uid, role = get_auth(self)
try:
self.mongo['db']['oauth_clients'].insert(
{'identifier': username,
'secret': password,
'redirect_uris': [],
'authorized_grants': [ oauth2.grant.RefreshToken.grant_type,
oauth2.grant.ResourceOwnerGrant.grant_type,
oauth2.grant.ClientCredentialsGrant.grant_type]})
except pymongo.errors.DuplicateKeyError as e:
log.warn("Already had this user in db, skip adding...")
response = self._dispatch_request()
self._map_response(response)
def _dispatch_request(self):
request = self.request
request.post_param = lambda key: json.loads(request.body.decode())[key]
return self.auth_controller.dispatch(request, environ={})
def _map_response(self, response):
for name, value in list(response.headers.items()):
self.set_header(name, value)
self.set_status(response.status_code)
if response.status_code == 200:
log.info("New token granted...")
self.write(response.body)
def data_received(self, chunk):
pass
class OAuthRevokeAPI(RequestHandler):
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
self.auth_controller = auth_controller
@catch_exceptions
def delete(self, token):
"""DELETE token (logout)"""
uid, role = get_auth(self)
key = 'oauth2_{}'.format(token)
res = self.auth_controller.access_token_store.rs.delete(key)
if res == 0:
raise zoe_api.exceptions.ZoeRestAPIException('No token found in database')
ret = {'res': 'Revoked token.'}
self.write(ret)
# Copyright (c) 2016, Quang-Nhat HOANG-XUAN
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions needed by the Zoe REST API."""
import base64
import logging
from zoe_lib.config import get_conf
from zoe_api.exceptions import ZoeRestAPIException
import tornado.web
import oauth2
import oauth2.tokengenerator
import oauth2.grant
import oauth2.store.redisdb
import oauth2.store.mongodb
import time
import json
import fakeredis
import mongomock
import hashlib
import os
import uuid
log = logging.getLogger(__name__)
class OAuthSiteAdapter(oauth2.web.ResourceOwnerGrantSiteAdapter):
def authenticate(self, request, environ, scopes, client):
return {}
class TokenGenerator(object):
"""
Base class of every token generator.
"""
def __init__(self):
"""
Create a new instance of a token generator.
"""
self.expires_in = {}
self.refresh_expires_in = 0
def create_access_token_data(self, grant_type):
"""
Create data needed by an access token.
:param grant_type:
:type grant_type: str
:return: A ``dict`` containing he ``access_token`` and the
``token_type``. If the value of ``TokenGenerator.expires_in``
is larger than 0, a ``refresh_token`` will be generated too.
:rtype: dict
"""
if grant_type == 'password':
self.expires_in['password'] = 36000
result = {"access_token": self.generate(), "token_type": "Bearer"}
if self.expires_in.get(grant_type, 0) > 0:
result["refresh_token"] = self.generate()
result["expires_in"] = self.expires_in[grant_type]
return result
def generate(self):
"""
Implemented by generators extending this base class.
:raises NotImplementedError:
"""
raise NotImplementedError
class URandomTokenGenerator(TokenGenerator):
"""
Create a token using ``os.urandom()``.
"""
def __init__(self, length=40):
self.token_length = length
TokenGenerator.__init__(self)
def generate(self):
"""
:return: A new token
:rtype: str
"""
random_data = os.urandom(100)
hash_gen = hashlib.new("sha512")
hash_gen.update(random_data)
return hash_gen.hexdigest()[:self.token_length]
class Uuid4(TokenGenerator):
"""
Generate a token using uuid4.
"""
def generate(self):
"""
:return: A new token
:rtype: str
"""
return str(uuid.uuid4())
def get_username_password(handler: tornado.web.RequestHandler):
"""Get username, password"""
auth_header = handler.request.headers.get('Authorization')
if auth_header is None or not (auth_header.startswith('Basic ') or auth_header.startswith('Bearer ')):
raise ZoeRestAPIException('missing or wrong authentication information', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
auth_decoded = base64.decodebytes(bytes(auth_header[6:], 'ascii')).decode('utf-8')
username, password = auth_decoded.split(':', 2)
return username, password
mongo = mongomock.MongoClient()
mongo['db']['oauth_clients'].ensure_index("identifier", unique=True)
client_store = oauth2.store.mongodb.ClientStore(mongo['db']['oauth_clients'])
token_store = oauth2.store.redisdb.TokenStore(rs=fakeredis.FakeStrictRedis())
token_generator = Uuid4()
token_generator.expires_in[oauth2.grant.ClientCredentialsGrant.grant_type] = 3600
auth_controller = oauth2.Provider(
access_token_store=token_store,
auth_code_store=token_store,
client_store=client_store,
token_generator=token_generator
)
site_adapter = OAuthSiteAdapter()
auth_controller.token_path = '/api/0.6/oauth/token'
auth_controller.add_grant(oauth2.grant.ClientCredentialsGrant())
auth_controller.add_grant(oauth2.grant.RefreshToken(expires_in=3600))
auth_controller.add_grant(oauth2.grant.ResourceOwnerGrant(site_adapter=site_adapter))
......@@ -28,9 +28,12 @@ from zoe_api.auth.ldap import LDAPAuthenticator
from zoe_api.auth.file import PlainTextAuthenticator
from zoe_api.auth.base import BaseAuthenticator # pylint: disable=unused-import
from zoe_api.rest_api.oauth_utils import auth_controller, mongo
log = logging.getLogger(__name__)
import json
import time
log = logging.getLogger(__name__)
def catch_exceptions(func):
"""
......@@ -69,11 +72,31 @@ def catch_exceptions(func):
def get_auth(handler: tornado.web.RequestHandler):
"""Try to authenticate a request."""
auth_header = handler.request.headers.get('Authorization')
if auth_header is None or not auth_header.startswith('Basic '):
if auth_header is None or not (auth_header.startswith('Basic ') or auth_header.startswith('Bearer ')):
raise ZoeRestAPIException('missing or wrong authentication information', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
auth_decoded = base64.decodebytes(bytes(auth_header[6:], 'ascii')).decode('utf-8')
username, password = auth_decoded.split(':', 2)
#Process for authentication with token
if "Bearer" in auth_header:
token = auth_header[7:]
key = 'oauth2_{}'.format(token)
access = auth_controller.access_token_store.rs.get(key)
if access:
access = json.loads(access.decode())
username = access['client_id']
passwords = mongo['db']['oauth_clients'].find({'identifier':username})
password = ''
for p in passwords:
password = p['secret']
else:
raise ZoeRestAPIException('Invalid Token', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
if access['expires_at'] <= int(time.time()):
raise ZoeRestAPIException('Expired token', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
#Process for authentication with username, password
else:
auth_decoded = base64.decodebytes(bytes(auth_header[6:], 'ascii')).decode('utf-8')
username, password = auth_decoded.split(':', 2)
if get_conf().auth_type == 'text':
authenticator = PlainTextAuthenticator() # type: BaseAuthenticator
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment