mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-28 19:55:20 +08:00
add in api auth system; update api docs
This commit is contained in:
@@ -13,7 +13,7 @@ app = Flask(__name__)
|
||||
app.config.from_object('api.settings')
|
||||
|
||||
# Import functions
|
||||
import errors, decorators, views
|
||||
import errors, views
|
||||
|
||||
# Add in blueprints
|
||||
from .docs import docs
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
v1auth = Blueprint('v1auth', __name__, url_prefix='/v1')
|
||||
v1auth = Blueprint('v1auth', __name__, url_prefix='')
|
||||
|
||||
import views
|
||||
import views
|
||||
|
||||
from authentication import auth_required
|
||||
|
||||
42
api/auth/authentication.py
Normal file
42
api/auth/authentication.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from hashlib import sha256
|
||||
from functools import wraps, update_wrapper
|
||||
from werkzeug.datastructures import Authorization
|
||||
from flask import g, request
|
||||
|
||||
from .models import User
|
||||
from ..errors import APIError
|
||||
|
||||
def authenticate_user(app_id, app_secret):
|
||||
app_secret_hash = sha256(app_secret).hexdigest()
|
||||
users = User.objects(app_id=app_id, app_secret=app_secret)
|
||||
if users and len(users) == 1:
|
||||
user = users[0]
|
||||
user.request_count = user.request_count + 1
|
||||
try:
|
||||
user.save()
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def auth_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
app_id = request.values.get('app_id')
|
||||
app_secret = request.values.get('app_secret')
|
||||
|
||||
if request.authorization:
|
||||
auth = request.authorization
|
||||
app_id = request.authorization.username
|
||||
app_secret = request.authorization.password
|
||||
elif app_id and app_secet:
|
||||
auth = Authorization('basic', data={'username': app_id, 'password': app_secret})
|
||||
else:
|
||||
raise APIError('API credentials missing', status_code=400)
|
||||
|
||||
if not authenticate_user(app_id, app_secret):
|
||||
raise APIError('Invalid API credentials', status_code=400)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
24
api/auth/models.py
Normal file
24
api/auth/models.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Onename API
|
||||
Copyright 2014 Halfmoon Labs, Inc.
|
||||
~~~~~
|
||||
"""
|
||||
|
||||
import json, datetime, binascii
|
||||
from utilitybelt import dev_random_entropy, dev_urandom_entropy
|
||||
|
||||
from utils import generate_app_id
|
||||
from ..db import db
|
||||
|
||||
class User(db.Document):
|
||||
# metadata
|
||||
created_at = db.DateTimeField(default=datetime.datetime.now, required=True)
|
||||
# account/auth data
|
||||
email = db.StringField(max_length=255, unique=True, required=True)
|
||||
# api keys
|
||||
app_id = db.StringField(default=generate_app_id(), max_length=255, unique=True, required=True)
|
||||
app_secret = db.StringField(max_length=255, unique=True)
|
||||
app_secret_hash = db.StringField(max_length=255, unique=True, required=True)
|
||||
request_count = db.IntField(min_value=0, default=0)
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Onename API
|
||||
Copyright 2014 Halfmoon Labs, Inc.
|
||||
~~~~~
|
||||
"""
|
||||
|
||||
import time, datetime
|
||||
from hashlib import md5
|
||||
|
||||
from ..db import db
|
||||
|
||||
nodes = db.nodes
|
||||
|
||||
MAX_QUOTA = 25
|
||||
|
||||
#---------------------
|
||||
#Account Creation
|
||||
#---------------------
|
||||
def save_user(username, account_type):
|
||||
|
||||
""" used for temporary Token generation. (to be replaced)
|
||||
create a new user (developer) given a username and account_type
|
||||
"""
|
||||
|
||||
user = {}
|
||||
user['username'] = username
|
||||
user['account_type'] = account_type
|
||||
user['access_token'] = generate_token(username)
|
||||
user['api_quota'] = MAX_QUOTA
|
||||
user['last_call'] = datetime.datetime.now()
|
||||
|
||||
nodes.save(user)
|
||||
|
||||
return user['access_token']
|
||||
#-------------------------------------------------------------
|
||||
def generate_token(username):
|
||||
"""Receives username/email as input and generate md5 hash key of the input"""
|
||||
|
||||
hash_input = username + str(time.time())
|
||||
access_token = md5(hash_input).hexdigest()
|
||||
|
||||
return access_token
|
||||
|
||||
#----------------------------------------------
|
||||
def validate_token(access_token):
|
||||
"""Checks if a 'key' is valid"""
|
||||
|
||||
if access_token == None:
|
||||
return False
|
||||
|
||||
return nodes.find({'access_token' : access_token}).limit(1).count()
|
||||
|
||||
#---------------------
|
||||
#Token Validation
|
||||
#---------------------
|
||||
def initialize_quota(username):
|
||||
"""Initialize quota for a specific username to some starting value such as 1000 """
|
||||
|
||||
user = nodes.find_one({'username' : username})
|
||||
|
||||
user['api_quota'] = MAX_QUOTA
|
||||
nodes.save(user)
|
||||
|
||||
#--------------------------------------
|
||||
def decrement_quota(access_token):
|
||||
"""returns False if quota associated with 'username' has expired. other
|
||||
otherwise decrements quota"""
|
||||
|
||||
user = nodes.find_one({'access_token' : access_token})
|
||||
|
||||
if user['api_quota'] < 1:
|
||||
return False
|
||||
|
||||
else:
|
||||
user = nodes.find_one({'access_token' : access_token})
|
||||
|
||||
time_now = datetime.datetime.now()
|
||||
|
||||
difference = time_now - user['last_call']
|
||||
|
||||
difference = divmod(difference.days * 86400 + difference.seconds, 60) #format: 0 minutes, 8 seconds
|
||||
|
||||
#reset if 15mins have passed since the last call
|
||||
if difference[0] > 14:
|
||||
reset_quota(user)
|
||||
|
||||
user['last_call'] = datetime.datetime.now()
|
||||
user['api_quota'] = user['api_quota'] - 1
|
||||
|
||||
nodes.save(user)
|
||||
|
||||
return True
|
||||
|
||||
#--------------------------------------
|
||||
def reset_quota(user):
|
||||
"""Reset (initialize) quota for all the users"""
|
||||
|
||||
user['api_quota'] = MAX_QUOTA
|
||||
|
||||
nodes.save(user)
|
||||
27
api/auth/registration.py
Normal file
27
api/auth/registration.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import traceback
|
||||
from hashlib import sha256
|
||||
from flask import render_template
|
||||
|
||||
from .models import User
|
||||
from .utils import generate_app_secret
|
||||
from ..email import send_w_mailgun
|
||||
from ..errors import APIError
|
||||
|
||||
def register_user(email):
|
||||
app_secret = generate_app_secret()
|
||||
app_secret_hash = sha256(app_secret).hexdigest()
|
||||
user = User(email=email, app_secret=app_secret, app_secret_hash=app_secret_hash)
|
||||
try:
|
||||
user.save()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise APIError('Could not register user')
|
||||
|
||||
template = render_template('email/registration.html',
|
||||
user=user, app_secret=app_secret)
|
||||
subject = 'Your Onename API Credentials'
|
||||
send_w_mailgun(subject, user.email.encode('utf8'), template)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
8
api/auth/utils.py
Normal file
8
api/auth/utils.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from binascii import hexlify
|
||||
from utilitybelt import dev_urandom_entropy
|
||||
|
||||
def generate_app_id():
|
||||
return hexlify(dev_urandom_entropy(16))
|
||||
|
||||
def generate_app_secret():
|
||||
return hexlify(dev_urandom_entropy(16))
|
||||
@@ -1,15 +1,37 @@
|
||||
from flask import request, jsonify
|
||||
|
||||
from flask import request, jsonify, render_template, redirect, url_for
|
||||
|
||||
from . import v1auth
|
||||
from .rate_limit import save_user
|
||||
from .core import register_user
|
||||
from ..decorators import parameters_required
|
||||
from ..errors import APIError
|
||||
|
||||
"""
|
||||
@v1auth.route('/gen_developer_key/', methods=['GET'])
|
||||
@parameters_required(parameters=['developer_id'])
|
||||
def create_account():
|
||||
""" creates a new dev. account
|
||||
"""
|
||||
|
||||
access_token = save_user(request.values['developer_id'], 'basic')
|
||||
|
||||
return jsonify({'developer_id': request.values['developer_id'],
|
||||
'access_token': access_token}), 200
|
||||
"""
|
||||
|
||||
@v1auth.route('/registered')
|
||||
def registered():
|
||||
return render_template('registered.html')
|
||||
|
||||
@v1auth.route('/signup', methods=['GET', 'POST'])
|
||||
def signup():
|
||||
if request.method == 'POST':
|
||||
if request.form and 'email' in request.form:
|
||||
email = request.form['email']
|
||||
try:
|
||||
user = register_user(email)
|
||||
except APIError:
|
||||
return "user already exists"
|
||||
return redirect(url_for('v1auth.registered'))
|
||||
else:
|
||||
return "something went wrong"
|
||||
|
||||
return render_template('signup.html')
|
||||
16
api/db.py
16
api/db.py
@@ -4,12 +4,18 @@
|
||||
Copyright 2014 Halfmoon Labs, Inc.
|
||||
~~~~~
|
||||
"""
|
||||
import os
|
||||
from pymongo import MongoClient
|
||||
|
||||
from . import app
|
||||
|
||||
client = MongoClient(app.config['MONGODB_HOST'], app.config['MONGODB_PORT'])
|
||||
db = client[app.config['MONGODB_DB']]
|
||||
#from pymongo import MongoClient
|
||||
#client = MongoClient(app.config['MONGODB_HOST'], app.config['MONGODB_PORT'])
|
||||
#db = client[app.config['MONGODB_DB']]
|
||||
#db.authenticate(app.config['MONGODB_USERNAME'], app.config['MONGODB_PASSWORD'])
|
||||
|
||||
db.authenticate(app.config['MONGODB_USERNAME'], app.config['MONGODB_PASSWORD'])
|
||||
# MongoDB + MongoEngine
|
||||
from mongoengine import connect
|
||||
from flask.ext.mongoengine import MongoEngine
|
||||
|
||||
if 'MONGODB_URI' in app.config:
|
||||
connect(app.config['MONGODB_DB'], host=app.config['MONGODB_URI'])
|
||||
db = MongoEngine(app)
|
||||
|
||||
@@ -11,7 +11,6 @@ from werkzeug.datastructures import Authorization
|
||||
from flask import g, request
|
||||
|
||||
from . import app
|
||||
from .rate_limit import validate_token, decrement_quota
|
||||
from .errors import APIError
|
||||
|
||||
"""
|
||||
@@ -28,29 +27,6 @@ def per_request_callbacks(response):
|
||||
return response
|
||||
"""
|
||||
|
||||
def access_token_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
demo_tokens = ['demo-1234']
|
||||
token = request.values.get('token')
|
||||
if request.authorization:
|
||||
auth = request.authorization
|
||||
token = request.authorization.username
|
||||
elif token:
|
||||
auth = Authorization('basic', data={'username':token, 'password':''})
|
||||
else:
|
||||
raise APIError('Access token missing', status_code=400)
|
||||
|
||||
if token not in demo_tokens:
|
||||
if not validate_token(token):
|
||||
raise APIError('Invalid token', status_code=400)
|
||||
|
||||
if not decrement_quota(token):
|
||||
raise APIError('Quota exceeded', status_code=401)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def parameters_required(parameters):
|
||||
def decorator(f):
|
||||
def decorated_function(*args, **kwargs):
|
||||
|
||||
15
api/email.py
Normal file
15
api/email.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import requests
|
||||
|
||||
from . import app
|
||||
|
||||
def send_w_mailgun(subject, recipient, template):
|
||||
return requests.post(
|
||||
"https://api.mailgun.net/v2/onename.io/messages",
|
||||
auth=("api", app.config['MAILGUN_API_KEY']),
|
||||
data={
|
||||
"from": app.config['MAIL_USERNAME'],
|
||||
"to": recipient,
|
||||
"subject": subject,
|
||||
"html": template
|
||||
}
|
||||
)
|
||||
@@ -7,10 +7,10 @@ from .samples import ryanshea
|
||||
from ..errors import APIError, ProfileNotFoundError, BadProfileError, \
|
||||
UsernameTakenError
|
||||
from ..crossdomain import crossdomain
|
||||
from ..decorators import access_token_required
|
||||
from ..auth import auth_required
|
||||
|
||||
@v1profile.route('/openname/<username>')
|
||||
@access_token_required
|
||||
@v1profile.route('/users/<username>')
|
||||
@auth_required
|
||||
@crossdomain(origin='*')
|
||||
def api_user(username):
|
||||
if username == 'ryanshea-example':
|
||||
|
||||
@@ -4,15 +4,18 @@ from flask import jsonify, request
|
||||
from . import v1proofs
|
||||
from .proofs import profile_to_verifications
|
||||
from ..errors import APIError
|
||||
from ..decorators import parameters_required, access_token_required
|
||||
from ..decorators import parameters_required
|
||||
from ..crossdomain import crossdomain
|
||||
from ..profile import get_blockchain_profile
|
||||
from ..auth import auth_required
|
||||
|
||||
@v1proofs.route('/verifications', methods=['POST'])
|
||||
@parameters_required(parameters=["openname"])
|
||||
@v1proofs.route('/users/<openname>/verifications')
|
||||
@auth_required
|
||||
@crossdomain(origin='*')
|
||||
def verify_profile():
|
||||
if not (request.data or request.form):
|
||||
def verify_profile(openname):
|
||||
profile = get_blockchain_profile(openname)
|
||||
|
||||
"""if not (request.data or request.form):
|
||||
raise APIError('A payload must be included', status_code=400)
|
||||
|
||||
if request.data:
|
||||
@@ -32,6 +35,7 @@ def verify_profile():
|
||||
profile = data["profile"]
|
||||
else:
|
||||
profile = get_blockchain_profile(openname)
|
||||
"""
|
||||
|
||||
verifications = profile_to_verifications(profile, openname)
|
||||
|
||||
|
||||
@@ -11,11 +11,12 @@ from flask import render_template, send_from_directory, Response, url_for, \
|
||||
|
||||
from . import v1search
|
||||
from ..errors import APIError
|
||||
from ..decorators import access_token_required, parameters_required
|
||||
from ..decorators import parameters_required
|
||||
from ..crossdomain import crossdomain
|
||||
from ..auth import auth_required
|
||||
|
||||
@v1search.route('/search', methods=['GET'])
|
||||
@access_token_required
|
||||
@auth_required
|
||||
@parameters_required(parameters=['query'])
|
||||
@crossdomain(origin='*')
|
||||
def search_people():
|
||||
|
||||
@@ -40,3 +40,5 @@ else:
|
||||
from .secrets import *
|
||||
|
||||
MONGODB_URI = 'mongodb://' + MONGODB_USERNAME + ':' + MONGODB_PASSWORD + '@' + MONGODB_HOST + ':' + str(MONGODB_PORT) + '/' + MONGODB_DB
|
||||
|
||||
MAIL_USERNAME = 'support@onename.io'
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<meta property="og:site_name" content='{{ site_name }}'/>
|
||||
<meta property="og:description" content=""/>
|
||||
|
||||
<link href="{{ url_for('static', filename='css/bootstrap.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/docs.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- SUPPORT FOR IE6-8 OF HTML5 ELEMENTS -->
|
||||
@@ -36,7 +37,7 @@
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar navbar-default navbar-subtle">
|
||||
<div class="navbar navbar-fixed-top navbar-default navbar-subtle">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
@@ -50,6 +51,9 @@
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a href="/signup">Sign up</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs">Docs</a>
|
||||
</li>
|
||||
@@ -58,8 +62,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 60px;">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
{% block content %}
|
||||
<div ng-app="docApp">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="row" style="padding-bottom: 60px;">
|
||||
<div class="col-md-3">
|
||||
<div class="bs-docs-sidebar affix" style="margin-top: 60px;">
|
||||
<div class="list-group">
|
||||
<a href="/docs" class="list-group-item" ng-class="{active: activetab == '/docs'}">
|
||||
Getting Started
|
||||
@@ -11,8 +12,8 @@
|
||||
<!--<a href="/docs/auth" class="list-group-item" ng-class="{active: activetab == '/docs/auth'}">
|
||||
Authentication
|
||||
</a>-->
|
||||
<a href="/docs/opennames" class="list-group-item" ng-class="{active: activetab == '/docs/opennames'}">
|
||||
Openname Lookups
|
||||
<a href="/docs/profiles" class="list-group-item" ng-class="{active: activetab == '/docs/profiles'}">
|
||||
Profiles
|
||||
</a>
|
||||
<a href="/docs/verifications" class="list-group-item" ng-class="{active: activetab == '/docs/verifications'}">
|
||||
Verifications
|
||||
@@ -21,6 +22,7 @@
|
||||
Search
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div ng-view=""></div>
|
||||
|
||||
17
api/templates/email/registration.html
Normal file
17
api/templates/email/registration.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<p>
|
||||
Hi there,
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Thank you for using the Onename API.
|
||||
</p>
|
||||
<p>
|
||||
Your App ID: {{ user.app_id }}
|
||||
</p>
|
||||
<p>
|
||||
Your App Secret: {{ app_secret }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Cheers,<br>The Onename Team
|
||||
</p>
|
||||
17
api/templates/registered.html
Normal file
17
api/templates/registered.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
|
||||
<h3>Thanks for signing up!</h3>
|
||||
|
||||
<p>
|
||||
Your API credentials have been sent to you. Check your email to start using the API.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
26
api/templates/signup.html
Normal file
26
api/templates/signup.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
|
||||
<h3>Sign Up for an API Key</h3>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<form role="form" method="post">
|
||||
<div class="form-group">
|
||||
<input type="email" class="form-control" id="exampleInputEmail1" placeholder="Email" name="email">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import redirect, url_for
|
||||
from flask import redirect, url_for, render_template, request
|
||||
|
||||
from . import app
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
Flask==0.10.1
|
||||
Flask-WTF==0.10.2
|
||||
Jinja2==2.7.3
|
||||
MarkupSafe==0.23
|
||||
WTForms==2.0.1
|
||||
Werkzeug==0.9.6
|
||||
beautifulsoup4==4.3.2
|
||||
commontools==0.1.0
|
||||
flask-mongoengine==0.7.1
|
||||
gunicorn==19.1.1
|
||||
itsdangerous==0.24
|
||||
mongoengine==0.8.7
|
||||
pymongo==2.7.2
|
||||
requests==2.3.0
|
||||
utilitybelt==0.1.6
|
||||
wsgiref==0.1.2
|
||||
|
||||
Reference in New Issue
Block a user