Skip to content

Commit

Permalink
Merge PR #3: Added userpass authentication method
Browse files Browse the repository at this point in the history
Added `userpass` authentication method
  • Loading branch information
bearlike authored Apr 7, 2022
2 parents ce10e09 + 062aaa9 commit 9914d65
Show file tree
Hide file tree
Showing 15 changed files with 261 additions and 46 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ jobs:
env:
IMG_NAME: ${{ 'krishnaalagiri/ssm' }}
# Versioning: MAJOR.MINOR.PATCH (eg., 1.2.3)
VERSION_FULL: ${{ '1.0.0' }}
VERSION_FULL: ${{ '1.1.0' }}
# For v1.2.3, VERSION_SHORT is '1.2'
VERSION_SHORT: ${{ '1.0' }}
VERSION_SHORT: ${{ '1.1' }}
# For v1.2.3, VERSION_MAJOR is '1'
VERSION_MAJOR: ${{ '1' }}
with:
Expand Down
28 changes: 28 additions & 0 deletions Access/is_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from Api.api import conn, api

# Auth Init
userpass = HTTPBasicAuth()
token = HTTPTokenAuth(scheme='Bearer')

# TODO: error_handler


@token.verify_token
def abort_if_authorization_fail(token):
""" Check if an API token is valid
Args:
token (str): API Token
"""
check, username = conn.tokens.is_authorized(token)
if check:
return username
api.abort(401, "Not Authorized to access the requested resource")


@userpass.verify_password
def verify_userpass(username, password):
if conn.userpass.is_authorized(username, password):
return username
api.abort(401, "Not Authorized to access the requested resource")
18 changes: 12 additions & 6 deletions Access/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, token_auth_col):
# * db.tokens.createIndex( { "token": 1 }, { unique: true } )
self._tokens = token_auth_col

def generate(self, max_ttl=15811200):
def generate(self, username, max_ttl=15811200):
""" Generates an API token
Args:
max_ttl (int, optional): Maximum TTL for generated token in seconds
Expand All @@ -29,18 +29,23 @@ def generate(self, max_ttl=15811200):
token = secrets.token_hex(32)
data = {
"token": token,
"owner": username,
"generated_on": Timestamp(int(dt.datetime.today().timestamp()), 1),
}
_ = self._tokens.insert_one(data)
status = dict(**{"token": token}, **{"status": "OK"})
return status

def revoke(self, token):
finder = self._tokens.find_one({"token": token})
def revoke(self, token, username):
data = {
"token": token,
"owner": username
}
finder = self._tokens.find_one(data)
if not finder:
result = {"status": "Path not found"}
else:
_ = self._tokens.delete_one({"token": token})
_ = self._tokens.delete_one(data)
result = {"status": "OK"}
return result

Expand All @@ -50,14 +55,15 @@ def is_authorized(self, token):
token (str): API token
Returns:
bool: True for valid tokens and False otherwise.
str: username if valid or None otherwise.
"""
finder = self._tokens.find_one({"token": token})
# Return False, if token is not found
if not finder:
return False
return False, None
# Return True, if token is found
# TODO: Check for Max TTL
return True
return True, finder["owner"]

def renew(self):
pass
69 changes: 69 additions & 0 deletions Access/userpass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
""" User-Pass authentication for Secrets Manager
"""
from bson.timestamp import Timestamp
import datetime as dt
from werkzeug.security import generate_password_hash, check_password_hash


class User_Pass:
def __init__(self, userpass_auth_col):
""" Userpass operations
Args:
userpass_auth_col (pymongo.collection.Collection)
"""
# * Create unique index on 'username' for secrets_manager_auth.userpass
# * db.userpass.createIndex( { "username": 1 }, { unique: true } )
self._userpass = userpass_auth_col

def register(self, username, password):
""" Register a new user
Args:
username (str): Username
password (str): Password
Returns:
dict : Dictionary with operation status
"""
finder = self._userpass.find_one({"username": username})
if not finder:
password = generate_password_hash(password, method='sha256')
data = {
"username": username,
"password": password,
"added_on": Timestamp(int(dt.datetime.today().timestamp()), 1),
}
_ = self._userpass.insert_one(data)
status = {"status": "OK"}
else:
status = {"status": "User already exist"}
return status

