class ClientResource(Resource): """Client Resource This resource represents an OAuth 2.0 client that is associated with a user. """ def __init__(self): self.client_schema = OAuth2ClientSchema() self.clients_schema = OAuth2ClientSchema(many=True) self.response_handler = ResponseBody() @require_oauth() def get(self, id: str = None): if not id: clients = OAuth2Client.query.all() clients_obj = self.clients_schema.dump(clients) return self.response_handler.get_all_response(clients_obj) else: client = OAuth2Client.query.filter_by(id=id).first() if client: client_obj = self.client_schema.dump(client) return self.response_handler.get_one_response( client_obj, request={'id': id}) else: return self.response_handler.not_found_response(id) @require_oauth() def post(self, id: str = None): ''' The POST method mainly enables the following: (1) creation of a new Client. (2) deletion or rotatation of the secret of an existing client – accomplished by POSTing with query params (?action=delete_secret, or ?action=rotate_secret). ''' # Check for data, since all POST requests need it. try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() if not request_data: return self.response_handler.empty_request_body_response() # Check for query params action = request.args.get("action") if action: try: client_id = request_data['id'] except KeyError as e: return self.response_handler.custom_response( code=422, messages='Please provide the ID of the client.') if action == 'delete_secret': return self._delete_secret(client_id) elif action == 'rotate_secret': return self._rotate_secret(client_id) else: return self.response_handler.custom_response( code=422, messages= "Invalid query param! 'action' must be either 'delete_secret' or 'rotate_secret'." ) # Assume that the request intends to create a new user: an ID should not be in the request data. if id is not None: return self.response_handler.method_not_allowed_response() errors = self.client_schema.validate(request_data) if errors: return self.response_handler.custom_response(code=422, messages=errors) try: client = OAuth2Client() client.id = str(uuid4()).replace('-', '') client.user_id = request_data['user_id'] client.client_id = gen_salt(24) client.client_secret = gen_salt(48) client_metadata = {'scope': ''} for k, v in request_data.items(): if hasattr(client, k): if k == 'roles': try: for role_id in request_data[k]: role = Role.query.filter_by(id=role_id).first() if role: client.roles.append(role) else: return self.response_handler.custom_response( code=400, messages={ 'roles': [ 'Error assigning role to client.' ] }) except Exception as e: return self.response_handler.custom_response( code=400, messages={ 'roles': ['Error assigning role to client.'] }) else: try: setattr(client, k, v) except Exception as e: client_metadata[k] = v client.set_client_metadata(client_metadata) client.client_id_issued_at = int(time.time()) db.session.add(client) db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response( exception_name, request=request_data) return self.response_handler.successful_creation_response( 'Client', client.id, request_data) @require_oauth() def put(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self.update(id, False) @require_oauth() def patch(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self.update(id) @require_oauth() def delete(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() try: client = OAuth2Client.query.filter_by(id=id).first() if client: client_obj = self.client_schema.dump(client) db.session.delete(client) db.session.commit() return self.response_handler.successful_delete_response( 'Client', id, client_obj) else: return self.response_handler.not_found_response(id) except Exception: return self.response_handler.not_found_response(id) def update(self, id: str, partial=True): """General update function for PUT and PATCH. Using Marshmallow, the logic for PUT and PATCH differ by a single parameter. This method abstracts that logic and allows for switching the Marshmallow validation to partial for PATCH and complete for PUT. """ try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() client = OAuth2Client.query.filter_by(id=id).first() if not client: return self.response_handler.not_found_response(id) if not request_data: return self.response_handler.empty_request_body_response() errors = self.client_schema.validate(request_data, partial=partial) if errors: return self.response_handler.custom_response(code=422, messages=errors) for k, v in request_data.items(): if hasattr(client, k): if k == 'roles': try: if len(request_data[k] ) == 0: # allow removal of all roles client.roles = [] else: current_roles = client.roles.copy() new_roles = [] for role_id in request_data[k]: role = Role.query.filter_by(id=role_id).first() if role: new_roles.append(role) if role not in client.roles: # only append roles that aren't already there client.roles.append(role) else: return self.response_handler.custom_response( code=400, messages={ 'roles': [ 'Error assigning role to client.' ] }) for role in current_roles: if role not in new_roles: client.roles.remove(role) except Exception as e: return self.response_handler.custom_response( code=400, messages={ 'roles': ['Error assigning role to client.'] }) elif k == 'client_secret': return self.response_handler.custom_response( code=422, messages={ 'client_secret': ['The client_secret cannot be updated.'] }) else: setattr(client, k, v) try: db.session.commit() return self.response_handler.successful_update_response( 'Client', id, request_data) except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response( exception_name, request=request_data) def _update_secret(self, client_id: str, new_secret): """Helper function for updating the client_secret. This function serves the delete and rotate actions, available in the POST method. """ client = OAuth2Client.query.filter_by(id=client_id).first() if not client: return self.response_handler.not_found_response(client_id) client.client_secret = new_secret try: db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response(exception_name) return self.response_handler.custom_response( code=200, status='OK', messages={'client_secret': new_secret}) def _delete_secret(self, client_id): new_secret = None return self._update_secret(client_id, new_secret) def _rotate_secret(self, client_id): new_secret = gen_salt(48) return self._update_secret(client_id, new_secret)
class DataTrustResource(Resource): """A Data Trust Resource.""" def __init__(self): self.data_trust_schema = DataTrustSchema() self.data_trusts_schema = DataTrustSchema(many=True) self.response_handler = ResponseBody() @require_oauth() def get(self, id: str = None): if not id: data_trusts = DataTrust.query.all() data_trusts_obj = self.data_trusts_schema.dump(data_trusts).data return self.response_handler.get_all_response(data_trusts_obj) else: data_trust = DataTrust.query.filter_by(id=id).first() if data_trust: data_trusts_obj = self.data_trust_schema.dump(data_trust).data return self.response_handler.get_one_response( data_trusts_obj, request={'id': id}) else: return self.response_handler.not_found_response(id) @require_oauth() def post(self, id=None): if id is not None: return self.response_handler.method_not_allowed_response() try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() if not request_data: return self.response_handler.empty_request_body_response() data, errors = self.data_trust_schema.load(request_data) if errors: return self.response_handler.custom_response(code=422, messages=errors) try: data_trust = DataTrust(request_data['data_trust_name']) db.session.add(data_trust) db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response( exception_name, request=request_data) return self.response_handler.successful_creation_response( 'Data Trust', data_trust.id, request_data) @require_oauth() def put(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self.update(id, False) @require_oauth() def patch(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self.update(id) @require_oauth() def delete(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() try: data_trust = DataTrust.query.filter_by(id=id).first() if data_trust: data_trust_obj = self.data_trust_schema.dump(data_trust).data db.session.delete(data_trust) db.session.commit() return self.response_handler.successful_delete_response( 'Data Trust', id, data_trust_obj) else: return self.response_handler.not_found_response(id) except Exception: return self.response_handler.not_found_response(id) def update(self, id: str, partial=True): """General update function for PUT and PATCH. Using Marshmallow, the logic for PUT and PATCH differ by a single parameter. This method abstracts that logic and allows for switching the Marshmallow validation to partial for PATCH and complete for PUT. """ try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() data_trust = DataTrust.query.filter_by(id=id).first() if not data_trust: return self.response_handler.not_found_response(id) if not request_data: return self.response_handler.empty_request_body_response() data, errors = self.data_trust_schema.load(request_data, partial=partial) if errors: return self.response_handler.custom_response(code=422, messages=errors) for k, v in request_data.items(): if hasattr(data_trust, k): setattr(data_trust, k, v) try: data_trust.date_last_updated = datetime.utcnow() db.session.commit() return self.response_handler.successful_update_response( 'Data Trust', id, request_data) except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response( exception_name, request=request_data)
class RoleResource(Resource): """Role Resource This resource represents a role associated with a client. """ def __init__(self): self.role_schema = RoleSchema() self.roles_schema = RoleSchema(many=True) self.response_handler = ResponseBody() @require_oauth() def get(self, id: str = None): if not id: roles = Role.query.all() roles_obj = self.roles_schema.dump(roles).data return self.response_handler.get_all_response(roles_obj) else: role = Role.query.filter_by(id=id).first() if role: role_obj = self.role_schema.dump(role).data return self.response_handler.get_one_response( role_obj, request={'id': id}) else: return self.response_handler.not_found_response(id) @require_oauth() def post(self, id: str = None): if id is not None: return self.response_handler.method_not_allowed_response() try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() if not request_data: return self.response_handler.empty_request_body_response() data, errors = self.role_schema.load(request_data) if errors: return self.response_handler.custom_response(code=422, messages=errors) try: role = Role(role=request_data['role'], description=request_data['description']) if 'rules' in request_data.keys(): role.rules = request_data['rules'] db.session.add(role) db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response( exception_name, request=request_data) return self.response_handler.successful_creation_response( 'Role', role.id, request_data) @require_oauth() def put(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self.update(id, False) @require_oauth() def patch(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self.update(id) @require_oauth() def delete(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() try: role = Role.query.filter_by(id=id).first() if role: role_obj = self.role_schema.dump(role).data db.session.delete(role) db.session.commit() return self.response_handler.successful_delete_response( 'Role', id, role_obj) else: return self.response_handler.not_found_response(id) except Exception: return self.response_handler.not_found_response(id) def update(self, id: str, partial=True): """General update function for PUT and PATCH. Using Marshmallow, the logic for PUT and PATCH differ by a single parameter. This method abstracts that logic and allows for switching the Marshmallow validation to partial for PATCH and complete for PUT. """ try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() role = Role.query.filter_by(id=id).first() if not role: return self.response_handler.not_found_response(id) if not request_data: return self.response_handler.empty_request_body_response() data, errors = self.role_schema.load(request_data, partial=partial) if errors: return self.response_handler.custom_response(code=422, messages=errors) for k, v in request_data.items(): if hasattr(role, k): setattr(role, k, v) try: db.session.commit() return self.response_handler.successful_update_response( 'Role', id, request_data) except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response( exception_name, request=request_data)
class UserResource(Resource): """A User Resource. This resource defines an Auth Service user who may have zero or more OAuth 2.0 clients associated with their accounts. """ def __init__(self): self.data_trust_schema = DataTrustSchema() self.data_trusts_schema = DataTrustSchema(many=True) self.user_schema = UserSchema() self.users_schema = UserSchema(many=True) self.response_handler = ResponseBody() @require_oauth() def get(self, id: str = None): if not id: users = User.query.all() users_obj = self.users_schema.dump(users).data return self.response_handler.get_all_response(users_obj) else: user = User.query.filter_by(id=id).first() if user: user_obj = self.user_schema.dump(user).data return self.response_handler.get_one_response(user_obj, request={'id': id}) else: return self.response_handler.not_found_response(id) @require_oauth() @use_args(POST_ARGS) def post(self, action, id: str = None): # Check for data, since all POST requests need it. try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() if not request_data: return self.response_handler.empty_request_body_response() if id is not None: return self.response_handler.method_not_allowed_response() # Check for query params/webargs (i.e., action). if action: try: user_id = request_data['id'] except KeyError as e: return self.response_handler.custom_response(code=422, messages='Please provide the ID of the user.') else: return self._deactivate(user_id) data, errors = self.user_schema.load(request_data) if errors: return self.response_handler.custom_response(code=422, messages=errors) try: user = User(request_data['username'], request_data['password'], firstname=request_data['firstname'], lastname=request_data['lastname'], organization=request_data['organization'], email_address=request_data['email_address'], data_trust_id=request_data['data_trust_id']) if 'telephone' in request_data.keys(): user.telephone = request_data['telephone'] db.session.add(user) db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response(exception_name, request=request_data) return self.response_handler.successful_creation_response('User', user.id, request_data) @require_oauth() def put(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self._update(id, False) @require_oauth() def patch(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self._update(id) @require_oauth() def delete(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() try: user = User.query.filter_by(id=id).first() if user: user_obj = self.user_schema.dump(user).data db.session.delete(user) db.session.commit() return self.response_handler.successful_delete_response('User', id, user_obj) else: return self.response_handler.not_found_response(id) except Exception: return self.response_handler.not_found_response(id) def _update(self, id: str, partial=True): """General update function for PUT and PATCH. Using Marshmallow, the logic for PUT and PATCH differ by a single parameter. This method abstracts that logic and allows for switching the Marshmallow validation to partial for PATCH and complete for PUT. """ try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() user = User.query.filter_by(id=id).first() if not user: return self.response_handler.not_found_response(id) if not request_data: return self.response_handler.empty_request_body_response() data, errors = self.user_schema.load(request_data, partial=partial) if errors: return self.response_handler.custom_response(code=422, messages=errors) for k, v in request_data.items(): if hasattr(user, k) and k != 'password_hash': setattr(user, k, v) if k == 'password': user.password = v try: user.date_last_updated = datetime.utcnow() db.session.commit() return self.response_handler.successful_update_response('User', id, request_data) except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response(exception_name, request=request_data) def _deactivate(self, user_id: str): user = User.query.filter_by(id=user_id).first() if not user: return self.response_handler.not_found_response(user_id) user.active = False self._db_commit() clients = OAuth2Client.query.filter_by(user_id=user_id).all() for client in clients: client.client_secret = None self._db_commit() return self.response_handler.successful_update_response('User', user_id) def _db_commit(self): try: db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response(exception_name)
class UserResource(Resource): """A User Resource. This resource defines an Auth Service user who may have zero or more OAuth 2.0 clients associated with their accounts. """ def __init__(self): self.user_schema = UserSchema() self.users_schema = UserSchema(many=True) self.response_handler = ResponseBody() @require_oauth() def get(self, id: str = None): if not id: users = User.query.all() users_obj = self.users_schema.dump(users) users_obj_clean = [{k: v for k, v in user.items() if k != 'role_id'} for user in users_obj] return self.response_handler.get_all_response(users_obj_clean) else: user = User.query.filter_by(id=id).first() if user: user_obj = self.user_schema.dump(user) user_obj.pop('role_id') return self.response_handler.get_one_response(user_obj, request={'id': id}) else: return self.response_handler.not_found_response(id) @require_oauth() def post(self, id: str = None): # Check for data, since all POST requests need it. try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() if not request_data: return self.response_handler.empty_request_body_response() if id is not None: return self.response_handler.method_not_allowed_response() # Check for query params action = request.args.get("action") if action: try: user_id = request_data['id'] except KeyError as e: return self.response_handler.custom_response(code=422, messages='Please provide the ID of the user.') else: if action == "deactivate": return self._deactivate(user_id) elif action == "activate": return self._activate(user_id) else: return self.response_handler.custom_response(code=422, messages="Invalid query param! 'action' can only be 'deactivate'.") errors = self.user_schema.validate(request_data) if errors: return self.response_handler.custom_response(code=422, messages=errors) try: user = User( request_data['username'], request_data['password'], role_id=request_data['role_id'] if 'role_id' in request_data.keys( ) else None, person_id=request_data['person_id'] if 'person_id' in request_data.keys( ) else None, can_login=request_data['can_login'] if 'can_login' in request_data.keys( ) else False, active=request_data['active'] if 'active' in request_data.keys() else False) db.session.add(user) db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response(exception_name, request=request_data) return self.response_handler.successful_creation_response('User', user.id, request_data) @require_oauth() def put(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self._update(id, False) @require_oauth() def patch(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() return self._update(id) @require_oauth() def delete(self, id: str = None): if id is None: return self.response_handler.method_not_allowed_response() user = User.query.filter_by(id=id).first() if user: user_obj = self.user_schema.dump(user) db.session.delete(user) db.session.commit() return self.response_handler.successful_delete_response('User', id, user_obj) else: return self.response_handler.not_found_response(id) def _update(self, id: str, partial=True): """General update function for PUT and PATCH. Using Marshmallow, the logic for PUT and PATCH differ by a single parameter. This method abstracts that logic and allows for switching the Marshmallow validation to partial for PATCH and complete for PUT. """ try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() user = User.query.filter_by(id=id).first() if not user: return self.response_handler.not_found_response(id) if not request_data: return self.response_handler.empty_request_body_response() errors = self.user_schema.validate(request_data, partial=partial) if errors: return self.response_handler.custom_response(code=422, messages=errors) for k, v in request_data.items(): if hasattr(user, k) and k != 'password_hash': setattr(user, k, v) if k == 'password': user.password = v try: user.date_last_updated = datetime.utcnow() db.session.commit() return self.response_handler.successful_update_response('User', id, request_data) except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response(exception_name, request=request_data) def _deactivate(self, user_id: str): user = User.query.filter_by(id=user_id).first() if not user: return self.response_handler.not_found_response(user_id) user.active = False user.can_login = False user.date_last_updated = datetime.utcnow() tokens = OAuth2Token.query.filter_by(user_id=user.id).all() for token in tokens: token.revoked = True token.expires_in = 0 clients = OAuth2Client.query.filter_by(user_id=user_id).all() for client in clients: client.client_secret = None self._db_commit() return self.response_handler.successful_update_response('User', user_id) def _activate(self, user_id: str): user = User.query.filter_by(id=user_id).first() if not user: raise RecordNotFoundError(record_id=user_id) user.active = True user.can_login = True user.date_last_updated = datetime.utcnow() clients = OAuth2Client.query.filter_by(user_id=user_id).all() for client in clients: client.client_secret = gen_salt(48) self._db_commit() return self.response_handler.successful_update_response('User', user_id) def _db_commit(self): try: db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response(exception_name)
class AuthorizedScopeResource(Resource): """Authorized scope resource for linking roles to scopes. This resource enables the notion of an OAuth2 scope to be shared between user roles and OAuth2 clients. This way, the capabilities of specific clients can be restricted to the scopes associated with a user's role. """ def __init__(self): self.authorized_scope_schema = AuthorizedScopeSchema() self.authorized_scopes_schema = AuthorizedScopeSchema(many=True) self.response_handler = ResponseBody() @require_oauth() def get(self, id: str = None, sid: str = None): if sid is None: try: authorized_scopes = AuthorizedScope.query.all() scopes_obj = self.authorized_scopes_schema.dump( authorized_scopes) scopes_obj_clean = [{ k: v for k, v in authorized_scope.items() if k != 'scope_id' and k != 'role_id' and k != 'role' } for authorized_scope in scopes_obj] return self.response_handler.get_all_response(scopes_obj_clean) except Exception: return self.response_handler.exception_response('Unknown') else: try: authorized_scope = AuthorizedScope.query.filter( AuthorizedScope.role_id == id, AuthorizedScope.scope_id == sid).first() scope_obj = self.authorized_scope_schema.dump(authorized_scope) if scope_obj: scope_obj.pop('role_id', None) scope_obj.pop('scope_id', None) return self.response_handler.get_one_response(scope_obj) else: return self.response_handler.not_found_response(id=sid) except Exception: return self.response_handler.exception_response('Unkown') @require_oauth() def post(self, id: str = None, sid: str = None): if sid is not None: return self.response_handler.method_not_allowed_response() try: request_data = request.get_json(force=True) except Exception as e: return self.response_handler.empty_request_body_response() if not request_data: return self.response_handler.empty_request_body_response() errors = self.authorized_scope_schema.validate(request_data) if errors: return self.response_handler.custom_response(code=422, messages=errors) try: authorized_scope = AuthorizedScope( role_id=id, scope_id=request_data['scope_id']) db.session.add(authorized_scope) db.session.commit() except Exception as e: db.session.rollback() exception_name = type(e).__name__ return self.response_handler.exception_response( exception_name, request=request_data) return self.response_handler.successful_creation_response( 'AuthorizedScope', authorized_scope.role_id, request_data) @require_oauth() def put(self, id: str = None, sid: str = None): return self.response_handler.method_not_allowed_response() @require_oauth() def patch(self, id: str = None, sid: str = None): return self.response_handler.method_not_allowed_response() @require_oauth() def delete(self, id: str, sid: str = None): if sid is None: return self.response_handler.method_not_allowed_response() try: authorized_scope = AuthorizedScope.query.filter( AuthorizedScope.role_id == id, AuthorizedScope.scope_id == sid).first() if authorized_scope: authorized_scope_obj = self.authorized_scope_schema.dump( authorized_scope) db.session.delete(authorized_scope) db.session.commit() return self.response_handler.successful_delete_response( 'Role', id, authorized_scope_obj) else: return self.response_handler.not_found_response(sid) except Exception: return self.response_handler.not_found_response(sid)