CouchDB write/read only (no edit) user - python

Toolchain/frameworks
I'm using django==2.1.3 and python-cloudant==2.1.3 and running CouchDB ver. 2.2.0, and pretty much doing all of my setup/configuration through Fauxton. I like to think that I know my way around python/django in general, and I'm testing this approach in a small little project to see how it works
Problem Description
Suppose I have a fairly simple CRUD application with just 1 model:
class Asset(models.Model):
asset_id = models.CharField(max_length=32)
asset_name = models.CharField(max_length=32)
and I have a view that I use to create the asset
class CreateAssetView(views.View):
def get(self, request, *args, **kwargs):
#some code here
def post(self, request, *args, **kwargs):
#some code here|
#log request data into database
client = CouchDB('myusername', 'mypassword', url='http://127.0.0.1:5984', connect=True)
db = client['assets']
log_data = {'view_name': self.view_name, 'post_data': post_data,'user': request.user.username,
'time': str(timezone.now())}
db.create_document(log_data)
return render(...)
I understand that I should be doing the logging portion using a middleware (which I plan to) and probably just use django's CreateView in that case, I'm doing this approach for now just during early development.
What I'm having a problem wrapping my head around is creating a user with myusername and mypassword that has the permissions to:
Write new documents
Read old documents
not edit already created documents
I could even settle for 1 and 3 only (and only use admin to read). I spent a little bit of time playing around with Fauxton's interface for permissions, but I can only basically create a user and assign a role (couldn't even get to assigning a password :/)
clarification
The Asset is not a CouchDB document, that's a normal SQL model, I only want to dump the logs with post data to CouchDB
Any help/gudiance/documentation pointers would be really appreciated

