Skip to content

Commit

Permalink
Bump version to 1.2.0 (#8)
Browse files Browse the repository at this point in the history
* Added basic 404 handling
* Bug fixes & added support for password policy
  • Loading branch information
bearlike authored Apr 16, 2022
1 parent 39aec25 commit bec8ca1
Show file tree
Hide file tree
Showing 15 changed files with 193 additions and 100 deletions.
15 changes: 15 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
name: Feature Request
about: "For feature requests. Please search for existing issues first. Also see CONTRIBUTING."
---

#### Feature description
> Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.
#### Suggest a solution (Optional)
> Replace This Text: A concise description of your preferred solution. Things to address include:
> * Details of the technical implementation
> * Tradeoffs made in design decisions
> * Caveats and considerations for the future
>
> If there are multiple solutions, please present each one separately. Save comparisons for the very end.
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: Build and deploy multiarch image

on:
push:
# branches:
# - 'releases/v*'
tags:
- 'v*'
paths-ignore:
Expand Down Expand Up @@ -37,9 +35,9 @@ jobs:
env:
IMG_NAME: ${{ 'krishnaalagiri/ssm' }}
# Versioning: MAJOR.MINOR.PATCH (eg., 1.2.3)
VERSION_FULL: ${{ '1.1.2' }}
VERSION_FULL: ${{ '1.2.0' }}
# For v1.2.3, VERSION_SHORT is '1.2'
VERSION_SHORT: ${{ '1.1' }}
VERSION_SHORT: ${{ '1.2' }}
# For v1.2.3, VERSION_MAJOR is '1'
VERSION_MAJOR: ${{ '1' }}
with:
Expand Down
16 changes: 1 addition & 15 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: "Code Quality Analysis"

on:
push:
Expand All @@ -33,9 +22,6 @@ jobs:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support

steps:
- name: Checkout repository
uses: actions/checkout@v3
Expand Down
68 changes: 59 additions & 9 deletions Access/userpass.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,54 @@
#!/usr/bin/env python3
""" User-Pass authentication for Secrets Manager
"""
from werkzeug.security import generate_password_hash, check_password_hash
from bson.timestamp import Timestamp
from textwrap import dedent
import datetime as dt
from werkzeug.security import generate_password_hash, check_password_hash
import re
import os


class _password_policy:
def __init__(self):
# Username policy: regex pattern
self.uname_pat = "[a-zA-Z0-9_]+"
# min length (default: 6)
self.length = os.environ.get("PASSWORD_POLICY_LENGTH", 6)
# need min. (default: 1) uppercase letters
self.uppercase = os.environ.get("PASSWORD_POLICY_UPPERCASE", 1)
# need min. (default: 1) uppercase letters
self.lowercase = os.environ.get("PASSWORD_POLICY_LOWERCASE", 1)
# need min. (default: 1) digits
self.numbers = os.environ.get("PASSWORD_POLICY_NUMBERS", 1)
# need min. (default: 1) special characters
self.special = os.environ.get("PASSWORD_POLICY_SPECIAL", 1)

def __repr__(self):
policy_str = f"""
(1) Minimum of { self.length } characters in length.
(2) Must have at least { self.lowercase } lowercase characters.
(3) Must have at least { self.uppercase }uppercase characters.
(4) Must have at least { self.numbers } numbers.
(5) Must have at least { self.special } special characters.
"""
policy_str = dedent(policy_str).replace('\n', ' ')
return policy_str

def check(self, password):
""" Password policy/rules to encourage users to employ strong passwords.
Args:
password (str): Password string
Returns:
bool: True if password policy is satisfied
"""
if len(password) >= self.length and \
len(re.findall("[a-z]", password)) >= self.lowercase and \
len(re.findall("[A-Z]", password)) >= self.uppercase and \
len(re.findall("[0-9]", password)) >= self.numbers and \
len(re.findall("[^a-z^A-Z^0-9]", password)) >= self.special:
return True
return False


class User_Pass:
Expand All @@ -15,6 +60,7 @@ def __init__(self, userpass_auth_col):
# * Create unique index on 'username' for secrets_manager_auth.userpass
# * db.userpass.createIndex( { "username": 1 }, { unique: true } )
self._userpass = userpass_auth_col
self.p_pol = _password_policy()

def register(self, username, password):
""" Register a new user
Expand All @@ -24,6 +70,12 @@ def register(self, username, password):
Returns:
dict : Dictionary with operation status
"""
# Username policy check
if not re.fullmatch(self.p_pol.uname_pat, username):
return f"Username does not match { self.p_pol.uname_pat }", 400
# Password policy check
if not self.p_pol.check(password):
return f"Password policy not met. { self.p_pol }", 400
finder = self._userpass.find_one({"username": username})
if not finder:
password = generate_password_hash(password, method='sha256')
Expand All @@ -34,9 +86,8 @@ def register(self, username, password):
}
_ = self._userpass.insert_one(data)
status = {"status": "OK"}
else:
status = {"status": "User already exist"}
return status
return status, 200
return "User already exist", 400

