Skip to content

Commit

Permalink
Merge pull request #6 from sandwichcloud/authstuff
Browse files Browse the repository at this point in the history
Redo auth system, add service accounts, add builtin auth, other stuffs
  • Loading branch information
rmb938 authored Dec 4, 2017
2 parents 6626944 + a1448ed commit 495f65c
Show file tree
Hide file tree
Showing 39 changed files with 881 additions and 441 deletions.
14 changes: 11 additions & 3 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,22 @@ RABBITMQ_PASSWORD=hunter2
# Auth #
####################

# Choose an auth driver
# Choose auth drivers to use (comma separated list)
# The first driver is shown as the default in /v1/auth/discover
# Github: deli_counter.auth.drivers.github.driver:GithubAuthDriver
# Gitlab: deli_counter.auth.drivers.gitlab.driver:GitlabAuthDriver
# OpenID: deli_counter.auth.drivers.openid.driver:OpenIDAuthDriver
# LDAP: deli_counter.auth_drivers.ldap.driver:LDAPAuthDriver
# DB: deli_counter.auth_drivers.db.driver:DBAuthDriver (always enabled)
# LDAP: deli_counter.auth.drivers.ldap.driver:LDAPAuthDriver
# DB: deli_counter.auth.drivers.builtin.driver:BuiltInAuthDriver
AUTH_DRIVERS=deli_counter.auth.drivers.github.driver:GithubAuthDriver

# A url safe 32 bit base64 encoded string used to encrypt tokens
# Multiple keys can be listed to allow rotation (comma separated). The first
# key in the list is the primary key.
# To rotate keys simply generate a new key and put it in the front of the list
# then after a while remove the old key from the list
AUTH_FERNET_KEYS=

####################
# GITHUB AUTH #
####################
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ WORKDIR /usr/src/app
# Install build time dependencies for uwsgi
# Install uwsgi and dumb-init
RUN apk --no-cache add --virtual build-deps \
build-base bash linux-headers pcre-dev postgresql-dev && \
build-base bash linux-headers pcre-dev postgresql-dev libffi-dev && \
pip install uwsgi dumb-init

# COPY tar.gz from build container
Expand All @@ -24,7 +24,7 @@ COPY wsgi.ini wsgi.ini
# Remove build time dependencies
# Install runtime dependencies
RUN apk del build-deps && \
apk --no-cache add openssl pcre libpq ca-certificates
apk --no-cache add openssl pcre libpq libffi ca-certificates

# add entrypoint
COPY docker-entrypoint.sh /bin/docker-entrypoint.sh
Expand Down
52 changes: 34 additions & 18 deletions deli_counter/auth/driver.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json
import logging
import secrets
from abc import ABCMeta, abstractmethod
from typing import Dict

from ingredients_db.models.authn import AuthNUser, AuthNToken, AuthNTokenRole
from cryptography.fernet import Fernet
from simple_settings import settings

from ingredients_db.models.authn import AuthNUser
from ingredients_db.models.authz import AuthZRole
from ingredients_http.router import Router

Expand All @@ -23,7 +26,8 @@ def discover_options(self) -> Dict:
def auth_router(self) -> Router:
raise NotImplementedError

def generate_user_token(self, session, username, roles):
def generate_user_token(self, session, expires_at, username, global_role_names, project_id=None,
project_role_ids=None):
user = session.query(AuthNUser).filter(AuthNUser.username == username).filter(
AuthNUser.driver == self.name).first()
if user is None:
Expand All @@ -32,19 +36,31 @@ def generate_user_token(self, session, username, roles):
user.driver = self.name
session.add(user)
session.flush()
session.refresh(user)

global_role_ids = []

for role_name in global_role_names:
role = session.query(AuthZRole).filter(AuthZRole.name == role_name).filter(
AuthZRole.project_id == None).first() # noqa: E711
if role is not None:
global_role_ids.append(role.id)

fernet = Fernet(settings.AUTH_FERNET_KEYS[0])

token_data = {
'expires_at': expires_at,
'user_id': user.id,
'roles': {
'global': global_role_ids,
'project': []
}
}

if project_id is not None:
token_data['project_id'] = project_id
if project_role_ids is None:
project_role_ids = []
token_data['roles']['project'] = project_role_ids

token = AuthNToken()
token.user_id = user.id
token.access_token = secrets.token_urlsafe()
session.add(token)
session.flush()

for role in roles:
db_role = session.query(AuthZRole).filter(AuthZRole.name == role).first()
if db_role is not None:
token_role = AuthNTokenRole()
token_role.token_id = token.id
token_role.role_id = db_role.id
session.add(token_role)

return token
return fernet.encrypt(json.dumps(token_data).encode())
Empty file.
16 changes: 16 additions & 0 deletions deli_counter/auth/drivers/builtin/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Dict

from deli_counter.auth.driver import AuthDriver
from deli_counter.auth.drivers.builtin.router import DatabaseAuthRouter
from ingredients_http.router import Router


class BuiltInAuthDriver(AuthDriver):
def __init__(self):
super().__init__('builtin')

def discover_options(self) -> Dict:
return {}

def auth_router(self) -> Router:
return DatabaseAuthRouter(self)
163 changes: 163 additions & 0 deletions deli_counter/auth/drivers/builtin/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import uuid

import arrow
import cherrypy

from deli_counter.auth.validation_models.builtin import RequestBuiltInLogin, RequestBuiltInCreateUser, \
ResponseBuiltInUser, RequestBuiltInChangePassword, RequestBuiltInUserRole, ParamsBuiltInUser, ParamsListBuiltInUser
from deli_counter.http.mounts.root.routes.v1.auth.validation_models.tokens import ResponseOAuthToken
from ingredients_db.models.builtin import BuiltInUser
from ingredients_http.request_methods import RequestMethods
from ingredients_http.route import Route
from ingredients_http.router import Router


class DatabaseAuthRouter(Router):
def __init__(self, driver):
super().__init__(uri_base='builtin')
self.driver = driver

@Route(route='login', methods=[RequestMethods.POST])
@cherrypy.config(**{'tools.authentication.on': False})
@cherrypy.tools.model_in(cls=RequestBuiltInLogin)
@cherrypy.tools.model_out(cls=ResponseOAuthToken)
def login(self):
request: RequestBuiltInLogin = cherrypy.request.model
with cherrypy.request.db_session() as session:
user: BuiltInUser = session.query(BuiltInUser).filter(BuiltInUser.username == request.username).first()
if user is None or user.password != request.password:
raise cherrypy.HTTPError(403, "Invalid username or password")

expiry = arrow.now().shift(days=+1)
token = self.driver.generate_user_token(session, expiry, user.username, user.roles)
session.commit()

response = ResponseOAuthToken()
response.access_token = token
response.expiry = expiry
return response

@Route(route='users', methods=[RequestMethods.POST])
@cherrypy.tools.enforce_policy(policy_name="builtin:users:create")
@cherrypy.tools.model_in(cls=RequestBuiltInCreateUser)
@cherrypy.tools.model_out(cls=ResponseBuiltInUser)
def create_user(self):
request: RequestBuiltInCreateUser = cherrypy.request.model
with cherrypy.request.db_session() as session:
user = BuiltInUser()
user.username = request.username
user.password = request.password

session.add(user)
session.commit(user)
session.refresh(user)

return ResponseBuiltInUser.from_database(user)

@Route(route='users/{user_id}')
@cherrypy.tools.model_params(cls=ParamsBuiltInUser)
@cherrypy.tools.enforce_policy(policy_name="builtin:users:get")
@cherrypy.tools.model_out(cls=ResponseBuiltInUser)
@cherrypy.tools.resource_object(id_param="user_id", cls=BuiltInUser)
def get_user(self, user_id):
return ResponseBuiltInUser.from_database(cherrypy.request.resource_object)

@Route(route='users')
@cherrypy.tools.model_params(cls=ParamsListBuiltInUser)
@cherrypy.tools.enforce_policy(policy_name="builtin:users:list")
@cherrypy.tools.model_out_pagination(cls=ResponseBuiltInUser)
def list_users(self, limit: int, marker: uuid.UUID):
return self.paginate(BuiltInUser, ResponseBuiltInUser, limit, marker)

@Route(route='users/{user_id}', methods=[RequestMethods.DELETE])
@cherrypy.tools.model_params(cls=ParamsBuiltInUser)
@cherrypy.tools.enforce_policy(policy_name="builtin:users:delete")
@cherrypy.tools.resource_object(id_param="user_id", cls=BuiltInUser)
def delete_user(self, user_id):
cherrypy.response.status = 204
# Fix for https://github.com/cherrypy/cherrypy/issues/1657
del cherrypy.response.headers['Content-Type']
with cherrypy.request.db_session() as session:
user: BuiltInUser = session.merge(cherrypy.request.resource_object, load=False)

if user.username == "admin":
raise cherrypy.HTTPError(400, "Cannot delete admin user.")

session.delete(user)
session.commit()

@Route(route='users', methods=[RequestMethods.PATCH])
@cherrypy.tools.model_in(cls=RequestBuiltInChangePassword)
def change_password_self(self):
request: RequestBuiltInChangePassword = cherrypy.request.model
with cherrypy.request.db_session() as session:
if cherrypy.request.user.driver != self.driver.name:
raise cherrypy.HTTPError(400, "Token is not using 'builtin' authentication.")

