Skip to content
Snippets Groups Projects
Commit eb07d2c4 authored by Megan Henning's avatar Megan Henning Committed by Nathaniel Kofalt
Browse files

Add job-based api keys

parent cc24d125
No related branches found
No related tags found
No related merge requests found
import bson
import datetime
from . import APIAuthProviderException
from .. import config, util
log = config.log
class APIKey(object):
"""
Abstract API key class
"""
@staticmethod
def _preprocess_key(key):
"""
Convention for API keys is that they can have arbitrary information, separated by a :,
before the actual key. Generally, this will have a connection string in it.
Strip this preamble, if any, before processing the key.
"""
return key.split(":")[-1] # Get the last segment of the string after any : separators
@staticmethod
def validate(key):
"""
AuthN for user accounts via api key.
401s via APIAuthProviderException on failure.
"""
key = APIKey._preprocess_key(key)
timestamp = datetime.datetime.utcnow()
api_key = config.db.apikeys.find_one_and_update({'_id': key}, {'$set': {'last_used': timestamp}})
if api_key:
# Some api keys may have additional requirements that must be met
try:
APIKeyTypes[api_key['type']].check(api_key)
except KeyError:
log.warning('Unknown API key type ({})'.format(api_key.get('type')))
APIAuthProviderException('Invalid API key')
return api_key
else:
raise APIAuthProviderException('Invalid API key')
@staticmethod
def generate_api_key(key_type):
return {
'_id': util.create_nonce(),
'created': datetime.datetime.utcnow(),
'type': key_type,
'last_used': None
}
class UserApiKey(APIKey):
key_type = 'user'
@classmethod
def generate(cls, uid):
"""
Generates API key for user, replaces existing API key if exists
"""
api_key = cls.generate_api_key(cls.key_type)
api_key['uid'] = uid
config.db.apikeys.delete_many({'uid': uid, 'type': cls.key_type})
config.db.apikeys.insert_one(api_key)
return api_key['_id']
@classmethod
def get(cls, uid):
return config.db.apikeys.find_one({'uid': uid, 'type': cls.key_type})
@classmethod
def check(cls, api_key):
pass
class JobApiKey(APIKey):
"""
API key that grants API access as a specified user during execution of a job
Job must be in 'running' state to user API key
"""
key_type = 'job'
@classmethod
def generate(cls, uid, job_id):
"""
Generates an API key for user for use by a specific job
"""
api_key = cls.generate_api_key(cls.key_type)
api_key['uid'] = uid
api_key['job'] = job_id
config.db.apikeys.insert_one(api_key)
return api_key['_id']
@classmethod
def remove(cls, job_id):
config.db.apikeys.delete({'type': cls.key_type, 'job': bson.ObjectId(job_id)})
@classmethod
def check(cls, api_key):
job_id = api_key['job']
if config.db.jobs.count({'_id': bson.ObjectId(job_id), 'state': 'running'}) != 1:
raise APIAuthProviderException('Use of API key requires job to be in progress')
APIKeyTypes = {
'user' : UserApiKey,
'job' : JobApiKey
}
......@@ -5,6 +5,7 @@ import urllib
import urlparse
from . import APIAuthProviderException, APIUnknownUserException, APIRefreshTokenException
from .apikeys import APIKey
from .. import config, util
from ..dao import dbutil
......@@ -12,9 +13,7 @@ log = config.log
class AuthProvider(object):
"""
This class provides access to mongodb collection elements (called containers).
It is used by ContainerHandler istances for get, create, update and delete operations on containers.
Examples: projects, sessions, acquisitions and collections
Abstract auth provider class
"""
def __init__(self, auth_type, set_config=True):
......@@ -297,42 +296,18 @@ class APIKeyAuthProvider(AuthProvider):
"""
super(APIKeyAuthProvider, self).__init__('api-key', set_config=False)
@staticmethod
def _preprocess_key(key):
"""
Convention for API keys is that they can have arbitrary information, separated by a :,
before the actual key. Generally, this will have a connection string in it.
Strip this preamble, if any, before processing the key.
"""
return key.split(":")[-1] # Get the last segment of the string after any : separators
def validate_code(self, code, **kwargs):
code = APIKeyAuthProvider._preprocess_key(code)
uid = self.validate_user_api_key(code)
api_key = APIKey.validate(code)
return {
'access_token': code,
'uid': uid,
'uid': api_key['uid'],
'auth_type': self.auth_type,
'expires': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
@staticmethod
def validate_user_api_key(key):
"""
AuthN for user accounts via api key.
401s via APIAuthProviderException on failure.
"""
key = APIKeyAuthProvider._preprocess_key(key)
timestamp = datetime.datetime.utcnow()
user = config.db.users.find_one_and_update({'api_key.key': key}, {'$set': {'api_key.last_used': timestamp}}, ['_id'])
if user:
return user['_id']
else:
raise APIAuthProviderException('Invalid API key')
AuthProviders = {
......
......@@ -8,6 +8,7 @@ from .. import util
from .. import config
from .. import validators
from ..auth import userauth, require_admin
from ..auth.apikeys import UserApiKey
from ..dao import containerstorage
from ..dao import noop, APIStorageException
......@@ -23,9 +24,9 @@ class UserHandler(base.RequestHandler):
def get(self, _id):
user = self._get_user(_id)
permchecker = userauth.default(self, user)
projection = {'api_key': 0}
projection = None
if not self.user_is_admin:
projection['wechat'] = 0
projection = {'wechat': 0}
result = permchecker(self.storage.exec_op)('GET', _id, projection=projection or None)
if result is None:
self.abort(404, 'User does not exist')
......@@ -38,6 +39,13 @@ class UserHandler(base.RequestHandler):
user = self.storage.exec_op('GET', self.uid)
if not user:
self.abort(403, 'user does not exist')
api_key = UserApiKey.get(self.uid)
if api_key:
user['api_key'] = {
'key': api_key['_id'],
'created': api_key['created'],
'last_used': api_key['last_used']
}
return user
def get_all(self):
......@@ -167,14 +175,8 @@ class UserHandler(base.RequestHandler):
def generate_api_key(self):
if not self.uid:
self.abort(400, 'no user is logged in')
generated_key = util.create_nonce()
now = datetime.datetime.utcnow()
payload = {'api_key': {'key': generated_key, 'created': now, 'last_used': None}}
result = self.storage.exec_op('PUT', _id=self.uid, payload=payload)
if result.modified_count == 1:
return {'key': generated_key}
else:
self.abort(500, 'New key for user {} not generated'.format(self.uid))
generated_key = UserApiKey.generate(self.uid)
return {'key': generated_key}
@require_admin
def reset_registration(self, uid):
......
......@@ -11,7 +11,8 @@ from .. import files
from .. import config
from ..types import Origin
from .. import validators
from ..auth.authproviders import AuthProvider, APIKeyAuthProvider
from ..auth.authproviders import AuthProvider
from ..auth.apikeys import APIKey
from ..auth import APIAuthProviderException, APIUnknownUserException, APIRefreshTokenException
from ..dao import APIConsistencyException, APIConflictException, APINotFoundException, APIPermissionException, APIValidationException
from elasticsearch import ElasticsearchException
......@@ -63,7 +64,7 @@ class RequestHandler(webapp2.RequestHandler):
if session_token.startswith('scitran-user '):
# User (API key) authentication
key = session_token.split()[1]
self.uid = APIKeyAuthProvider.validate_user_api_key(key)
self.uid = APIKey.validate(key)['uid']
elif session_token.startswith('scitran-drone '):
# Drone (API key) authentication
# When supported, remove custom headers and shared secret
......
......@@ -1220,6 +1220,32 @@ def upgrade_to_37():
process_cursor(cursor, upgrade_to_32_closure, context = coll)
def upgrade_to_37_closure(user):
api_key = user['api_key']
new_api_key_doc = {
'_id': api_key['key'],
'created': api_key['created'],
'last_used': api_key['last_used'],
'uid': user['_id'],
'type': 'user'
}
config.db.apikeys.insert(new_api_key_doc)
config.db.users.update_one({'_id': user['_id']}, {'$unset': {'api_key': 0}})
return True
def upgrade_to_37():
"""
Move existing user api keys to new 'apikeys' collection
"""
cursor = config.db.users.find({'api_key': {'$exists': True }})
process_cursor(cursor, upgrade_to_36_closure)
###
### BEGIN RESERVED UPGRADE SECTION
###
......
......@@ -36,15 +36,13 @@ def main():
'root': True,
})
api_db = pymongo.MongoClient(SCITRAN_PERSISTENT_DB_URI).get_default_database()
api_db.users.update_one(
{'_id': abao_user},
{'$set': {
'api_key': {
'key': abao_api_key,
'created': datetime.datetime.utcnow()
}
}}
)
api_db.apikeys.insert_one({
'_id': abao_api_key,
'created': datetime.datetime.utcnow(),
'last_seen': None,
'type': 'user',
'uid': abao_user
})
as_root = BaseUrlSession()
as_root.headers.update({'Authorization': 'scitran-user {}'.format(abao_api_key)})
......
......@@ -297,15 +297,14 @@ class DataBuilder(object):
# inject api key if it was provided
if resource == 'user' and user_api_key:
_api_db.users.update_one(
{'_id': _id},
{'$set': {
'api_key': {
'key': user_api_key,
'created': datetime.datetime.utcnow()
}
}}
)
_api_db.apikeys.insert_one({
'_id': user_api_key,
'created': datetime.datetime.utcnow(),
'last_seen': None,
'type': 'user',
'uid': _id
})
self.resources.append((resource, _id))
return _id
......
import pytest
from api.auth.authproviders import APIKeyAuthProvider
from api.auth.apikeys import APIKey
def test_api_key_preprocess():
assert APIKeyAuthProvider._preprocess_key("key") == "key"
assert APIKeyAuthProvider._preprocess_key("preamble:key") == "key"
assert APIKeyAuthProvider._preprocess_key("preamble:37:key") == "key"
assert APIKeyAuthProvider._preprocess_key("preamble::key") == "key"
assert APIKey._preprocess_key("key") == "key"
assert APIKey._preprocess_key("preamble:key") == "key"
assert APIKey._preprocess_key("preamble:37:key") == "key"
assert APIKey._preprocess_key("preamble::key") == "key"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment