Django: Display website name in Template/Emails without using Sites framework - python

I want to render my website name in django templates. Django's own docs on Sites state:
Use it if your single Django installation powers more than one site
and you need to differentiate between those sites in some way.
My django app doesn't. I know I can still use it, but it seems like an overkill. I just want to pull a variable with my website's name (!= domain) in ANY template. I don't want to pass it in views either because that doesn't seem DRY enough.
Writing a custom processor seemed like a simple-enough option, but for some reason these variables aren't available in the .txt emails django-registration sends (while other variables definitely are, so I guess it's not impossible).
TIA
Edit: was asked to include code that doesn't work:
processors.py:
def get_website_name(request):
website_name = 'SomeWebsite'
return {'mysite_name': website_name}
Included successfully in context_processors in settings.py. It works nicely in "regular" templates, but not in emails.
Here's how I'm sending the emails, inside a change_email_view:
msg_plain = render_to_string('email_change_email.txt', context)
msg_html = render_to_string('email_change_email.html', context)
send_mail(
'Email change request',
msg_plain,
'my#email',
[profile.pending_email],
html_message=msg_html,
)
A further problem is that django-regitration further abstracts some of those views away: so when a user registers, wants to reset a password, etc...I don't even have access to the views.

Based on Django custom context_processors in render_to_string method you should pass the request to render_to_string.
msg_plain = render_to_string('email_change_email.txt', context, request=request)
msg_html = render_to_string('email_change_email.html', context, request=request)

Related

Redirection in django and passing arguments

I spent some time reading through the documentation and forums, but not sure I understand this. I have this bit of code in the views of my app:
def billboard_index(request):
if request.method == 'POST':
form = SpotiForm(request.POST)
if form.is_valid():
date = form.cleaned_data['spotiread_date']
try:
url = 'https://www.billboard.com/charts/hot-100/' + date
billboard = requests.get(url)
billboard.raise_for_status()
except:
print("No response")
else:
soup = BeautifulSoup(billboard.text, 'html.parser')
positions = [int(i.text) for i in soup.find_all(name='span', class_='chart-element__rank__number')]
songs = [i.text for i in soup.find_all(name='span', class_='chart-element__information__song')]
artists = [i.text for i in soup.find_all(name='span', class_='chart-element__information__artist')]
top100 = list(zip(positions, songs, artists))
if Top100model.objects.exists():
Top100model.objects.all().delete()
for position in top100:
top100data = Top100model(
idtop=str(position[0]), artist=str(position[2]), song=str(position[1])
)
top100data.save()
params = {
'client_id': SPOTIPY_CLIENT_ID,
'response_type': 'code',
'redirect_uri': request.build_absolute_uri(reverse('spotiauth')),
'scope': 'playlist-modify-private'
}
query_string = urlencode(params)
url = '{}?{}'.format('https://accounts.spotify.com/authorize', query_string)
return redirect(to=url)
# if a GET (or any other method) we'll create a blank form
else:
form = SpotiForm()
return render(request, 'billboardapp.html', {'form': form})
on billboard_index I have a form with one field in which user puts a date. This date is then used as an input for a webscraper. I save the scraped data in the database, this works (I know this code will break in couple instances, but I'll deal with this later). Next I want to follow the spotify authorization flow, so I redirect to a url at spotify/authorization, it works. This gives me the code back when I'm redirected to spotiauth.html. At the same time, I print there all the database entries that were added during scraping. This is the spotiauth view:
def spotiauth(request):
Positions100 = Top100model.objects.all()
print(request)
context = {
'positions': Positions100,
}
return render(request, 'spotiauth.html', context=context)
I have couple questions:
How do I pass additional arguments to the spotiauth view? I tried
return redirect(to=url, date=date)
But I can't access it in spotiauth view. So I don't really want to pass it in the url, I just want it as an argument to another function, is this doable?
Is this the actual way to go about it? Not sure this is the simplest thing to do.
Thank you for your help!
Authentication is something that should be handled in a generic way, and not individually and explicitly per request. This is because you don't want to duplicate the authentication code in every request that needs authentication.
Lucky you, you are using Django which already comes with an authentication and authorization layer, and a great community that creates great libraries such as django-allauth that integrate OAuth2 authentication into Django's authentication layer.
OAuth2 against Spotify is what you are trying to implement here. Just
include django-allauth via pip,
configure the Spotify provider in the settings following their documentation,
include their URLs for login and registration (see their docs)
... and you should be able to sign into your app using a Spotify account.
For your regular views then, the decorator login_required would then suffice.
Django-allauth will do the following:
for users who sign in via OAuth2 providers, regular Django accounts will be created automatically
you can see these users in the Django admin, in the same list as the regular Django users
you can manage the configuration of the OAuth2 provider configuration via the Django Admin - django-allauth brings a model with an admin for it
django-allauth brings additional functionality like email verification, multiple email address management etc.
If you want to style the login and registration pages, you can implement your own templates using django-allauth's templates as basis.

Django Rest Framework gives 302 in Unit tests when force_login() on detail view?

I'm using Django Rest Framework to serve an API. I've got a couple tests which work great. To do a post the user needs to be logged in and I also do some checks for the detail view for a logged in user. I do this as follows:
class DeviceTestCase(APITestCase):
USERNAME = "username"
EMAIL = 'a#b.com'
PASSWORD = "password"
def setUp(self):
self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
self.sa_group.user_set.add(self.authorized_user)
def test_post(self):
device = DeviceFactory.build()
url = reverse('device-list')
self.client.force_login(self.authorized_user)
response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
self.client.logout()
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# And some more tests here
def test_detail_logged_in(self):
device = DeviceFactory.create()
url = reverse('device-detail', kwargs={'pk': device.pk})
self.client.force_login(self.authorized_user)
response = self.client.get(url)
self.client.logout()
self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
# And some more tests here
The first test works great. It posts the new record and all checks pass. The second test fails though. It gives an error saying
AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/
It turns out the list view redirects the user to the login screen. Why does the first test log the user in perfectly, but does the second test redirect the user to the login screen? Am I missing something?
Here is the view:
class APIAuthGroup(InAuthGroup):
"""
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
"""
allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]
def has_permission(self, request, view):
return request.method in SAFE_METHODS \
or super(APIAuthGroup, self).has_permission(request, view)
class DevicesViewSet(DatapuntViewSetWritable):
"""
A view that will return the devices and makes it possible to post new ones
"""
queryset = Device.objects.all().order_by('id')
serializer_class = DeviceSerializer
serializer_detail_class = DeviceSerializer
http_method_names = ['post', 'list', 'get']
permission_classes = [APIAuthGroup]
Here is why you are getting this error.
Dependent Libraries
I did some searching by Class Names to find which libraries you were using so that I can re-create the problem on my machine. The library causing the problem is the one called keycloak_idc. This library installs another library mozilla_django_oidc which would turn out to be the reason you are getting this.
Why This Library Is Causing The Problem
Inside the README file of this library, it gives you instructions on how to set it up. These are found in this file. Inside these instructions, it instructed you to add the AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]
When you add this authentication backend, all your requests pass through a Middleware defined inside the SessionRefresh class defined inside mozilla_django_oidc/middleware.py. Inside this class, the method process_request() is always called.
The first thing this method does is call the is_refreshable_url() method which always returns False if the request method was POST. Otherwise (when the request method is GET), it will return True.
Now the body of this if condition was as follows.
if not self.is_refreshable_url(request):
LOGGER.debug('request is not refreshable')
return
# lots of stuff in here
return HttpResponseRedirect(redirect_url)
Since this is a middleware, if the request was POST and the return was None, Django would just proceed with actually doing your request. However when the request is GET and the line return HttpResponseRedirect(redirect_url) is triggered instead, Django will not even proceed with calling your view and will return the 302 response immediately.
The Solution
After a couple of hours debugging this, I do not the exact logic behind this middleware or what exactly are you trying to do to provide a concrete solution since this all started based off guess-work but a naive fix can be that you remove the AUTHENTICATION_BACKENDS from your settings file. While I feel that this is not acceptable, maybe you can try using another library that accomplishes what you're trying to do or find an alternative way to do it. Also, maybe you can contact the author and see what they think.
So i guess you have tested this and you get still the same result:
class APIAuthGroup(InAuthGroup):
def has_permission(self, request, view):
return True
Why do you use DeviceFactory.build() in the first test and DeviceFactory.create() in the second?
Maybe a merge of the two can help you:
def test_get(self):
device = DeviceFactory.build()
url = reverse('device-list')
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
Is this a problem with the setUp() method? From what I see, you may be setting self.authorize_user to a user that was already created on the first test.
Instead, I would create the user on each test, making sure that the user doesn't exist already, like so:
user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
self.authorize_user = User.objects.create_user....
That would explain why your first test did pass, why your second didn't, and why #anupam-chaplot's answer didn't reproduce the error.
Your reasoning and code looks ok.
However you are not giving the full code, there must be error you are not seeing.
Suspicious fact
It isn't be default 302 when you are not logged in.
(#login_required, etc redirects but your code doesn't have it)
Your APIAuthGroup permission does allow GET requests for non-logged-in user ( return request.method in SAFE_METHODS), and you are using GET requests (self.client.get(url))
So it means you are not hitting the endpoint that you think you are hitting (your get request is not hitting the DevicesViewSet method)
Or it could be the case you have some global permission / redirect related setting in your settings.py which could be DRF related..
eg :
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Guess
url = reverse('device-detail', kwargs={'pk': device.pk})
might not point to the url you are thinking..
maybe there's another url (/sa/devices/1/) that overrides the viewset's url. (You might have a django view based url)
And I didn't address why you are getting redirected after force_login.
If it's indeed login related redirect, all I can think of is self.authorized_user.refresh_from_db() or refreshing the request ..
I guess some loggin related property (such as session, or request.user) might point to old instance .. (I have no evidence or fact this can happen, but just a hunch) and you better off not logging out/in for every test case)
You should make a seperate settings file for testing and add to the test command --settings=project_name.test_settings, that's how I was told to do.

Django two-factor authentication, require 2FA on specific views

I am implementing Django two-factor-auth on my website and I would love to have some views protected by two-FA, and some other not.
In order to do so, I use the decorator #otp_required which works great, but unfortunately asks the users to input their credentials again (to handle user sessions, I use the registration module).
Would you be able to give me a good to way to hack the form in order to just ask the user to input the token (skipping a step of the form, basically) ?
Thanks a lot,
For those who care, I found a way to do it that is quite clean.
The trick was to override the LoginView class in the core.py module of the two_factor_authentication module.
In order to do so, go to your views and insert the following code:
class CustomLoginView(LoginView):
form_list = (
('token', AuthenticationTokenForm),
('backup', BackupTokenForm),
)
def get_user(self):
self.request.user.backend = 'django.contrib.auth.backends.ModelBackend'
return self.request.user
Basically, I erase the 'auth' step and override the method get_user() in order to return the current user.
The backend must be specified otherwise Django raises an error.
Now, to make that class be used instead of the LoginView, go to your urls and insert the following line BEFORE including the two_factor.urls.
url(r'^account/login/$', tradingviews.CustomLoginView.as_view(), name='login'),
That's it!

Flask-Admin - Customizing views

I'm developing a web-app using Flask and pyMongo, and I've recently started to integrate the Flask-Admin module (1.0.4), given the fresh mongodb support.
All is smooth and fine when using ModelViews, but when it comes to subclassing a BaseView I simply can't get it working.
Here is my setup:
user_view = Admin(app, name='User stuff', url="/user", endpoint="user")
class ProfileForm(wtf.Form):
username = wtf.TextField('Username', [wtf.Required()])
name = wtf.TextField('Name', [wtf.Required()])
class Profile(BaseView):
#expose('/', methods=('GET', 'POST'))
def profile(self):
user = User(uid) # gets the user's data from DB
form = ProfileForm(request.form, obj=user)
if form.validate_on_submit():
data = form.data
user.set(**data)
user.save()
flash("Your profile has been saved")
else:
flash("form did not validate on submit")
return self.render('user/profile.html', form=form, data=user)
user_view.add_view(Profile(name='Profile', url='profile'))
When submitting the form, wtforms does not report any error (unless there is any) but the validation does not return to my profile view (the else: branch is always executed)
There is no way I could find to make this work, inspite having thoroughly scanned flask-admin documentation, source code and examples.
Could anybody suggest how I could fix my code, or work around this problem ?
I have suspicion that form is getting submitted using GET method instead of POST or Flask-WTF CSRF check fails.
Here's small gist I made with your sample code. It works as expected: https://gist.github.com/4556210
Few comments:
Template uses some Flask-Admin library functions to render the form. You don't have to use them if you dont want to;
Uses mock user object
Put template under templates/ subdirectory if you want to run the sample.
In either case, Flask-Admin views behave exactly same way like "normal" Flask views, they're just organised differently.