user: BuiltInUser = session.query(BuiltInUser).filter(
BuiltInUser.username == cherrypy.request.user.username).first()
user.password = request.password
session.commit()

cherrypy.response.status = 204
# Fix for https://github.com/cherrypy/cherrypy/issues/1657
del cherrypy.response.headers['Content-Type']

@Route(route='users/{user_id}', methods=[RequestMethods.PATCH])
@cherrypy.tools.model_params(cls=ParamsBuiltInUser)
@cherrypy.tools.enforce_policy(policy_name="builtin:users:password")
@cherrypy.tools.model_in(cls=RequestBuiltInChangePassword)
@cherrypy.tools.resource_object(id_param="user_id", cls=BuiltInUser)
def change_password_other(self, user_id):
cherrypy.response.status = 204
# Fix for https://github.com/cherrypy/cherrypy/issues/1657
del cherrypy.response.headers['Content-Type']
request: RequestBuiltInChangePassword = cherrypy.request.model
with cherrypy.request.db_session() as session:
user: BuiltInUser = session.merge(cherrypy.request.resource_object, load=False)

if user.username == "admin":
raise cherrypy.HTTPError(400, "Only the admin user can change it's password.")

user.password = request.password
session.commit()

@Route(route='users/{user_id}/role/add', methods=[RequestMethods.PUT])
@cherrypy.tools.model_params(cls=ParamsBuiltInUser)
@cherrypy.tools.enforce_policy(policy_name="builtin:users:role:add")
@cherrypy.tools.model_in(cls=RequestBuiltInUserRole)
@cherrypy.tools.resource_object(id_param="user_id", cls=BuiltInUser)
def add_user_role(self, user_id):
cherrypy.response.status = 204
# Fix for https://github.com/cherrypy/cherrypy/issues/1657
del cherrypy.response.headers['Content-Type']
request: RequestBuiltInUserRole = cherrypy.request.model
with cherrypy.request.db_session() as session:
user: BuiltInUser = session.merge(cherrypy.request.resource_object, load=False)

if user.username == "admin":
raise cherrypy.HTTPError(400, "Cannot change roles for the admin user.")

user.roles.append(request.role)
session.commit()

@Route(route='users/{user_id}/role/remove', methods=[RequestMethods.PUT])
@cherrypy.tools.model_params(cls=ParamsBuiltInUser)
@cherrypy.tools.enforce_policy(policy_name="builtin:users:role:remove")
@cherrypy.tools.model_in(cls=RequestBuiltInUserRole)
@cherrypy.tools.resource_object(id_param="user_id", cls=BuiltInUser)
def remove_user_role(self, user_id):
cherrypy.response.status = 204
# Fix for https://github.com/cherrypy/cherrypy/issues/1657
del cherrypy.response.headers['Content-Type']
request: RequestBuiltInUserRole = cherrypy.request.model
with cherrypy.request.db_session() as session:
user: BuiltInUser = session.merge(cherrypy.request.resource_object, load=False)

if user.username == "admin":
raise cherrypy.HTTPError(400, "Cannot change roles for the admin user.")

if request.role not in user.roles:
raise cherrypy.HTTPError(400, "User does not have the requested role.")
user.roles.remove(request.role)
session.commit()
13 changes: 9 additions & 4 deletions deli_counter/auth/drivers/github/router.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import arrow
import cherrypy
import github
import github.AuthenticatedUser
Expand All @@ -8,7 +9,7 @@
from sqlalchemy_utils.types.json import json

from deli_counter.auth.validation_models.github import RequestGithubAuthorization, RequestGithubToken
from deli_counter.http.mounts.root.routes.v1.auth.z.validation_models.auth import ResponseOAuthToken
from deli_counter.http.mounts.root.routes.v1.auth.validation_models.tokens import ResponseOAuthToken
from ingredients_http.request_methods import RequestMethods
from ingredients_http.route import Route
from ingredients_http.router import Router
Expand All @@ -25,11 +26,15 @@ def generate_token(self, token_github_client):
raise cherrypy.HTTPError(403, "User not a member of GitHub organization: '" + settings.GITHUB_ORG + "'")

with cherrypy.request.db_session() as session:
token = self.driver.generate_user_token(session, github_user.login, self.driver.find_roles(github_user))
expiry = arrow.now().shift(days=+1)
token = self.driver.generate_user_token(session, expiry, github_user.login,
self.driver.find_roles(github_user))
session.commit()
session.refresh(token)

return ResponseOAuthToken.from_database(token)
response = ResponseOAuthToken()
response.access_token = token
response.expiry = expiry
return response

@Route(route='authorization', methods=[RequestMethods.POST])
@cherrypy.config(**{'tools.authentication.on': False})
Expand Down
Loading

0 comments on commit 495f65c

Please sign in to comment.