Skip to content

Commit

Permalink
Merge branch 'tkt_white_6709_patch_and_bulks' into 'white/dev'
Browse files Browse the repository at this point in the history
PATCH and v3

Closes #6709

See merge request faradaysec/faraday!1513
  • Loading branch information
EricHorvat committed Feb 12, 2021
2 parents 35ef4cd + fb80c0e commit 1ebbd4e
Show file tree
Hide file tree
Showing 56 changed files with 1,946 additions and 902 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG/current/v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Add v3 API, which includes:
* All endpoints ends without `/`
* `PATCH {model}/id` endpoints
* Bulk update via PATCH `{model}` endpoints
* Bulk delete via DELETE `{model}` endpoints
* Endpoints removed:
* `/v2/ws/<workspace_id>/activate/`
* `/v2/ws/<workspace_id>/change_readonly/`
* `/v2/ws/<workspace_id>/deactivate/`
* `/v2/ws/<workspace_name>/hosts/bulk_delete/`
* `/v2/ws/<workspace_name>/vulns/bulk_delete/`
* Endpoints updated:
* `/v2/ws/<workspace_name>/vulns/<int:vuln_id>/attachments/` => \
`/v3/ws/<workspace_name>/vulns/<int:vuln_id>/attachment`
89 changes: 85 additions & 4 deletions faraday/server/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,12 +1094,12 @@ def put(self, object_id, **kwargs):
flask.request)
# just in case an schema allows id as writable.
data.pop('id', None)
self._update_object(obj, data)
self._update_object(obj, data, partial=False)
self._perform_update(object_id, obj, data, **kwargs)

return self._dump(obj, kwargs), 200

def _update_object(self, obj, data):
def _update_object(self, obj, data, **kwargs):
"""Perform changes in the selected object
It modifies the attributes of the SQLAlchemy model to match
Expand All @@ -1112,7 +1112,7 @@ def _update_object(self, obj, data):
for (key, value) in data.items():
setattr(obj, key, value)

def _perform_update(self, object_id, obj, data, workspace_name=None):
def _perform_update(self, object_id, obj, data, workspace_name=None, partial=False):
"""Commit the SQLAlchemy session, check for updating conflicts"""
try:
db.session.add(obj)
Expand All @@ -1138,6 +1138,48 @@ def _perform_update(self, object_id, obj, data, workspace_name=None):
return obj


class PatchableMixin:
# TODO must be used with a UpdateMixin, when v2 be deprecated, add patch() to that Mixin

def patch(self, object_id, **kwargs):
"""
---
tags: ["{tag_name}"]
summary: Updates {class_model}
parameters:
- in: path
name: object_id
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema: {schema_class}
responses:
200:
description: Ok
content:
application/json:
schema: {schema_class}
409:
description: Duplicated key found
content:
application/json:
schema: {schema_class}
"""
obj = self._get_object(object_id, **kwargs)
context = {'updating': True, 'object': obj}
data = self._parse_data(self._get_schema_instance(kwargs, context=context, partial=True),
flask.request)
# just in case an schema allows id as writable.
data.pop('id', None)
self._update_object(obj, data, partial=True)
self._perform_update(object_id, obj, data, partial=True, **kwargs)

return self._dump(obj, kwargs), 200

class UpdateWorkspacedMixin(UpdateMixin, CommandMixin):
"""Add PUT /<workspace_name>/<route_base>/<id>/ route
Expand Down Expand Up @@ -1181,7 +1223,7 @@ def put(self, object_id, workspace_name=None):
"""
return super(UpdateWorkspacedMixin, self).put(object_id, workspace_name=workspace_name)

def _perform_update(self, object_id, obj, data, workspace_name=None):
def _perform_update(self, object_id, obj, data, workspace_name=None, partial=False):
# # Make sure that if I created new objects, I had properly commited them
# assert not db.session.new

Expand All @@ -1193,6 +1235,45 @@ def _perform_update(self, object_id, obj, data, workspace_name=None):
object_id, obj, data, workspace_name)


class PatchableWorkspacedMixin(PatchableMixin):
# TODO must be used with a UpdateWorkspacedMixin, when v2 be deprecated, add patch() to that Mixin

def patch(self, object_id, workspace_name=None):
"""
---
tags: ["{tag_name}"]
summary: Updates {class_model}
parameters:
- in: path
name: object_id
required: true
schema:
type: integer
- in: path
name: workspace_name
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema: {schema_class}
responses:
200:
description: Ok
content:
application/json:
schema: {schema_class}
409:
description: Duplicated key found
content:
application/json:
schema: {schema_class}
"""
return super(PatchableWorkspacedMixin, self).patch(object_id, workspace_name=workspace_name)


class DeleteMixin:
"""Add DELETE /<id>/ route"""
def delete(self, object_id, **kwargs):
Expand Down
8 changes: 7 additions & 1 deletion faraday/server/api/modules/activity_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from flask import Blueprint
from marshmallow import fields

from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin
from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin
from faraday.server.models import Command
from faraday.server.schemas import PrimaryKeyRelatedField

Expand Down Expand Up @@ -90,4 +90,10 @@ def _envelope_list(self, objects, pagination_metadata=None):
}


class ActivityFeedV3View(ActivityFeedView, PatchableWorkspacedMixin):
route_prefix = '/v3/ws/<workspace_name>/'
trailing_slash = False


ActivityFeedView.register(activityfeed_api)
ActivityFeedV3View.register(activityfeed_api)
41 changes: 37 additions & 4 deletions faraday/server/api/modules/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
ReadOnlyView,
CreateMixin,
GenericView,
ReadOnlyMultiWorkspacedView
ReadOnlyMultiWorkspacedView,
PatchableMixin
)
from faraday.server.api.modules.workspaces import WorkspaceSchema
from faraday.server.models import Agent, Executor, AgentExecution, db, \
Expand Down Expand Up @@ -97,6 +98,7 @@ class Meta:
'workspaces',
)


class AgentCreationView(CreateMixin, GenericView):
"""
---
Expand Down Expand Up @@ -168,6 +170,11 @@ def _perform_create(self, data, **kwargs):
return agent


