mirror of
https://github.com/github/codeql.git
synced 2026-04-30 19:26:02 +02:00
Merge pull request #7016 from RasmusWL/django-rest-framework
Python: Model Django REST framework
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of HTTP requests and responses when using the Django REST Framework (`djangorestframework` PyPI package), which leads to additional remote flow sources.
|
||||
@@ -30,6 +30,7 @@ private import semmle.python.frameworks.Peewee
|
||||
private import semmle.python.frameworks.Psycopg2
|
||||
private import semmle.python.frameworks.Pydantic
|
||||
private import semmle.python.frameworks.PyMySQL
|
||||
private import semmle.python.frameworks.RestFramework
|
||||
private import semmle.python.frameworks.Rsa
|
||||
private import semmle.python.frameworks.RuamelYaml
|
||||
private import semmle.python.frameworks.Simplejson
|
||||
|
||||
@@ -17,10 +17,12 @@ private import semmle.python.frameworks.internal.SelfRefMixin
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for the `django` PyPI package.
|
||||
* See https://www.djangoproject.com/.
|
||||
*/
|
||||
private module Django {
|
||||
module Django {
|
||||
/** Provides models for the `django.views` module */
|
||||
module Views {
|
||||
/**
|
||||
@@ -367,6 +369,52 @@ private module Django {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `django.contrib.auth.models.User` class
|
||||
*
|
||||
* See https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#user-model.
|
||||
*/
|
||||
module User {
|
||||
/**
|
||||
* A source of instances of `django.contrib.auth.models.User`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `User::instance()` to get references to instances of `django.contrib.auth.models.User`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** Gets a reference to an instance of `django.contrib.auth.models.User`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `django.contrib.auth.models.User`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `django.contrib.auth.models.User`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "django.contrib.auth.models.User" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in ["username", "first_name", "last_name", "email"]
|
||||
}
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `django.core.files.uploadedfile.UploadedFile` class
|
||||
*
|
||||
@@ -466,10 +514,12 @@ private module Django {
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for the `django` PyPI package (that we are not quite ready to publicly expose yet).
|
||||
* See https://www.djangoproject.com/.
|
||||
*/
|
||||
private module PrivateDjango {
|
||||
module PrivateDjango {
|
||||
// ---------------------------------------------------------------------------
|
||||
// django
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -496,6 +546,7 @@ private module PrivateDjango {
|
||||
/** Gets a reference to the `django.db.connection` object. */
|
||||
API::Node connection() { result = db().getMember("connection") }
|
||||
|
||||
/** A `django.db.connection` is a PEP249 compliant DB connection. */
|
||||
class DjangoDbConnection extends PEP249::Connection::InstanceSource {
|
||||
DjangoDbConnection() { this = connection().getAUse() }
|
||||
}
|
||||
@@ -692,6 +743,7 @@ private module PrivateDjango {
|
||||
|
||||
/** Provides models for the `django.conf` module */
|
||||
module conf {
|
||||
/** Provides models for the `django.conf.urls` module */
|
||||
module conf_urls {
|
||||
// -------------------------------------------------------------------------
|
||||
// django.conf.urls
|
||||
@@ -890,6 +942,7 @@ private module PrivateDjango {
|
||||
* See https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpResponse.
|
||||
*/
|
||||
module HttpResponse {
|
||||
/** Gets a reference to the `django.http.response.HttpResponse` class. */
|
||||
API::Node baseClassRef() {
|
||||
result = response().getMember("HttpResponse")
|
||||
or
|
||||
@@ -897,7 +950,7 @@ private module PrivateDjango {
|
||||
result = http().getMember("HttpResponse")
|
||||
}
|
||||
|
||||
/** Gets a reference to the `django.http.response.HttpResponse` class. */
|
||||
/** Gets a reference to the `django.http.response.HttpResponse` class or any subclass. */
|
||||
API::Node classRef() { result = baseClassRef().getASubclass*() }
|
||||
|
||||
/**
|
||||
@@ -1893,14 +1946,11 @@ private module PrivateDjango {
|
||||
* with the django framework.
|
||||
*
|
||||
* Most functions take a django HttpRequest as a parameter (but not all).
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `DjangoRouteHandler::Range` instead.
|
||||
*/
|
||||
private class DjangoRouteHandler extends Function {
|
||||
DjangoRouteHandler() {
|
||||
exists(DjangoRouteSetup route | route.getViewArg() = poorMansFunctionTracker(this))
|
||||
or
|
||||
any(DjangoViewClass vc).getARequestHandler() = this
|
||||
}
|
||||
|
||||
class DjangoRouteHandler extends Function instanceof DjangoRouteHandler::Range {
|
||||
/**
|
||||
* Gets the index of the parameter where the first routed parameter can be passed --
|
||||
* that is, the one just after any possible `self` or HttpRequest parameters.
|
||||
@@ -1920,6 +1970,24 @@ private module PrivateDjango {
|
||||
Parameter getRequestParam() { result = this.getArg(this.getRequestParamIndex()) }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling new django route handlers. */
|
||||
module DjangoRouteHandler {
|
||||
/**
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `DjangoRouteHandler` instead.
|
||||
*/
|
||||
abstract class Range extends Function { }
|
||||
|
||||
/** Route handlers from normal usage of django. */
|
||||
private class StandardDjangoRouteHandlers extends Range {
|
||||
StandardDjangoRouteHandlers() {
|
||||
exists(DjangoRouteSetup route | route.getViewArg() = poorMansFunctionTracker(this))
|
||||
or
|
||||
any(DjangoViewClass vc).getARequestHandler() = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A method named `get_redirect_url` on a django view class.
|
||||
*
|
||||
@@ -1941,7 +2009,7 @@ private module PrivateDjango {
|
||||
}
|
||||
|
||||
/** A data-flow node that sets up a route on a server, using the django framework. */
|
||||
abstract private class DjangoRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CfgNode {
|
||||
abstract class DjangoRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CfgNode {
|
||||
/** Gets the data-flow node that is used as the argument for the view handler. */
|
||||
abstract DataFlow::Node getViewArg();
|
||||
|
||||
|
||||
369
python/ql/lib/semmle/python/frameworks/RestFramework.qll
Normal file
369
python/ql/lib/semmle/python/frameworks/RestFramework.qll
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `djangorestframework` PyPI package
|
||||
* (imported as `rest_framework`)
|
||||
*
|
||||
* See
|
||||
* - https://www.django-rest-framework.org/
|
||||
* - https://pypi.org/project/djangorestframework/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
private import semmle.python.frameworks.Django
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for the `djangorestframework` PyPI package
|
||||
* (imported as `rest_framework`)
|
||||
*
|
||||
* See
|
||||
* - https://www.django-rest-framework.org/
|
||||
* - https://pypi.org/project/djangorestframework/
|
||||
*/
|
||||
private module RestFramework {
|
||||
// ---------------------------------------------------------------------------
|
||||
// rest_framework.views.APIView handling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* An `API::Node` representing the `rest_framework.views.APIView` class or any subclass
|
||||
* that has explicitly been modeled in the CodeQL libraries.
|
||||
*/
|
||||
private class ModeledApiViewClasses extends Django::Views::View::ModeledSubclass {
|
||||
ModeledApiViewClasses() {
|
||||
this = API::moduleImport("rest_framework").getMember("views").getMember("APIView")
|
||||
or
|
||||
// imports generated by python/frameworks/internal/SubclassFinder.qll
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("authtoken")
|
||||
.getMember("views")
|
||||
.getMember("APIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("authtoken")
|
||||
.getMember("views")
|
||||
.getMember("ObtainAuthToken")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("decorators").getMember("APIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("CreateAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("DestroyAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("GenericAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("ListAPIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework").getMember("generics").getMember("ListCreateAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("RetrieveAPIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("generics")
|
||||
.getMember("RetrieveDestroyAPIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework").getMember("generics").getMember("RetrieveUpdateAPIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("generics")
|
||||
.getMember("RetrieveUpdateDestroyAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("UpdateAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("routers").getMember("APIRootView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("routers").getMember("SchemaView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("schemas")
|
||||
.getMember("views")
|
||||
.getMember("APIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("schemas")
|
||||
.getMember("views")
|
||||
.getMember("SchemaView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("GenericViewSet")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("ModelViewSet")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework").getMember("viewsets").getMember("ReadOnlyModelViewSet")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("ViewSet")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that has a super-type which is a rest_framework APIView class, therefore also
|
||||
* becoming a APIView class.
|
||||
*/
|
||||
class RestFrameworkApiViewClass extends PrivateDjango::DjangoViewClassFromSuperClass {
|
||||
RestFrameworkApiViewClass() {
|
||||
this.getABase() = any(ModeledApiViewClasses c).getASubclass*().getAUse().asExpr()
|
||||
}
|
||||
|
||||
override Function getARequestHandler() {
|
||||
result = super.getARequestHandler()
|
||||
or
|
||||
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
|
||||
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
|
||||
result = this.getAMethod() and
|
||||
result.getName() in [
|
||||
// these method names where found by looking through the APIView
|
||||
// implementation in
|
||||
// https://github.com/encode/django-rest-framework/blob/master/rest_framework/views.py#L104
|
||||
"initial", "http_method_not_allowed", "permission_denied", "throttled",
|
||||
"get_authenticate_header", "perform_content_negotiation", "perform_authentication",
|
||||
"check_permissions", "check_object_permissions", "check_throttles", "determine_version",
|
||||
"initialize_request", "finalize_response", "dispatch", "options"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// rest_framework.decorators.api_view handling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A function that is a request handler since it is decorated with `rest_framework.decorators.api_view`
|
||||
*/
|
||||
class RestFrameworkFunctionBasedView extends PrivateDjango::DjangoRouteHandler::Range {
|
||||
RestFrameworkFunctionBasedView() {
|
||||
this.getADecorator() =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("decorators")
|
||||
.getMember("api_view")
|
||||
.getACall()
|
||||
.asExpr()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensuring that all `RestFrameworkFunctionBasedView` are also marked as a
|
||||
* `HTTP::Server::RequestHandler`. We only need this for the ones that doesn't have a
|
||||
* known route setup.
|
||||
*/
|
||||
class RestFrameworkFunctionBasedViewWithoutKnownRoute extends HTTP::Server::RequestHandler::Range,
|
||||
PrivateDjango::DjangoRouteHandler instanceof RestFrameworkFunctionBasedView {
|
||||
RestFrameworkFunctionBasedViewWithoutKnownRoute() {
|
||||
not exists(PrivateDjango::DjangoRouteSetup setup | setup.getARequestHandler() = this)
|
||||
}
|
||||
|
||||
override Parameter getARoutedParameter() {
|
||||
// Since we don't know the URL pattern, we simply mark all parameters as a routed
|
||||
// parameter. This should give us more RemoteFlowSources but could also lead to
|
||||
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
|
||||
result in [this.getArg(_), this.getArgByName(_)] and
|
||||
not result = any(int i | i < this.getFirstPossibleRoutedParamIndex() | this.getArg(i))
|
||||
}
|
||||
|
||||
override string getFramework() { result = "Django (rest_framework)" }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// request modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A parameter that will receive a `rest_framework.request.Request` instance when a
|
||||
* request handler is invoked.
|
||||
*/
|
||||
private class RestFrameworkRequestHandlerRequestParam extends Request::InstanceSource,
|
||||
RemoteFlowSource::Range, DataFlow::ParameterNode {
|
||||
RestFrameworkRequestHandlerRequestParam() {
|
||||
// rest_framework.views.APIView subclass
|
||||
exists(RestFrameworkApiViewClass vc |
|
||||
this.getParameter() =
|
||||
vc.getARequestHandler().(PrivateDjango::DjangoRouteHandler).getRequestParam()
|
||||
)
|
||||
or
|
||||
// annotated with @api_view decorator
|
||||
exists(PrivateDjango::DjangoRouteHandler rh | rh instanceof RestFrameworkFunctionBasedView |
|
||||
this.getParameter() = rh.getRequestParam()
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "rest_framework.request.HttpRequest" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `rest_framework.request.Request` class
|
||||
*
|
||||
* See https://www.django-rest-framework.org/api-guide/requests/.
|
||||
*/
|
||||
module Request {
|
||||
/** Gets a reference to the `rest_framework.request.Request` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("rest_framework").getMember("request").getMember("Request")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `rest_framework.request.Request`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Request::instance()` to get references to instances of `rest_framework.request.Request`.
|
||||
*/
|
||||
abstract class InstanceSource extends PrivateDjango::django::http::request::HttpRequest::InstanceSource {
|
||||
}
|
||||
|
||||
/** A direct instantiation of `rest_framework.request.Request`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `rest_framework.request.Request`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `rest_framework.request.Request`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `rest_framework.request.Request`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "rest_framework.request.Request" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in ["data", "query_params", "user", "auth", "content_type", "stream"]
|
||||
}
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/** An attribute read that is a `MultiValueDict` instance. */
|
||||
private class MultiValueDictInstances extends Django::MultiValueDict::InstanceSource {
|
||||
MultiValueDictInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "query_params"
|
||||
}
|
||||
}
|
||||
|
||||
/** An attribute read that is a `User` instance. */
|
||||
private class UserInstances extends Django::User::InstanceSource {
|
||||
UserInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "user"
|
||||
}
|
||||
}
|
||||
|
||||
/** An attribute read that is a file-like instance. */
|
||||
private class FileLikeInstances extends Stdlib::FileLikeObject::InstanceSource {
|
||||
FileLikeInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "stream"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// response modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Provides models for the `rest_framework.response.Response` class
|
||||
*
|
||||
* See https://www.django-rest-framework.org/api-guide/responses/.
|
||||
*/
|
||||
module Response {
|
||||
/** Gets a reference to the `rest_framework.response.Response` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("rest_framework").getMember("response").getMember("Response")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `rest_framework.response.Response`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Response::instance()` to get references to instances of `rest_framework.response.Response`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `rest_framework.response.Response`. */
|
||||
private class ClassInstantiation extends PrivateDjango::django::http::response::HttpResponse::InstanceSource,
|
||||
DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
|
||||
override DataFlow::Node getBody() { result in [this.getArg(0), this.getArgByName("data")] }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() {
|
||||
result in [this.getArg(5), this.getArgByName("content_type")]
|
||||
}
|
||||
|
||||
override string getMimetypeDefault() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exception response modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Provides models for the `rest_framework.exceptions.APIException` class and subclasses
|
||||
*
|
||||
* See https://www.django-rest-framework.org/api-guide/exceptions/#api-reference
|
||||
*/
|
||||
module APIException {
|
||||
/** A direct instantiation of `rest_framework.exceptions.APIException` or subclass. */
|
||||
private class ClassInstantiation extends HTTP::Server::HttpResponse::Range,
|
||||
DataFlow::CallCfgNode {
|
||||
string className;
|
||||
|
||||
ClassInstantiation() {
|
||||
className in [
|
||||
"APIException", "ValidationError", "ParseError", "AuthenticationFailed",
|
||||
"NotAuthenticated", "PermissionDenied", "NotFound", "MethodNotAllowed", "NotAcceptable",
|
||||
"UnsupportedMediaType", "Throttled"
|
||||
] and
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("exceptions")
|
||||
.getMember(className)
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() {
|
||||
className in [
|
||||
"APIException", "ValidationError", "ParseError", "AuthenticationFailed",
|
||||
"NotAuthenticated", "PermissionDenied", "NotFound", "NotAcceptable"
|
||||
] and
|
||||
result = this.getArg(0)
|
||||
or
|
||||
className in ["MethodNotAllowed", "UnsupportedMediaType", "Throttled"] and
|
||||
result = this.getArg(1)
|
||||
or
|
||||
result = this.getArgByName("detail")
|
||||
}
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() { none() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Has predicates to help find subclasses in library code. Should only be used to aid in
|
||||
* the manual library modeling process,
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.filters.Tests
|
||||
|
||||
// very much inspired by the draft at https://github.com/github/codeql/pull/5632
|
||||
private module NotExposed {
|
||||
// Instructions:
|
||||
// This needs to be automated better, but for this prototype, here are some rough instructions:
|
||||
// 0) get a database of the library you are about to model
|
||||
// 1) fill out the `getAlreadyModeledClass` body below
|
||||
// 2) quick-eval the `quickEvalMe` predicate below, and copy the output to your modeling predicate
|
||||
class MySpec extends FindSubclassesSpec {
|
||||
MySpec() { this = "MySpec" }
|
||||
|
||||
override API::Node getAlreadyModeledClass() {
|
||||
// FILL ME OUT ! (but don't commit with any changes)
|
||||
none()
|
||||
// for example
|
||||
// result = API::moduleImport("rest_framework").getMember("views").getMember("APIView")
|
||||
}
|
||||
}
|
||||
|
||||
predicate quickEvalMe(string newImport) {
|
||||
newImport =
|
||||
"// imports generated by python/frameworks/internal/SubclassFinder.qll\n" + "this = API::" +
|
||||
concat(string newModelFullyQualified |
|
||||
newModel(any(MySpec spec), newModelFullyQualified, _, _, _)
|
||||
|
|
||||
fullyQualifiedToAPIGraphPath(newModelFullyQualified), " or this = API::"
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Implementation below
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// We are looking to find all subclassed of the already modelled classes, and ideally
|
||||
// we would identify an `API::Node` for each (then `toString` would give the API
|
||||
// path).
|
||||
//
|
||||
// An inherent problem with API graphs is that there doesn't need to exist a result
|
||||
// for the API graph path that we want to add to our modeling (the path to the new
|
||||
// subclass). As an example, the following query has no results when evaluated against
|
||||
// a django/django DB.
|
||||
//
|
||||
// select API::moduleImport("django") .getMember("contrib") .getMember("admin")
|
||||
// .getMember("views") .getMember("main") .getMember("ChangeListSearchForm")
|
||||
//
|
||||
//
|
||||
// Since it is a Form subclass that we would want to capture for our Django modeling,
|
||||
// we want to extend our modeling (that is written in a qll file) with exactly that
|
||||
// piece of code, but since the API::Node doesn't exist, we can't select that from a
|
||||
// predicate and print its path. We need a different approach, and for that we use
|
||||
// fully qualified names to capture new classes/new aliases, and transform these into
|
||||
// API paths (to be included in the modeling that is inserted into the `.qll` files),
|
||||
// see `fullyQualifiedToAPIGraphPath`.
|
||||
//
|
||||
// NOTE: this implementation was originally created to help with automatically
|
||||
// modeling packages in mind, and has been adjusted to help with manual library
|
||||
// modeling. See https://github.com/github/codeql/pull/5632 for more discussion.
|
||||
//
|
||||
//
|
||||
bindingset[fullyQaulified]
|
||||
string fullyQualifiedToAPIGraphPath(string fullyQaulified) {
|
||||
result = "moduleImport(\"" + fullyQaulified.replaceAll(".", "\").getMember(\"") + "\")"
|
||||
}
|
||||
|
||||
bindingset[this]
|
||||
abstract class FindSubclassesSpec extends string {
|
||||
abstract API::Node getAlreadyModeledClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `newModelFullyQualified` describes either a new subclass, or a new alias, belonging to `spec` that we should include in our automated modeling.
|
||||
* This new element is defined by `ast`, which is defined at `loc` in the module `mod`.
|
||||
*/
|
||||
query predicate newModel(
|
||||
FindSubclassesSpec spec, string newModelFullyQualified, AstNode ast, Module mod, Location loc
|
||||
) {
|
||||
(
|
||||
newSubclass(spec, newModelFullyQualified, ast, mod, loc)
|
||||
or
|
||||
newDirectAlias(spec, newModelFullyQualified, ast, mod, loc)
|
||||
or
|
||||
newImportStar(spec, newModelFullyQualified, ast, mod, _, _, loc)
|
||||
)
|
||||
}
|
||||
|
||||
API::Node newOrExistingModeling(FindSubclassesSpec spec) {
|
||||
result = spec.getAlreadyModeledClass()
|
||||
or
|
||||
exists(string newSubclassName |
|
||||
newModel(spec, newSubclassName, _, _, _) and
|
||||
result.getPath() = fullyQualifiedToAPIGraphPath(newSubclassName)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[fullyQualifiedName]
|
||||
predicate alreadyModeled(FindSubclassesSpec spec, string fullyQualifiedName) {
|
||||
fullyQualifiedToAPIGraphPath(fullyQualifiedName) = spec.getAlreadyModeledClass().getPath()
|
||||
}
|
||||
|
||||
predicate isNonTestProjectCode(AstNode ast) {
|
||||
not ast.getScope*() instanceof TestScope and
|
||||
not ast.getLocation().getFile().getRelativePath().matches("tests/%") and
|
||||
exists(ast.getLocation().getFile().getRelativePath())
|
||||
}
|
||||
|
||||
predicate hasAllStatement(Module mod) {
|
||||
exists(AssignStmt a, GlobalVariable all |
|
||||
a.defines(all) and
|
||||
a.getScope() = mod and
|
||||
all.getId() = "__all__"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `newAliasFullyQualified` describes new alias originating from the import
|
||||
* `from <module> import <member> [as <new-name>]`, where `<module>.<member>` belongs to
|
||||
* `spec`.
|
||||
* So if this import happened in module `foo.bar`, `newAliasFullyQualified` would be
|
||||
* `foo.bar.<member>` (or `foo.bar.<new-name>`).
|
||||
*
|
||||
* Note that this predicate currently respects `__all__` in sort of a backwards fashion.
|
||||
* - if `__all__` is defined in module `foo.bar`, we only allow new aliases where the member name is also in `__all__`. (this doesn't map 100% to the semantics of imports though)
|
||||
* - If `__all__` is not defined we don't impose any limitations.
|
||||
*
|
||||
* Also note that we don't currently consider deleting module-attributes at all, so in the code snippet below, we would consider that `my_module.foo` is a
|
||||
* reference to `django.foo`, although `my_module.foo` isn't even available at runtime. (there currently also isn't any code to discover that `my_module.bar`
|
||||
* is an alias to `django.foo`)
|
||||
* ```py
|
||||
* # module my_module
|
||||
* from django import foo
|
||||
* bar = foo
|
||||
* del foo
|
||||
* ```
|
||||
*/
|
||||
predicate newDirectAlias(
|
||||
FindSubclassesSpec spec, string newAliasFullyQualified, ImportMember importMember, Module mod,
|
||||
Location loc
|
||||
) {
|
||||
importMember = newOrExistingModeling(spec).getAUse().asExpr() and
|
||||
importMember.getScope() = mod and
|
||||
loc = importMember.getLocation() and
|
||||
(
|
||||
mod.isPackageInit() and
|
||||
newAliasFullyQualified = mod.getPackageName() + "." + importMember.getName()
|
||||
or
|
||||
not mod.isPackageInit() and
|
||||
newAliasFullyQualified = mod.getName() + "." + importMember.getName()
|
||||
) and
|
||||
(
|
||||
not hasAllStatement(mod)
|
||||
or
|
||||
mod.declaredInAll(importMember.getName())
|
||||
) and
|
||||
not alreadyModeled(spec, newAliasFullyQualified) and
|
||||
isNonTestProjectCode(importMember)
|
||||
}
|
||||
|
||||
/** same as `newDirectAlias` predicate, but handling `from <module> import *`, considering all `<member>`, where `<module>.<member>` belongs to `spec`. */
|
||||
predicate newImportStar(
|
||||
FindSubclassesSpec spec, string newAliasFullyQualified, ImportStar importStar, Module mod,
|
||||
API::Node relevantClass, string relevantName, Location loc
|
||||
) {
|
||||
relevantClass = newOrExistingModeling(spec) and
|
||||
loc = importStar.getLocation() and
|
||||
importStar.getScope() = mod and
|
||||
// WHAT A HACK :D :D
|
||||
relevantClass.getPath() =
|
||||
relevantClass.getAPredecessor().getPath() + ".getMember(\"" + relevantName + "\")" and
|
||||
relevantClass.getAPredecessor().getAUse().asExpr() = importStar.getModule() and
|
||||
(
|
||||
mod.isPackageInit() and
|
||||
newAliasFullyQualified = mod.getPackageName() + "." + relevantName
|
||||
or
|
||||
not mod.isPackageInit() and
|
||||
newAliasFullyQualified = mod.getName() + "." + relevantName
|
||||
) and
|
||||
(
|
||||
not hasAllStatement(mod)
|
||||
or
|
||||
mod.declaredInAll(relevantName)
|
||||
) and
|
||||
not alreadyModeled(spec, newAliasFullyQualified) and
|
||||
isNonTestProjectCode(importStar)
|
||||
}
|
||||
|
||||
/** Holds if `classExpr` defines a new subclass that belongs to `spec`, which has the fully qualified name `newSubclassQualified`. */
|
||||
predicate newSubclass(
|
||||
FindSubclassesSpec spec, string newSubclassQualified, ClassExpr classExpr, Module mod,
|
||||
Location loc
|
||||
) {
|
||||
classExpr = newOrExistingModeling(spec).getASubclass*().getAUse().asExpr() and
|
||||
classExpr.getScope() = mod and
|
||||
newSubclassQualified = mod.getName() + "." + classExpr.getName() and
|
||||
loc = classExpr.getLocation() and
|
||||
not alreadyModeled(spec, newSubclassQualified) and
|
||||
isNonTestProjectCode(classExpr)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
private module PrivateDjango {
|
||||
private module ExperimentalPrivateDjango {
|
||||
private module django {
|
||||
API::Node http() { result = API::moduleImport("django").getMember("http") }
|
||||
|
||||
|
||||
1
python/ql/test/library-tests/frameworks/rest_framework/.gitignore
vendored
Normal file
1
python/ql/test/library-tests/frameworks/rest_framework/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
db.sqlite3
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
@@ -0,0 +1,3 @@
|
||||
argumentToEnsureNotTaintedNotMarkedAsSpurious
|
||||
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
|
||||
failures
|
||||
@@ -0,0 +1 @@
|
||||
import experimental.meta.InlineTaintTest
|
||||
@@ -0,0 +1,23 @@
|
||||
See README for `django-v2-v3` which described how the project was set up.
|
||||
|
||||
Since this test project uses models (and a DB), you generally need to run there 3 commands:
|
||||
|
||||
```
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Then visit http://127.0.0.1:8000/
|
||||
|
||||
# References
|
||||
|
||||
- https://www.django-rest-framework.org/tutorial/quickstart/
|
||||
|
||||
# Editing data
|
||||
|
||||
To edit data you should add an admin user (will prompt for password)
|
||||
|
||||
```
|
||||
python manage.py createsuperuser --email admin@example.com --username admin
|
||||
```
|
||||
22
python/ql/test/library-tests/frameworks/rest_framework/manage.py
Executable file
22
python/ql/test/library-tests/frameworks/rest_framework/manage.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproj.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
@api_view()
|
||||
def normal_response(request): # $ requestHandler
|
||||
# has no pre-defined content type, since that will be negotiated
|
||||
# see https://www.django-rest-framework.org/api-guide/responses/
|
||||
data = "data"
|
||||
resp = Response(data) # $ HttpResponse responseBody=data
|
||||
return resp
|
||||
|
||||
@api_view()
|
||||
def plain_text_response(request): # $ requestHandler
|
||||
# this response is not the standard way to use the Djagno REST framework, but it
|
||||
# certainly is possible -- notice that the response contains double quotes
|
||||
data = 'this response will contain double quotes since it was a string'
|
||||
resp = Response(data, None, None, None, None, "text/plain") # $ HttpResponse mimetype=text/plain responseBody=data
|
||||
resp = Response(data=data, content_type="text/plain") # $ HttpResponse mimetype=text/plain responseBody=data
|
||||
return resp
|
||||
|
||||
################################################################################
|
||||
# Cookies
|
||||
################################################################################
|
||||
|
||||
@api_view
|
||||
def setting_cookie(request):
|
||||
resp = Response() # $ HttpResponse
|
||||
resp.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
resp.set_cookie(key="key4", value="value") # $ CookieWrite CookieName="key4" CookieValue="value"
|
||||
resp.headers["Set-Cookie"] = "key2=value2" # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
|
||||
resp.cookies["key3"] = "value3" # $ CookieWrite CookieName="key3" CookieValue="value3"
|
||||
resp.delete_cookie("key4") # $ CookieWrite CookieName="key4"
|
||||
resp.delete_cookie(key="key4") # $ CookieWrite CookieName="key4"
|
||||
return resp
|
||||
|
||||
################################################################################
|
||||
# Exceptions
|
||||
################################################################################
|
||||
|
||||
# see https://www.django-rest-framework.org/api-guide/exceptions/
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
def exception_test(request): # $ requestHandler
|
||||
data = "exception details"
|
||||
# note: `code details` not exposed by default
|
||||
code = "code details"
|
||||
e1 = APIException(data, code) # $ HttpResponse responseBody=data
|
||||
e2 = APIException(detail=data, code=code) # $ HttpResponse responseBody=data
|
||||
raise e2
|
||||
@@ -0,0 +1,131 @@
|
||||
from rest_framework.decorators import api_view, parser_classes
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import JSONParser
|
||||
|
||||
from django.urls import path
|
||||
|
||||
ensure_tainted = ensure_not_tainted = print
|
||||
|
||||
# function based view
|
||||
# see https://www.django-rest-framework.org/api-guide/views/#function-based-views
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@parser_classes([JSONParser])
|
||||
def test_taint(request: Request, routed_param): # $ requestHandler routedParameter=routed_param
|
||||
ensure_tainted(routed_param) # $ tainted
|
||||
|
||||
ensure_tainted(request) # $ tainted
|
||||
|
||||
# Has all the standard attributes of a django HttpRequest
|
||||
# see https://github.com/encode/django-rest-framework/blob/00cd4ef864a8bf6d6c90819a983017070f9f08a5/rest_framework/request.py#L410-L418
|
||||
ensure_tainted(request.resolver_match.args) # $ tainted
|
||||
|
||||
# special new attributes added, see https://www.django-rest-framework.org/api-guide/requests/
|
||||
ensure_tainted(
|
||||
request.data, # $ tainted
|
||||
request.data["key"], # $ tainted
|
||||
|
||||
# alias for .GET
|
||||
request.query_params, # $ tainted
|
||||
request.query_params["key"], # $ tainted
|
||||
request.query_params.get("key"), # $ tainted
|
||||
request.query_params.getlist("key"), # $ tainted
|
||||
request.query_params.getlist("key")[0], # $ tainted
|
||||
request.query_params.pop("key"), # $ tainted
|
||||
request.query_params.pop("key")[0], # $ tainted
|
||||
|
||||
# see more detailed tests of `request.user` below
|
||||
request.user, # $ tainted
|
||||
|
||||
request.auth, # $ tainted
|
||||
|
||||
# seems much more likely attack vector than .method, so included
|
||||
request.content_type, # $ tainted
|
||||
|
||||
# file-like
|
||||
request.stream, # $ tainted
|
||||
request.stream.read(), # $ tainted
|
||||
)
|
||||
|
||||
ensure_not_tainted(
|
||||
# although these could technically be user-controlled, it seems more likely to lead to FPs than interesting results.
|
||||
request.accepted_media_type,
|
||||
|
||||
# In normal Django, if you disable CSRF middleware, you're allowed to use custom
|
||||
# HTTP methods, like `curl -X FOO <url>`.
|
||||
# However, with Django REST framework, doing that will yield:
|
||||
# `{"detail":"Method \"FOO\" not allowed."}`
|
||||
#
|
||||
# In the end, since we model a Django REST framework request entirely as a
|
||||
# extension of a Django request, we're not easily able to remove the taint from
|
||||
# `.method`.
|
||||
request.method, # $ SPURIOUS: tainted
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# request.user
|
||||
# --------------------------------------------------------------------------
|
||||
#
|
||||
# This will normally be an instance of django.contrib.auth.models.User
|
||||
# (authenticated) so we assume that normally user-controlled fields such as
|
||||
# username/email is user-controlled, but that password isn't (since it's a hash).
|
||||
# see https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#fields
|
||||
ensure_tainted(
|
||||
request.user.username, # $ tainted
|
||||
request.user.first_name, # $ tainted
|
||||
request.user.last_name, # $ tainted
|
||||
request.user.email, # $ tainted
|
||||
)
|
||||
ensure_not_tainted(request.user.password)
|
||||
|
||||
return Response("ok") # $ HttpResponse responseBody="ok"
|
||||
|
||||
|
||||
# class based view
|
||||
# see https://www.django-rest-framework.org/api-guide/views/#class-based-views
|
||||
|
||||
|
||||
class MyClass(APIView):
|
||||
def initial(self, request, *args, **kwargs): # $ requestHandler
|
||||
# this method will be called before processing any request
|
||||
ensure_tainted(request) # $ tainted
|
||||
|
||||
def get(self, request: Request, routed_param): # $ requestHandler routedParameter=routed_param
|
||||
ensure_tainted(routed_param) # $ tainted
|
||||
|
||||
# request taint is the same as in function_based_view above
|
||||
ensure_tainted(
|
||||
request, # $ tainted
|
||||
request.data # $ tainted
|
||||
)
|
||||
|
||||
# same as for standard Django view
|
||||
ensure_tainted(self.args, self.kwargs) # $ tainted
|
||||
|
||||
return Response("ok") # $ HttpResponse responseBody="ok"
|
||||
|
||||
|
||||
|
||||
# fake setup, you can't actually run this
|
||||
urlpatterns = [
|
||||
path("test-taint/<routed_param>", test_taint), # $ routeSetup="test-taint/<routed_param>"
|
||||
path("ClassView/<routed_param>", MyClass.as_view()), # $ routeSetup="ClassView/<routed_param>"
|
||||
]
|
||||
|
||||
# tests with no route-setup, but we can still tell that these are using Django REST
|
||||
# framework
|
||||
|
||||
@api_view(["POST"])
|
||||
def function_based_no_route(request: Request, possible_routed_param): # $ requestHandler routedParameter=possible_routed_param
|
||||
ensure_tainted(
|
||||
request, # $ tainted
|
||||
possible_routed_param, # $ tainted
|
||||
)
|
||||
|
||||
|
||||
class ClassBasedNoRoute(APIView):
|
||||
def get(self, request: Request, possible_routed_param): # $ requestHandler routedParameter=possible_routed_param
|
||||
ensure_tainted(request, possible_routed_param) # $ tainted
|
||||
@@ -0,0 +1,8 @@
|
||||
from .models import Foo, Bar
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
||||
admin.site.register(Foo)
|
||||
admin.site.register(Bar)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TestappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'testapp'
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.2.8 on 2021-10-27 11:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Foo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('field_not_displayed', models.IntegerField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Bar',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('n', models.IntegerField()),
|
||||
('foo', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='testapp.foo')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.2.8 on 2021-10-27 12:06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def add_dummy_data(apps, schema_editor):
|
||||
Foo = apps.get_model("testapp", "Foo")
|
||||
Bar = apps.get_model("testapp", "Bar")
|
||||
|
||||
f1 = Foo(title="example 1", field_not_displayed=10)
|
||||
f1.save()
|
||||
f2 = Foo(title="example 2", field_not_displayed=20)
|
||||
f2.save()
|
||||
|
||||
b1 = Bar(n=42, foo=f1)
|
||||
b1.save()
|
||||
b2 = Bar(n=43, foo=f1)
|
||||
b2.save()
|
||||
b3 = Bar(n=1000, foo=f2)
|
||||
b3.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('testapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_dummy_data),
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Foo(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
field_not_displayed = models.IntegerField()
|
||||
|
||||
|
||||
class Bar(models.Model):
|
||||
n = models.IntegerField()
|
||||
foo = models.ForeignKey(Foo, on_delete=models.PROTECT)
|
||||
@@ -0,0 +1,14 @@
|
||||
from .models import Foo, Bar
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class FooSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Foo
|
||||
fields = ["title"]
|
||||
|
||||
|
||||
class BarSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Bar
|
||||
fields = ["n", "foo"]
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,18 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r"foos", views.FooViewSet)
|
||||
router.register(r"bars", views.BarViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
|
||||
path("class-based-view/", views.MyClass.as_view()), # $routeSetup="lcass-based-view/"
|
||||
path("function-based-view/", views.function_based_view), # $routeSetup="function-based-view/"
|
||||
path("cookie-test/", views.cookie_test), # $routeSetup="function-based-view/"
|
||||
path("exception-test/", views.exception_test), # $routeSetup="exception-test/"
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
from .models import Foo, Bar
|
||||
from .serializers import FooSerializer, BarSerializer
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
# Viewsets
|
||||
# see https://www.django-rest-framework.org/tutorial/quickstart/
|
||||
class FooViewSet(viewsets.ModelViewSet):
|
||||
queryset = Foo.objects.all()
|
||||
serializer_class = FooSerializer
|
||||
|
||||
|
||||
class BarViewSet(viewsets.ModelViewSet):
|
||||
queryset = Bar.objects.all()
|
||||
serializer_class = BarSerializer
|
||||
|
||||
# class based view
|
||||
# see https://www.django-rest-framework.org/api-guide/views/#class-based-views
|
||||
|
||||
class MyClass(APIView):
|
||||
def initial(self, request, *args, **kwargs):
|
||||
# this method will be called before processing any request
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def get(self, request):
|
||||
return Response("GET request")
|
||||
|
||||
def post(self, request):
|
||||
return Response("POST request")
|
||||
|
||||
|
||||
# function based view
|
||||
# see https://www.django-rest-framework.org/api-guide/views/#function-based-views
|
||||
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
def function_based_view(request: Request):
|
||||
return Response({"message": "Hello, world!"})
|
||||
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
def cookie_test(request: Request):
|
||||
resp = Response("wat")
|
||||
resp.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
resp.set_cookie(key="key4", value="value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
resp.headers["Set-Cookie"] = "key2=value2" # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
|
||||
resp.cookies["key3"] = "value3" # $ CookieWrite CookieName="key3" CookieValue="value3"
|
||||
return resp
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
def exception_test(request: Request):
|
||||
# see https://www.django-rest-framework.org/api-guide/exceptions/
|
||||
# note: `code details` not exposed by default
|
||||
raise APIException("exception details", "code details")
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for testproj project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproj.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Django settings for testproj project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.2.8.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent # $ getAPathArgument=Path(..)
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-hqg4$wqk3894#_4p$ibwzpg5+&dvx)%6q45v0yq=-43c886(($'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'testapp.apps.TestappConfig',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'testproj.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'testproj.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
@@ -0,0 +1,22 @@
|
||||
"""testproj URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls), # $ routeSetup="admin/"
|
||||
path("", include("testapp.urls")), # $routeSetup=""
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for testproj project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproj.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
Reference in New Issue
Block a user