Skip to content
This repository has been archived by the owner on Oct 10, 2019. It is now read-only.

Commit

Permalink
Merge pull request #41 from automationator/master
Browse files Browse the repository at this point in the history
Reverts JWT changes and goes back to API keys because reasons
  • Loading branch information
automationator committed Mar 11, 2019
2 parents ebdee7d + 5e20f76 commit a72a08d
Show file tree
Hide file tree
Showing 39 changed files with 1,870 additions and 865 deletions.
2 changes: 2 additions & 0 deletions services/web/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,9 @@ def seeddb():
admin_role = models.Role.query.filter_by(name='admin').first()
user_datastore.create_user(email='admin@localhost', password=hash_password(password), username='admin', first_name='Admin', last_name='Admin', roles=[admin_role, analyst_role])
db.session.commit()
admin = models.User.query.filter_by(username='admin').first()
app.logger.info('SETUP: Created admin user with password: {}'.format(password))
app.logger.info('SETUP: Created admin user with API key: {}'.format(admin.apikey))


if __name__ == '__main__':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""empty message
Revision ID: 5f6949ea172b
Revision ID: fc9854dd0bc0
Revises:
Create Date: 2019-03-07 10:20:12.729381
Create Date: 2019-03-11 16:37:10.472196
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '5f6949ea172b'
revision = 'fc9854dd0bc0'
down_revision = None
branch_labels = None
depends_on = None
Expand Down Expand Up @@ -71,6 +71,7 @@ def upgrade():
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('active', sa.Boolean(), nullable=False),
sa.Column('apikey', sa.String(length=36), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('first_name', sa.String(length=50), nullable=False),
sa.Column('last_name', sa.String(length=50), nullable=False),
Expand All @@ -80,6 +81,7 @@ def upgrade():
sa.UniqueConstraint('email'),
sa.UniqueConstraint('username')
)
op.create_index(op.f('ix_user_apikey'), 'user', ['apikey'], unique=True)
op.create_table('campaign_alias',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('alias', sa.String(length=255), nullable=False),
Expand Down Expand Up @@ -171,6 +173,7 @@ def downgrade():
op.drop_table('intel_reference')
op.drop_table('indicator')
op.drop_table('campaign_alias')
op.drop_index(op.f('ix_user_apikey'), table_name='user')
op.drop_table('user')
op.drop_table('tag')
op.drop_table('role')
Expand Down
9 changes: 0 additions & 9 deletions services/web/project/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from flask import Flask, url_for
from flask_admin import Admin
from flask_admin import helpers as admin_helpers
from flask_jwt_extended import JWTManager
from flask_migrate import Migrate
from flask_security import Security, SQLAlchemyUserDatastore
from flask_sqlalchemy import SQLAlchemy
Expand All @@ -14,7 +13,6 @@

admin = Admin(name='SIP', url='/SIP')
db = SQLAlchemy()
jwt = JWTManager()
migrate = Migrate()
security = Security()

Expand Down Expand Up @@ -82,20 +80,13 @@ def create_app():
# Flask-Migrate
migrate.init_app(app, db)

# Flask-JWT-Extended
jwt.init_app(app)

# Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, models.User, models.Role)
security_ctx = security.init_app(app, datastore=user_datastore, login_form=ExtendedLoginForm)

# Flask-Admin
admin.init_app(app)

# Auth Blueprint
from project.auth import bp as auth_bp
app.register_blueprint(auth_bp)

# GUI/Admin Blueprint
from project.gui import bp as gui_bp
app.register_blueprint(gui_bp)
Expand Down
78 changes: 60 additions & 18 deletions services/web/project/api/decorators.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import gzip

from flask import current_app, request, after_this_request
from flask_jwt_extended import verify_jwt_in_request, verify_fresh_jwt_in_request, get_jwt_claims
from functools import wraps
from jsonschema import validate
from jsonschema.exceptions import SchemaError, ValidationError
from werkzeug.exceptions import BadRequest

from project import db
from project.api.errors import error_response
from project.models import User


def gzipped_response(function):
Expand Down Expand Up @@ -35,9 +36,9 @@ def zipper(response):
return decorated_function


def check_if_token_required(function):
""" If the called HTTP method exists in the config file, it uses the value as the
user role required of the JWT token to perform the function. """
def check_apikey(function):
""" Checks if the HTTP method exists in the app's config.
If it does, it uses the value as the user role required to perform the function. """

@wraps(function)
def decorated_function(*args, **kwargs):
Expand All @@ -53,30 +54,71 @@ def decorated_function(*args, **kwargs):
if not required_role:
return function(*args, **kwargs)

# Verify the JWT exists and that it has the required role.
verify_jwt_in_request()
claims = get_jwt_claims()
if required_role in claims['roles']:
return function(*args, **kwargs)
# Get the API key if there is one.
# The header should look like:
# Authorization: Apikey blah-blah-blah
# So strip off the first 7 characters to get the actual key.
authorization = request.headers.get('Authorization')
apikey = None
if authorization and 'apikey' in authorization.lower():
apikey = authorization[7:]

# If there is an API key, look it up and get the user's roles.
if apikey:
user = db.session.query(User).filter_by(apikey=apikey).first()

# If the user exists and they have the required role, return the function.
if user:
if user.active:
if any(role.name.lower() == required_role for role in user.roles):
return function(*args, **kwargs)
else:
return error_response(401, 'Insufficient privileges')
else:
return error_response(401, 'API user is not active')
else:
return error_response(401, 'API user does not exist')
else:
return error_response(401, '{} role required'.format(required_role))
return error_response(401, 'Bad or missing API key')

return decorated_function


def admin_required(function):
""" Verifies the JWT is present and that the user has the admin role """
def verify_admin(function):
""" Verifies that the calling user has the admin role. """

@wraps(function)
def decorated_function(*args, **kwargs):

# Verify the (fresh) JWT token exists and that it has the admin role.
verify_fresh_jwt_in_request()
claims = get_jwt_claims()
if 'admin' in claims['roles']:
return function(*args, **kwargs)
# Get the role of the function name in the config.
required_role = 'admin'

# Get the API key if there is one.
# The header should look like:
# Authorization: Apikey blah-blah-blah
# So strip off the first 7 characters to get the actual key.
authorization = request.headers.get('Authorization')
apikey = None
if authorization and 'apikey' in authorization.lower():
apikey = authorization[7:]

# If there is an API key, look it up and get the user's roles.
if apikey:
user = db.session.query(User).filter_by(apikey=apikey).first()

# If the user exists and they have the required role, return the function.
if user:
if user.active:
if any(role.name.lower() == required_role for role in user.roles):
return function(*args, **kwargs)
else:
return error_response(401, 'Insufficient privileges')
else:
return error_response(401, 'API user is not active')
else:
return error_response(401, 'API user does not exist')
else:
return error_response(401, 'admin role required')
return error_response(401, 'Bad or missing API key')

return decorated_function

Expand Down
22 changes: 11 additions & 11 deletions services/web/project/api/routes/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from project import db
from project.api import bp
from project.api.decorators import check_if_token_required, validate_json, validate_schema
from project.api.decorators import check_apikey, validate_json, validate_schema
from project.api.errors import error_response
from project.api.schemas import campaign_create, campaign_update
from project.models import Campaign, CampaignAlias
Expand All @@ -14,7 +14,7 @@


@bp.route('/campaigns', methods=['POST'])
@check_if_token_required
@check_apikey
@validate_json
@validate_schema(campaign_create)
def create_campaign():
Expand Down Expand Up @@ -50,7 +50,7 @@ def create_campaign():
"name": "LOLcats"
}
:reqheader Authorization: Optional JWT Bearer token
:reqheader Authorization: Optional Apikey value
:resheader Content-Type: application/json
:status 201: Campaign created
:status 400: JSON does not match the schema
Expand Down Expand Up @@ -97,7 +97,7 @@ def create_campaign():