class AgentCreationV3View(AgentCreationView):
route_prefix = '/v3'
trailing_slash = False


class ExecutorDataSchema(Schema):
executor = fields.String(default=None)
args = fields.Dict(default=None)
Expand Down Expand Up @@ -201,7 +208,7 @@ def _get_workspace(self, workspace_name):
except NoResultFound:
flask.abort(404, f"No such workspace: {workspace_name}")

def _update_object(self, obj, data):
def _update_object(self, obj, data, **kwargs):
"""Perform changes in the selected object
It modifies the attributes of the SQLAlchemy model to match
Expand All @@ -211,9 +218,10 @@ def _update_object(self, obj, data):
with some specific field. Typically the new method should call
this one to handle the update of the rest of the fields.
"""
workspace_names = data.pop('workspaces')
workspace_names = data.pop('workspaces', '')
partial = False if 'partial' not in kwargs else kwargs['partial']

if len(workspace_names) == 0:
if len(workspace_names) == 0 and not partial:
abort(
make_response(
jsonify(
Expand Down Expand Up @@ -243,6 +251,11 @@ def _update_object(self, obj, data):
return obj


class AgentWithWorkspacesV3View(AgentWithWorkspacesView, PatchableMixin):
route_prefix = '/v3'
trailing_slash = False


class AgentView(ReadOnlyMultiWorkspacedView):
route_base = 'agents'
model_class = Agent
Expand Down Expand Up @@ -337,6 +350,26 @@ def run_agent(self, workspace_name, agent_id):
})


class AgentV3View(AgentView):
route_prefix = '/v3/ws/<workspace_name>/'
trailing_slash = False

@route('/<int:agent_id>', methods=['DELETE'])
def remove_workspace(self, workspace_name, agent_id):
# This endpoint is not an exception for V3, overrides logic of DELETE
return super(AgentV3View, self).remove_workspace(workspace_name, agent_id)

@route('/<int:agent_id>/run', methods=['POST'])
def run_agent(self, workspace_name, agent_id):
return super(AgentV3View, self).run_agent(workspace_name, agent_id)

remove_workspace.__doc__ = AgentView.remove_workspace.__doc__
run_agent.__doc__ = AgentView.run_agent.__doc__


AgentWithWorkspacesView.register(agent_api)
AgentWithWorkspacesV3View.register(agent_api)
AgentCreationView.register(agent_api)
AgentCreationV3View.register(agent_api)
AgentView.register(agent_api)
AgentV3View.register(agent_api)
5 changes: 5 additions & 0 deletions faraday/server/api/modules/agent_auth_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ def post(self):
{'token': faraday_server.agent_token})


class AgentAuthTokenV3View(AgentAuthTokenView):
route_prefix = '/v3'
trailing_slash = False

AgentAuthTokenView.register(agent_auth_token_api)
AgentAuthTokenV3View.register(agent_auth_token_api)