def remove(self, username):
""" Deletes an existing user
Expand All @@ -47,11 +98,10 @@ def remove(self, username):
"""
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
return "User does not exist", 400
_ = self._userpass.delete_one({"username": username})
result = {"status": "OK"}
return result, 200

def is_authorized(self, username, password):
""" Check if a userpass is valid
Expand Down
1 change: 1 addition & 0 deletions Api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/usr/bin/env python3
4 changes: 3 additions & 1 deletion Api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

conn = Connection()
api_v1 = Blueprint("api", __name__, url_prefix="/api")
api = Api(api_v1, version="1.1.2", title="Simple Secrets Manager",
api = Api(api_v1, version="1.2.0", title="Simple Secrets Manager",
description="Secrets management simplified",
authorizations=authorizations)
app = Flask(__name__)
Expand All @@ -34,3 +34,5 @@
# Authentication methods
from Api.resources.auth.userpass_resource \
import Auth_Userpass_delete, Auth_Userpass_register # noqa: F401
# Handling HTTP Errors
from Api.errors import errors # noqa: F401
1 change: 1 addition & 0 deletions Api/errors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/usr/bin/env python3
14 changes: 14 additions & 0 deletions Api/errors/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
from Api.api import app
from flask import jsonify


@app.errorhandler(404)
def not_found(e):
return jsonify(error="Resource not found"), 404


@app.errorhandler(Exception)
def server_error(e):
app.logger.exception(e)
return jsonify(error="Server error. Contact administrator"), 500
25 changes: 18 additions & 7 deletions Api/resources/auth/userpass_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
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")
help="Username must atleast be 1 characters long")
post_userpass_parser.add_argument(
"password", type=str, required=True, location="form",
help="Password should atleast be 6 characters long")
help="Password should satisfy policy")


@userpass_ns.route("/delete")
Expand All @@ -42,16 +42,21 @@
params={})
class Auth_Userpass_delete(Resource):
"""Userpass operations"""

@api.doc(
description="Revoke a given user",
responses={200: "User account removed"},
responses={
200: "User account removed",
400: "User does not exist",
},
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'])
status, code = conn.userpass.remove(username=args['username'])
if code != 200:
api.abort(code, status)
return status


@userpass_ns.route("/register")
Expand All @@ -60,14 +65,20 @@ def delete(self):
params={})
class Auth_Userpass_register(Resource):
"""Userpass operations"""

@api.doc(
description="Register new user.",
responses={
200: "User account created",
400: "Invalid username or password",
},
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)
status, code = conn.userpass.register(username=_usr, password=_pass)
if code != 200:
api.abort(code, status)
return status
33 changes: 22 additions & 11 deletions Api/resources/secrets/kv_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
@kv_ns.route("/<string:path>/<string:key>")
@api.doc(
responses={
404: "Path or KV not found",
403: 'Not Authorized'},
401: "Unauthorized",
404: "Path or KV not found"},
params={
"path": "Path to a KV store",
"key": "Key (index) in path where a secret (value) is stored",
Expand All @@ -53,36 +53,47 @@ def put(self, path, key):
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.update(path, key, args['value'])
status, code = conn.kv.update(path, key, args['value'])
if code != 200:
api.abort(code, status)
return None
return status

@api.doc(
description="Delete a KV from a path", security='Token',
responses={204: "Secrets deleted"})
responses={200: "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)
status, code = conn.kv.delete(path, key)
if code != 200:
api.abort(code, status)
return None
return status

@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'])
status, code = conn.kv.add(path, key, args['value'])
if code != 200:
api.abort(code, status)
return None
return status

@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)
status, code = conn.kv.get(path, key)
if code != 200:
api.abort(code, str(status))
return status
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 com.ssm.title="Simple Secrets Manager"
LABEL com.ssm.version="1.1.2"
LABEL com.ssm.version="1.2.0"
LABEL com.ssm.author.name="Krishnakanth Alagiri"
LABEL com.ssm.author.github="https://github.com/bearlike"
LABEL com.ssm.repo="https://github.com/bearlike/simple-secrets-manager"
Expand Down
Loading

0 comments on commit bec8ca1

Please sign in to comment.