@bp.route('/campaigns/<int:campaign_id>', methods=['GET'])
@check_if_token_required
@check_apikey
def read_campaign(campaign_id):
""" Gets a single campaign given its ID.
Expand Down Expand Up @@ -126,7 +126,7 @@ def read_campaign(campaign_id):
"name": "LOLcats"
}
:reqheader Authorization: Optional JWT Bearer token
:reqheader Authorization: Optional Apikey value
:resheader Content-Type: application/json
:status 200: Campaign found
:status 401: Invalid role to perform this action
Expand All @@ -141,7 +141,7 @@ def read_campaign(campaign_id):


@bp.route('/campaigns', methods=['GET'])
@check_if_token_required
@check_apikey
def read_campaigns():
""" Gets a list of all the campaigns.
Expand Down Expand Up @@ -179,7 +179,7 @@ def read_campaigns():
}
]
:reqheader Authorization: Optional JWT Bearer token
:reqheader Authorization: Optional Apikey value
:resheader Content-Type: application/json
:status 200: Campaigns found
:status 401: Invalid role to perform this action
Expand All @@ -195,7 +195,7 @@ def read_campaigns():


@bp.route('/campaigns/<int:campaign_id>', methods=['PUT'])
@check_if_token_required
@check_apikey
@validate_json
@validate_schema(campaign_update)
def update_campaign(campaign_id):
Expand Down Expand Up @@ -230,7 +230,7 @@ def update_campaign(campaign_id):
"name": "Derpsters"
}
:reqheader Authorization: Optional JWT Bearer token
:reqheader Authorization: Optional Apikey value
:resheader Content-Type: application/json
:status 200: Campaign updated
:status 400: JSON does not match the schema
Expand Down Expand Up @@ -266,7 +266,7 @@ def update_campaign(campaign_id):


@bp.route('/campaigns/<int:campaign_id>', methods=['DELETE'])
@check_if_token_required
@check_apikey
def delete_campaign(campaign_id):
""" Deletes a campaign.
Expand All @@ -285,7 +285,7 @@ def delete_campaign(campaign_id):
HTTP/1.1 204 No Content
:reqheader Authorization: Optional JWT Bearer token
:reqheader Authorization: Optional Apikey value
:status 204: Campaign deleted
:status 401: Invalid role to perform this action
:status 404: Campaign ID not found
Expand Down

0 comments on commit a72a08d

Please sign in to comment.