Overview
Couchdb has one overriding level of administrator setup in configuration instead of setup in the _users database and assigned the _admin permission to prevent any possibility of being locked out.
Each individual database then has a rough level security policy of 2 levels:
admins
members
specified via:
names
roles
making 4 fields.
These levels control access slightly differently for the 2 types of documents a db can contain:
id: _design/* - Design documents can contain functions that will be executed in some context
id: other - Normal documents are just normal data
Both levels of database access have read access to all documents in the database, but admins have write access to _design documents. Write access to normal documents is usually given to all users granted any access to the db, but can be limited by validate design documents.
To Summarize
The process for setting a unique security policy up is:
Experience the provided validate design document as a consumer while setting up _users.
Setup a new database and its basic security giving your users member access.
Add a design doc to the new database with a validate function that restricts member write access.
1 Setting up _users entries
Add a role to a user
As an admin add role: ["logger"] to a user's doc and save it, note that this must be done by admin due to this part of the default _users design document:
// DB: _users doc: _design/_auth
function(newDoc, oldDoc, userCtx, secObj) {
..
if (oldRoles.length !== newRoles.length) {
throw({forbidden: 'Only _admin may edit roles'});
}
Change the password for the user.
Either the admin or the user can change their password by setting password:"mynewpassword" in their document (which couchdb will transform into a hashed/salted password during the save process). This works for a user since they can add/modify fields aside from their name and roles, as long as a user is editing their own doc:
// DB: _users doc: _design/_auth
function(newDoc, oldDoc, userCtx, secObj) {
..
if (userCtx.name !== newDoc.name) {
throw({
forbidden: 'You may only update your own user document.'
});
}
// then checks that they don't modify roles
You could repeat this process with a user you assign an adminlogger role to create a delegated administrator that you assign permissions to can reconfigure a database or you can continue to use the couchdb admin with its _admin role for all administration.
2 Setup a new database and its basic security
Create a db named logger
assign the logger a security policy:
{
"admins": {
"names": [
],
"roles": [
"adminlogger"
]
},
"members": {
"names": [
],
"roles": [
"logger"
]
}
}
3. Create a new validate design document in the new db
As either _admin user or a user with the adminlogger role create a new validate design doc by copying the _users design document, removing the _rev and modifying the function:
// DB: logger doc: _design/auth
function(newDoc, oldDoc, userCtx, secObj) {
// Don't let non-admins write a pre-existing document:
if (!is_server_or_database_admin()) {
if (!!oldDoc) {
throw({
forbidden: 'You may not update existing documents.'
});
}
}
// Where the function to define admins can be copied verbatim from the doc:
var is_server_or_database_admin = function(userCtx, secObj) {
// see if the user is a server admin
if(userCtx.roles.indexOf('_admin') !== -1) {
return true; // a server admin
}
// see if the user a database admin specified by name
if(secObj && secObj.admins && secObj.admins.names) {
if(secObj.admins.names.indexOf(userCtx.name) !== -1) {
return true; // database admin
}
}
// see if the user a database admin specified by role
if(secObj && secObj.admins && secObj.admins.roles) {
var db_roles = secObj.admins.roles;
for(var idx = 0; idx < userCtx.roles.length; idx++) {
var user_role = userCtx.roles[idx];
if(db_roles.indexOf(user_role) !== -1) {
return true; // role matches!
}
}
}
return false; // default to no admin
}
}
If you followed these steps then the user you gave the logger role in step 1 can run your code to write new documents only in logger database configured in steps 2 and 3.

Related

Implementing Vue RESTful API calls in Django within Django Templates with Session Authentication

I have a Django project that requires page refreshes any time I want to update the content. Currently it uses the built in Django Class Based Views, which in turn use Templates.
I want to start implementing some javascript calls to refresh tables/forms/modal windows with new data, mostly in response to button clicks. I'm trying to use Vue to accomplish this, and I have created a Django REST Framework backend to facilitate this.
I got a simple 'Hello world' vue class working where the data is hard coded into the data field of the Vue class. I can't seem to make the jump to getting data from the API though. I am getting an Unauthorized response. I am using vue-resource for the HTTP API call.
I have unit tests where I call the API from the DRF APITestCase using the self.client.get('api/path') and they work as expected (unauthorized when there is no authenticated user attached to request, authorized when there is).
I have debugged into the DRF Permission class to see why the request is being refused and it is because there is no authenticated User attached to the request.
I have added SessionAuthentication to the DEFAULT_AUTHENTICATION_CLASSES in settings.
My question is, how do I add an authenticated user to the request so that when the Vue method is called from within my webapp the API request will be authorized?
I'm not sure if this is complicating matters but I am using a custom user model within Django for authentication.
I am hoping to start off by implementing a few Vue controls throughout my website, for instance the tables and forms mentioned. I don't want to turn this into a single page app. I would like to continue using the Django views for user authentication.
My Vue code looks like so;
new Vue({
delimiters: ['${', '}$'],
el: '.events-table',
data: {
message: 'Hello Vue!',
demo: [
{ id: 5 },
{ id: 2 },
{ id: 3 },
],
events: [],
},
http: {
root: 'http://localhost:8000',
},
methods: {
getEvents: function () {
this.$http.get('api/eventlog/events/?format=json').then(
function (data, status, request) {
if (status == 200) {
this.events = data.body.results;
}
}
)
}
},
mounted: function () {
this.getEvents();
}
})
I changed the http property like so
http: {
root: window.location.origin,
},
and now it seems to recognise that the request is coming from an authenticated session.

Django-python3-ldap - only certain active directory user groups can login?

I'm using the django-python3-ldap module located here
https://github.com/etianen/django-python3-ldap#available-settings
I only want users who are a member of certain groups to be able to login, so I have created a function as suggested
def app_users(ldap_fields):
# Add in simple filters.
ldap_fields["memberOf"] = "App_Admin"
# Call the base format callable.
search_filters = format_search_filters(ldap_fields)
# Advanced: apply custom LDAP filter logic.
search_filters.append("(|(memberOf=App_Admin)(memberOf=App_ITUser)(memberOf=App_NetworkUser))")
# All done!
return search_filters
however this returns the below then debugging
LDAP connect succeeded
LDAP user attributes empty
I think its something to do with with the foo sample but I'm not sure how to fix it
Thanks
I can't remember what I did its so long ago now, but this is my LDAP config and I am using the ldap module for python 3
I didn't write this code I will have obtained it from somewhere, but I'm not sure where.
import ldap
# The URL of the LDAP server.
LDAP_AUTH_URL = 'ldap://domain.com:389'
# Initiate TLS on connection.
LDAP_AUTH_USE_TLS = False
# The LDAP search base for looking up users.
LDAP_AUTH_SEARCH_BASE = 'DC=domain,DC=com'
# The LDAP class that represents a user.
LDAP_AUTH_OBJECT_CLASS = 'organizationalPerson'
# User model fields mapped to the LDAP
# attributes that represent them.
LDAP_AUTH_USER_FIELDS = {
'username': 'sAMAccountName',
'first_name': 'givenName',
'last_name': 'sn',
'email': 'mail',
}
# A tuple of django model fields used to uniquely identify a user.
LDAP_AUTH_USER_LOOKUP_FIELDS = ('username',)
# Path to a callable that takes a dict of {model_field_name: value},
# returning a dict of clean model data.
# Use this to customize how data loaded from LDAP is saved to the User model.
LDAP_AUTH_CLEAN_USER_DATA = 'django_python3_ldap.utils.clean_user_data'
# Path to a callable that takes a user model and a dict of {ldap_field_name: [value]},
# and saves any additional user relationships based on the LDAP data.
# Use this to customize how data loaded from LDAP is saved to User model relations.
# For customizing non-related User model fields, use LDAP_AUTH_CLEAN_USER_DATA.
LDAP_AUTH_SYNC_USER_RELATIONS = 'django_python3_ldap.utils.sync_user_relations'
# Path to a callable that takes a dict of {ldap_field_name: value},
# returning a list of [ldap_search_filter]. The search filters will then be AND'd
# together when creating the final search filter.
LDAP_AUTH_FORMAT_SEARCH_FILTERS = 'django_python3_ldap.utils.format_search_filters'
#LDAP_AUTH_FORMAT_SEARCH_FILTERS = 'itapp.ldap_filters.app_users'
# Path to a callable that takes a dict of {model_field_name: value}, and returns
# a string of the username to bind to the LDAP server.
# Use this to support different types of LDAP server.
LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_active_directory"
# Sets the login domain for Active Directory users.
LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = 'DOMAIN'
# The LDAP username and password of a user for querying the LDAP database for user
# details. If None, then the authenticated user will be used for querying, and
# the `ldap_sync_users` command will perform an anonymous query.
LDAP_AUTH_CONNECTION_USERNAME = None
LDAP_AUTH_CONNECTION_PASSWORD = None
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"django_python3_ldap": {
"handlers": ["console"],
"level": "INFO",
},
},
}
AUTHENTICATION_BACKENDS = (
'django_python3_ldap.auth.LDAPBackend',
'django.contrib.auth.backends.ModelBackend', #Comment out to prevent authentication from DB
)

How to create new user accounts in python eve api secured with User-Restricted Resource Access

I first created a web api using the python-eve framework, without authentication or user accounts, and it worked great! I am now trying to add authentication and user accounts, and am having some difficulty. I want to use User-Restricted Resource Access, but how can a user create a new user account, if the resources are restricted? What am I missing?
I have been trying to follow the Restful Account Management Tutorial and the introduction to Authentication and Authorization on python-eve.org, and I've searched stackoverflow, including this answer here.
Here is my implementation:
run.py
import os.path
from eve import Eve
import my_auth
from flask.ext.bootstrap import Bootstrap
from eve_docs import eve_docs
app = Eve(auth=my_auth.BCryptAuth, settings = 'deployed_settings.py')
app.on_insert_accounts += my_auth.create_user
Bootstrap(app)
app.register_blueprint(eve_docs, url_prefix='/docs')
if __name__ == '__main__':
app.run()
my_auth.py
import bcrypt
from eve import Eve
from eve.auth import BasicAuth
class BCryptAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
# use Eve's own db driver; no additional connections/resources are used
accounts = Eve.app.data.driver.db['accounts']
account = accounts.find_one({'username': username})
if account and 'user_id' in account:
self.set_request_auth_value(account['user_id'])
return account and bcrypt.hashpw(
password.encode('utf-8'),account['salt'].encode('utf-8')) == account['password']
def create_user(documents):
for document in documents:
document['salt'] = bcrypt.gensalt().encode('utf-8')
password = document['password'].encode('utf-8')
document['password'] = bcrypt.hashpw(password, document['salt'])
deployed_settings.py
# We are running on a local machine, so just use the local mongod instance.
# Note that MONGO_HOST and MONGO_PORT could very well be left
# out as they already default to a bare bones local 'mongod' instance.
MONGO_HOST = 'localhost'
MONGO_PORT = 27017
MONGO_USERNAME = ''
MONGO_PASSWORD = ''
MONGO_DBNAME = 'practice'
# Name of the field used to store the owner of each document
AUTH_FIELD = 'user_id'
# Enable reads (GET), inserts (POST) and DELETE for resources/collections
# (if you omit this line, the API will default to ['GET'] and provide
# read-only access to the endpoint).
RESOURCE_METHODS = ['GET', 'POST', 'DELETE']
# Enable reads (GET), edits (PATCH), replacements (PUT) and deletes of
# individual items (defaults to read-only item access).
ITEM_METHODS = ['GET', 'PATCH', 'PUT', 'DELETE']
IF_MATCH = False # When set to false, older versions may potentially replace newer versions
XML = False # disable xml output
# Schemas for data objects are defined here:
classes = {
# ... Contents omitted for this question
}
people = {
# ... Contents omitted for this question
}
logs = {
# ... Contents omitted for this question
}
sessions = {
# ... Contents omitted for this question
}
accounts = {
# the standard account entry point is defined as '/accounts/<ObjectId>'.
# an additional read-only entry point is accessible at '/accounts/<username>'.
'additional_lookup': {
'url': 'regex("[\w]+")',
'field': 'username',
},
# disable endpoint caching to prevent apps from caching account data
'cache_control': '',
'cache_expires': 0,
# schema for the accounts endpoint
'schema': {
'username': {
'type': 'string',
'required': True,
'unique': True,
},
'password': {
'type': 'string',
'required': True,
},
},
}
# The DOMAIN dict explains which resources will be available and how they will
# be accessible to the API consumer.
DOMAIN = {
'classes': classes,
'people': people,
'logs': logs,
'sessions': sessions,
'accounts': accounts,
}
One simple solution would be to not restrict your user creation method. Something like so:
class BCryptAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
# allow anyone to create a new account.
if resource == 'accounts' and method == 'POST':
return True
accounts = Eve.app.data.driver.db['accounts']
account = accounts.find_one({'username': username})
if account and 'user_id' in account:
self.set_request_auth_value(account['user_id'])
return account and bcrypt.hashpw(password.encode('utf-8'),account['salt'].encode('utf-8')) == account['password']
Alternatively, and especially so if you only allow POSTing to the account endpoint, you could opt out of authentication for the endpoint:
'accounts': {
# or you could provide a different custom class here,
# so you don't need the guard in the general-purpose auth class.
'authentication': None,
...
}
Hope this helps.

Why is my Django SessionWizardView Survey Application Data not showing up in a usable format in my MySQL Database?

I have built a multi-page survey application using a Django 1.6.2 SessionWizardView but I am having some trouble with how the data submitted through the survey form is being saved in my MySQL database django_db. That is I simply do not recognize or understand what is being saved. This is my first time building an application like this or even working with a databases so please forgive my ignorance.
My problem is that when I look at my database via phpMyAdmin nothing is recognizable as being from my application, So I have no idea where to start.
I believe the data from my SessionWizardView should get stored in the django_session table but when I inspect it it looks like gibberish.
Here is a copy of the session_data- Is this what I am meant to be looking at?
ZTEwNWUzZmI4NjA5MmQ3Nzk2MDQ1MWY3YzE2MjYxZWZjNDJmODQ3Yjp7ImVpZ2h0X2ltYWdlIjoiUDFEOS5qcGciLCJwYXRoX3R3b19pbWFnZXMiOltdLCJwYXRoX29uZV9pbWFnZXMiOltdLCJ0aGlyZF9pbWFnZSI6IlA5RDguanBnIiwiaW1hZ2VzIjpbXSwid2l6YXJkX3N1cnZleV93aXphcmRfb25lIjp7InN0ZXBfZmlsZXMiOnt9LCJzdGVwIjpudWxsLCJleHRyYV9kYXRhIjp7fSwic3RlcF9kYXRhIjp7fX0sInNpeHRoX2ltYWdlIjoiUDVENC5qcGciLCJmb3VydGhfaW1hZ2UiOiJQNEQzLmpwZyIsImZpZnRoX2ltYWdlIjoiUDJEMS5qcGciLCJ3aXphcmRfc3VydmV5X3dpemFyZF90d28iOnsic3RlcF9maWxlcyI6e30sInN0ZXAiOm51bGwsImV4dHJhX2RhdGEiOnt9LCJzdGVwX2RhdGEiOnt9fSwicGF0aF90aHJlZV9pbWFnZXMiOlsiUDNEMS5qcGciLCJQNEQyLmpwZyIsIlA1RDMuanBnIiwiUDZENC5qcGciLCJQN0Q1LmpwZyIsIlA4RDYuanBnIiwiUDlENy5qcGciLCJQMUQ4LmpwZyIsIlAyRDkuanBnIl0sImluc3RydWN0aW9uX3Rhc2tfb25lX2ltYWdlcyI6WyJJVDFBLmpwZyIsIklUMUIuanBnIiwiSVQxQy5qcGciXSwiZmlyc3RfaW1hZ2UiOiJQNkQ1LmpwZyIsInNlY29uZF9pbWFnZSI6IlA4RDcuanBnIiwic2V2ZW50aF9pbWFnZSI6IlAzRDIuanBnIiwic2xpZGVyX0RWX3ZhbHVlcyI6W10sImluc3RydWN0aW9uX3Rhc2tfdHdvX2ltYWdlcyI6WyJJVDJBLmpwZyIsIklUMkIuanBnIiwiSVQyQy5qcGciXSwibmludGhfaW1hZ2UiOiJQN0Q2LmpwZyIsIndpemFyZF9zdXJ2ZXlfd2l6YXJkX3RocmVlIjp7InN0ZXBfZmlsZXMiOnsiMSI6e30sIjAiOnt9LCIyIjp7fX0sInN0ZXAiOiIzIiwiZXh0cmFfZGF0YSI6e30sInN0ZXBfZGF0YSI6eyIxIjp7IjEtbm90aGluZyI6WyIiXSwic2xpZGVyX3ZhbHVlIjpbIisxMDAiXSwiY3NyZm1pZGRsZXdhcmV0b2tlbiI6WyIwblNZWTFuZkZ6QzdBb0R0UFlwZkpyc1NySGhGRWhONyJdLCJzdWJtaXQiOlsiTmV4dCJdLCJzdXJ2ZXlfd2l6YXJkX3RocmVlLWN1cnJlbnRfc3RlcCI6WyIxIl19LCIwIjp7IjAtbm90aGluZyI6WyIiXSwiY3NyZm1pZGRsZXdhcmV0b2tlbiI6WyIwblNZWTFuZkZ6QzdBb0R0UFlwZkpyc1NySGhGRWhONyJdLCJzdXJ2ZXlfd2l6YXJkX3RocmVlLWN1cnJlbnRfc3RlcCI6WyIwIl0sInN1Ym1pdCI6WyJOZXh0Il0sInNsaWRlcl92YWx1ZSI6WyIrMTAwIl19LCIyIjp7ImNzcmZtaWRkbGV3YXJldG9rZW4iOlsiMG5TWVkxbmZGekM3QW9EdFBZcGZKcnNTckhoRkVoTjciXSwiMi1ub3RoaW5nIjpbIiJdLCJzdXJ2ZXlfd2l6YXJkX3RocmVlLWN1cnJlbnRfc3RlcCI6WyIyIl0sInN1Ym1pdCI6WyJOZXh0Il19fX19
Questions:
Can someone take a look at my process/code below and tell me if I am
missing a step, or have not included something in my code?
Am I missing something at the MySQL end? Should I be creating
specific tables or somehow customizing it so that it stores the data
in a useable/readable format?
Am I even looking in the right place for completed survey
application data? Where does a SessionWizardView store/send it?
Once again this is my first database application so thank you for your patients
Process:
I created the database in MySQL (Ver 14.14 Distrib 5.6.20) via
Terminal CREATE database django_db; and the tables in it are
created when I run the command python manage.py syncdb Other than
that I do not touch the database.
I can complete my survey built using the SessionWizardView both on my
local machine and on the public server. No errors and it apepars
everything works fine
I have setup phpMyAdmin and can see the django_db database. However
I don't really know what I am looking at.
Code:
forms.py
For the most part the survey has relatively simple questions such as:
class SurveyFormA(forms.Form):
#When were you born?
birthdate = forms.DateField(widget=extras.SelectDateWidget(years = range(1995, 1900, -1)), label='What is your Date of Birth?', required = False)
#What is your current relationship status?
SINGLE = 'Single'
INARELATIONSHIP = 'In a relationship'
MARRIED = 'Married'
DIVORCED = 'Divorced'
SEPARATED = 'Separated'
WIDOWED = 'Widowed'
RELATIONSHIP = (
("", "----------"),
(SINGLE, "Single"),
(INARELATIONSHIP, "In a relationship"),
(MARRIED, "Married"),
(DIVORCED, "Divorced"),
(SEPARATED, "Separated"),
(WIDOWED, "Widowed"),
)
relationship = forms.ChoiceField(widget=forms.Select(), choices=RELATIONSHIP, initial= "", label='What is your relationship status?', required = False)
class SurveyFormB(forms.Form): #Internet usage questions
(second page questions here)
....
....
These seem to work fine as you can see in the image below
Each of the SessionWizardViews has a get_context_data used for captureing and storing data from one page of the survey form to the next and done method. I am not showing the full get_context_data as it is quite long.
views.py
class SurveyWizardOne(SessionWizardView):
def get_context_data(self, form, **kwargs):
context = super(SurveyWizardOne, self).get_context_data(form, **kwargs)
....
....
def done(self, form_list, **kwargs):
return render(self.request, 'Return_to_AMT.html', {
'form_data': [form.cleaned_data for form in form_list],
})
Database Connection:
My Django site is connected to a MySQL database in settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django_db',
'USER': 'root',
'PASSWORD': '************',
'HOST': '127.0.0.1',
#'PORT': '',
}
}
You have to either write the models.py or set a specific database that you have in mind to be the database accessed. Neither of which seem to be done here.
These links from the official documentation should help :
https://docs.djangoproject.com/en/1.6/ref/databases/
https://docs.djangoproject.com/en/1.6/topics/db/
PS - You should consider using Django 1.8 instead. It's even better documented and much easier to work with. Additionally porting your codes from one Django version to the next is really very easy. Hardly a little more than simple copy pasting.

Authenticating users using pusher in django

I am a bit confused on how does the Authentication works in Django using pusher i want to implement a one-to-one chatting system so i guess i will be using private channels that requires authentication before you can subscribe to the channel ... i read there that the endpoint is the url you want pusher to POST to, i added a url to test if it is working but every time the status returns 403 and it seems it doesn't enter the view i created to test it so any ideas ? here is a sample of my code :
message.html
var channel = pusher.subscribe('private-test');
channel.bind('message', function(data) {
var $message = $('<div class="message"/>').appendTo('#messages');
$('<span class="user"/>').text(data.user).appendTo($message);
$('<span/>').text(data.message).appendTo($message);
});;
Pusher.channel_auth_endpoint = 'test/';
Pusher.channel_auth_transport = 'ajax';
channel.bind('pusher:subscription_succeeded', function(status) {
alert(status);
});
channel.bind('pusher:subscription_error', function(status) {
alert(status);
});
Views.py:
def testUser(request,user_name):
print 'Test Passed'
return render_to_response('message.html', {
'PUSHER_KEY': settings.PUSHER_KEY,'channel_variable':request.user.id,'other_var':'3',
}, RequestContext(request))
when i checked the url it POSTs to, in my cmd i found it correct and it matched the one i put in urls.py but i still don't know why it does not enter my view
I don't know Django, but it seems highly likely that the framework is intercepting the call to prevent CSRF (Cross site resource forgery).
The Django docs talk about CSRF here:
https://docs.djangoproject.com/en/dev/ref/contrib/csrf/
As with a number of frameworks you'll need to provide a CSRF token as part of the XHR/AJAX call to the authentication endpoint, or override the framework interception (somehow).
Have a look at the auth section of the Pusher constructor options parameter. In there you'll find an example of how to pass a CSRF token.

Categories