# I'm Py3
8 changes: 8 additions & 0 deletions faraday/server/api/modules/bulk_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

logger = logging.getLogger(__name__)


class VulnerabilitySchema(vulns.VulnerabilitySchema):
class Meta(vulns.VulnerabilitySchema.Meta):
extra_fields = ('run_date',)
Expand Down Expand Up @@ -519,4 +520,11 @@ def post(self, workspace_name):

post.is_public = True


class BulkCreateV3View(BulkCreateView):
route_prefix = '/v3/ws/<workspace_name>/'
trailing_slash = False


BulkCreateView.register(bulk_create_api)
BulkCreateV3View.register(bulk_create_api)
19 changes: 18 additions & 1 deletion faraday/server/api/modules/commandsrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from flask_classful import route
from marshmallow import fields, post_load, ValidationError

from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin
from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin
from faraday.server.models import Command, Workspace
from faraday.server.schemas import MutableField, PrimaryKeyRelatedField, SelfNestedField, MetadataSchema

Expand Down Expand Up @@ -139,4 +139,21 @@ def last_command(self, workspace_name):
return flask.jsonify(command_obj)


class CommandV3View(CommandView, PatchableWorkspacedMixin):
route_prefix = '/v3/ws/<workspace_name>/'
trailing_slash = False

@route('/activity_feed')
def activity_feed(self, workspace_name):
return super(CommandV3View, self).activity_feed(workspace_name)

@route('/last', methods=['GET'])
def last_command(self, workspace_name):
return super(CommandV3View, self).last_command(workspace_name)

activity_feed.__doc__ = CommandView.activity_feed.__doc__
last_command.__doc__ = CommandView.last_command.__doc__


CommandView.register(commandsrun_api)
CommandV3View.register(commandsrun_api)
17 changes: 15 additions & 2 deletions faraday/server/api/modules/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from faraday.server.api.base import (
AutoSchema,
ReadWriteWorkspacedView,
InvalidUsage, CreateWorkspacedMixin, GenericWorkspacedView)
InvalidUsage, CreateWorkspacedMixin, GenericWorkspacedView, PatchableWorkspacedMixin)
from faraday.server.models import Comment
comment_api = Blueprint('comment_api', __name__)

Expand Down Expand Up @@ -55,6 +55,7 @@ class CommentView(CommentCreateMixing, ReadWriteWorkspacedView):
schema_class = CommentSchema
order_field = 'create_date'


class UniqueCommentView(GenericWorkspacedView, CommentCreateMixing):
"""
This view is used by the plugin engine to avoid duplicate comments
Expand Down Expand Up @@ -82,6 +83,18 @@ def _perform_create(self, data, workspace_name):
res = super(UniqueCommentView, self)._perform_create(data, workspace_name)
return res


class CommentV3View(CommentView, PatchableWorkspacedMixin):
route_prefix = '/v3/ws/<workspace_name>/'
trailing_slash = False


class UniqueCommentV3View(UniqueCommentView, PatchableWorkspacedMixin):
route_prefix = '/v3/ws/<workspace_name>/'
trailing_slash = False


CommentView.register(comment_api)
UniqueCommentView.register(comment_api)
# I'm Py3
CommentV3View.register(comment_api)
UniqueCommentV3View.register(comment_api)
14 changes: 12 additions & 2 deletions faraday/server/api/modules/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
AutoSchema,
ReadWriteWorkspacedView,
FilterSetMeta,
FilterAlchemyMixin, InvalidUsage)
FilterAlchemyMixin,
InvalidUsage,
PatchableWorkspacedMixin
)
from faraday.server.models import Credential, Host, Service, Workspace, db
from faraday.server.schemas import MutableField, SelfNestedField, MetadataSchema

Expand Down Expand Up @@ -78,6 +81,8 @@ def set_parent(self, data, **kwargs):
parent_class = Service
parent_field = 'service_id'
not_parent_field = 'host_id'
elif 'partial' in kwargs and kwargs['partial']:
return data
else:
raise ValidationError(
f'Unknown parent type: {parent_type}')
Expand Down Expand Up @@ -126,5 +131,10 @@ def _envelope_list(self, objects, pagination_metadata=None):
}


class CredentialV3View(CredentialView, PatchableWorkspacedMixin):
route_prefix = '/v3/ws/<workspace_name>/'
trailing_slash = False


CredentialView.register(credentials_api)
# I'm Py3
CredentialV3View.register(credentials_api)
Loading

0 comments on commit 1ebbd4e

Please sign in to comment.