def remove(self, username):
""" Deletes an existing user
Args:
username (str): Username
Returns:
dict : Dictionary with operation status
"""
finder = self._userpass.find_one({"username": username})
if not finder:
result = {"status": "Username does not exist"}
else:
_ = self._userpass.delete_one({"username": username})
result = {"status": "OK"}
return result

def is_authorized(self, username, password):
""" Check if a userpass is valid
Args:
username (str): Username
password (str): Password
Returns:
bool: True for valid userpass and False otherwise.
"""
finder = self._userpass.find_one({"username": username})
# Return False, if username is not found
if not finder:
return False
# Return True, if userpass is valid
return check_password_hash(finder["password"], password)
23 changes: 11 additions & 12 deletions Api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,32 @@
from flask import Flask, Blueprint

authorizations = {
'apikey': {
"Token": {
'type': 'apiKey',
'in': 'header',
'name': 'X-API-KEY'
},
"UserPass": {
'type': 'basic'
}
}


conn = Connection()
api_v1 = Blueprint("api", __name__, url_prefix="/api")
api = Api(api_v1, version="1.0", title="Secrets Manager",
api = Api(api_v1, version="1.1.0", title="Simple Secrets Manager",
description="Secrets management simplified",
authorizations=authorizations, security='apikey')
authorizations=authorizations)
app = Flask(__name__)
app.register_blueprint(api_v1)


def abort_if_authorization_fail(token):
""" Check if an API token is valid
Args:
token (str): API Token
"""
if not conn.tokens.is_authorized(token):
api.abort(403, "Not Authorized to access the requested resource")


# Import API Resources
# The below conditions prevents IDE auto-formatting
if True:
# Secret Engines
from Api.resources.secrets.kv_resource import Engine_KV # noqa: F401
from Api.resources.auth.tokens_resource import Auth_Tokens # noqa: F401
# Authentication methods
from Api.resources.auth.userpass_resource \
import Auth_Userpass_delete, Auth_Userpass_register # noqa: F401
22 changes: 17 additions & 5 deletions Api/resources/auth/tokens_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Token Authentication API Resource
from flask_restx import fields, Resource
from Api.api import api, conn
from Access.is_auth import userpass

# tokens Namespace
tokens_ns = api.namespace(
Expand Down Expand Up @@ -30,21 +31,32 @@
params={})
class Auth_Tokens(Resource):
"""Token operations"""

@api.doc(
description="Revoke a given API token",
responses={200: "Token revoked"},
security='UserPass',
responses={
200: "Token revoked",
401: "Unauthorized"},
parser=tokens_parser)
@api.marshal_with(tokens_model)
@userpass.login_required
def delete(self):
"""Revoke a given API token"""
# TODO: Add support for userpass
args = tokens_parser.parse_args()
return conn.tokens.revoke(token=args['token'])
return conn.tokens.revoke(
username=userpass.current_user(),
token=args['token'])

@api.doc(description="Generate a new API token.")
@api.doc(
description="Generate a new API token.",
security='UserPass',
responses={
200: "Token generated",
401: "Unauthorized"})
@api.marshal_with(tokens_model)
@userpass.login_required
def get(self):
"""Generate a new API token"""
# TODO: Add support for userpass
return conn.tokens.generate()
return conn.tokens.generate(username=userpass.current_user())
73 changes: 73 additions & 0 deletions Api/resources/auth/userpass_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# Userpass Authentication API Resource
from flask_restx import fields, Resource
from Api.api import api, conn

# Userpass Auth Namespace
userpass_ns = api.namespace(
name="auth/userpass",
description="Allows authentication using a username and password.")
userpass_model = api.model(
"Auth Method - Userpass", {
"username": fields.String(
required=True, pattern="[a-fA-F0-9_]+", min_length=2,
description="Username for userpass authentication"),
"password": fields.String(
required=True, min_length=6,
description="Password for userpass authentication"),
"status": fields.String(
required=False,
description="Operation Status"),
})

# Userpass Arguments
# For deleting user
delete_userpass_parser = api.parser()
delete_userpass_parser.add_argument(
"username", type=str, required=True, location="form",
help="Username must already exist.")
# For adding new user
post_userpass_parser = api.parser()
post_userpass_parser.add_argument(
"username", type=str, required=True, location="form",
help="Username must atleast be 2 characters long")
post_userpass_parser.add_argument(
"password", type=str, required=True, location="form",
help="Password should atleast be 6 characters long")


@userpass_ns.route("/delete")
@api.doc(
responses={},
params={})
class Auth_Userpass_delete(Resource):
"""Userpass operations"""

@api.doc(
description="Revoke a given user",
responses={200: "User account removed"},
parser=delete_userpass_parser)
@api.marshal_with(userpass_model)
def delete(self):
"""Revoke a given user"""
args = delete_userpass_parser.parse_args()
return conn.userpass.remove(username=args['username'])


@userpass_ns.route("/register")
@api.doc(
responses={},
params={})
class Auth_Userpass_register(Resource):
"""Userpass operations"""

@api.doc(
description="Register new user.",
parser=post_userpass_parser)
@api.marshal_with(userpass_model)
def post(self):
"""Register new user"""
# TODO: Support for root key to create new users
args = post_userpass_parser.parse_args()
_usr, _pass = args['username'], args['password']
return conn.userpass.register(username=_usr, password=_pass)
19 changes: 13 additions & 6 deletions Api/resources/secrets/kv_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Key-Value (KV) Secrets Engines API Resource
from flask_restx import fields, Resource
from flask import request
from Api.api import api, conn, abort_if_authorization_fail
from Api.api import api, conn
from Access.is_auth import abort_if_authorization_fail

# KV Namespace
kv_ns = api.namespace(
Expand Down Expand Up @@ -43,7 +44,9 @@
class Engine_KV(Resource):
"""Key-Value API operations"""

@api.doc(description="Update a kv in a path", parser=kv_parser)
@api.doc(
description="Update a kv in a path", security='Token',
parser=kv_parser)
@api.marshal_with(kv_model)
def put(self, path, key):
"""Update a given resource"""
Expand All @@ -53,29 +56,33 @@ def put(self, path, key):
return conn.kv.update(path, key, args['value'])

@api.doc(
description="Delete a KV from a path",
description="Delete a KV from a path", security='Token',
responses={204: "Secrets deleted"})
def delete(self, path, key):
"""Delete a given kv"""
# ! Appropriate HTTP response codes need to be returned
API_KEY = request.headers.get('X-API-KEY', type=str, default=None)
abort_if_authorization_fail(API_KEY)

return conn.kv.delete(path, key)

@api.doc(description="Add a KV to a path", parser=kv_parser)
@api.doc(
description="Add a KV to a path", security='Token',
parser=kv_parser)
@api.marshal_with(kv_model)
def post(self, path, key):
"""Add a new kv to a path"""
# ! Appropriate HTTP response codes need to be returned
args = kv_parser.parse_args()
API_KEY = request.headers.get('X-API-KEY', type=str, default=None)
abort_if_authorization_fail(API_KEY)

return conn.kv.add(path, key, args['value'])

@api.doc(description="Return a KV from a path")
@api.doc(description="Return a KV from a path", security='Token')
@api.marshal_with(kv_model)
def get(self, path, key):
"""Fetch a given KV from a path"""
# ! Appropriate HTTP response codes need to be returned
API_KEY = request.headers.get('X-API-KEY', type=str, default=None)
abort_if_authorization_fail(API_KEY)
return conn.kv.get(path, key)
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
FROM python:3.8-slim-buster

LABEL title="Simple Secrets Manager"
LABEL version="1.0.0"
LABEL version="1.1.0"
LABEL author.name="Krishnakanth Alagiri"
LABEL author.github="https://github.com/bearlike"
LABEL repository="https://github.com/bearlike/simple-secrets-manager"
Expand Down
Loading

0 comments on commit 9914d65

Please sign in to comment.