Django user impersonation by admin

I have a Django app. When logged in as an admin user, I want to be able to pass a secret parameter in the URL and have the whole site behave as if I were another user.
Let's say I have the URL /my-profile/ which shows the currently logged in user's profile. I want to be able to do something like /my-profile/?__user_id=123 and have the underlying view believe that I am actually the user with ID 123 (thus render that user's profile).
Why do I want that?
Simply because it's much easier to reproduce certain bugs that only appear in a single user's account.
My questions:
What would be the easiest way to implement something like this?
Is there any security concern I should have in mind when doing this? Note that I (obviously) only want to have this feature for admin users, and our admin users have full access to the source code, database, etc. anyway, so it's not really a "backdoor"; it just makes it easier to access a user's account.
I don't have enough reputation to edit or reply yet (I think), but I found that although ionaut's solution worked in simple cases, a more robust solution for me was to use a session variable. That way, even AJAX requests are served correctly without modifying the request URL to include a GET impersonation parameter.
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.session['impersonate_id'] = int(request.GET["__impersonate"])
elif "__unimpersonate" in request.GET:
del request.session['impersonate_id']
if request.user.is_superuser and 'impersonate_id' in request.session:
request.user = User.objects.get(id=request.session['impersonate_id'])
Usage:
log in: http://localhost/?__impersonate=[USERID]
log out (back to admin): http://localhost/?__unimpersonate=True
It looks like quite a few other people have had this problem and have written re-usable apps to do this and at least some are listed on the django packages page for user switching. The most active at time of writing appear to be:
django-hijack puts a "hijack" button in the user list in the admin, along with a bit at the top of page for while you've hijacked an account.
impostor means you can login with username "me as other" and your own password
django-impersonate sets up URLs to start impersonating a user, stop, search etc
I solved this with a simple middleware. It also handles redirects (that is, the GET parameter is preserved during a redirect). Here it is:
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.user = models.User.objects.get(id=int(request.GET["__impersonate"]))
def process_response(self, request, response):
if request.user.is_superuser and "__impersonate" in request.GET:
if isinstance(response, http.HttpResponseRedirect):
location = response["Location"]
if "?" in location:
location += "&"
else:
location += "?"
location += "__impersonate=%s" % request.GET["__impersonate"]
response["Location"] = location
return response
#Charles Offenbacher's answer is great for impersonating users who are not being authenticated via tokens. However, it will not work with clients side apps that use token authentication. To get user impersonation to work with apps using tokens, one has to directly set the HTTP_AUTHORIZATION header in the Impersonate Middleware. My answer basically plagiarizes Charles's answer and adds lines for manually setting said header.
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.session['impersonate_id'] = int(request.GET["__impersonate"])
elif "__unimpersonate" in request.GET:
del request.session['impersonate_id']
if request.user.is_superuser and 'impersonate_id' in request.session:
request.user = User.objects.get(id=request.session['impersonate_id'])
# retrieve user's token
token = Token.objects.get(user=request.user)
# manually set authorization header to user's token as it will be set to that of the admin's (assuming the admin has one, of course)
request.META['HTTP_AUTHORIZATION'] = 'Token {0}'.format(token.key)
i don't see how that is a security hole any more than using su - someuser as root on a a unix machine. root or an django-admin with root/admin access to the database can fake anything if he/she wants to. the risk is only in the django-admin account being cracked at which point the cracker could hide tracks by becoming another user and then faking actions as the user.
yes, it may be called a backdoor, but as ibz says, admins have access to the database anyways. being able to make changes to the database in that light is also a backdoor.
Set up so you have two different host names to the same server. If you are doing it locally, you can connect with 127.0.0.1, or localhost, for example. Your browser will see this as three different sites, and you can be logged in with different users. The same works for your site.
So in addition to www.mysite.com you can set up test.mysite.com, and log in with the user there. I often set up sites (with Plone) so I have both www.mysite.com and admin.mysite.com, and only allow access to the admin pages from there, meaning I can log in to the normal site with the username that has the problems.

Categories

Resources