Commit 151563c8 authored by Birin Sanchez's avatar Birin Sanchez

Add tunnel account logic for User model.

Signed-off-by: Birin Sanchez's avatarBirin Sanchez <birin.sanchez@puri.sm>
parent b5a858bf
Pipeline #4796 passed with stage
in 39 seconds
......@@ -37,3 +37,4 @@ OVPN_PORT=22
OVPN_USERNAME=username
OVPN_FILEPATH="/path/to/{IDENTITY}/{IDENTITY}.ovpn"
TUNNEL_HOST="https://example.com"
\ No newline at end of file
......@@ -6,3 +6,12 @@ DJANGO_SECRET_KEY = random_key
AUTH_LDAP_BIND_PASSWORD = ldap_password
WOO_CONSUMER_KEY = woo_key
WOO_CONSUMER_SECRET = woo_secret
TUNNEL_IDENTITY = "partner_id"
TUNNEL_SECRET = "secret"
# Fernet key generation can be done with:
# $ python3 -c 'from cryptography.fernet import Fernet; \
# print(Fernet.generate_key().decode())'
# You should get something like this:
# jBv_8z3mq0jurqeB5MFYav1n6MTyWd4bOXJ8tzy9ywU=
TUNNEL_KEY = generate_your_own_key!
\ No newline at end of file
......@@ -71,7 +71,18 @@ SPDX-License-Identifier: AGPL-3.0
{% for limit in limits %}
<tr>
<th class="row_header">{{ limit.service_label() }}</th>
<th class="row_header">{{ limit.service_label() }}
{% if limit.service_label() == 'Tunnel' and limit.service_activable_by_user() %}
<form method="post" action="{% url 'toggle_tunnel' %}">{% csrf_token %}
{% if limit.is_active %}
<input type="submit" value="Deactivate"/>
{% else %}
<input type="submit" value="Activate"/>
{% endif %}
<input type="hidden" name="enable_tunnel" value="{{ not limit.is_active }}"/>
</form>
{% endif %}
</th>
<td>{{ limit.active_label() }}</td>
<td title="{{ limit.endpoint_full_label() }}">{{ limit.endpoint_short_label() }}</td>
<td>{{ limit.credit_label() }}</td>
......
......@@ -109,6 +109,14 @@ class Limit(models.Model):
return label
def service_activable_by_user(self):
"""Returns True when the user is allowed to activate the
service. Returns False when the user is NOT allowed to
activate the service.
"""
if self.expiry_date is None:
return False
return (self.expiry_date - timezone.now()).total_seconds() > 0
class ExternalBundle(models.Model):
parser = models.IntegerField(default=settings.LM_PARSERS.UNDEFINED,
......
import requests
BASE_URL = '{host}/resellers/api/v1/{operation}'
class TunnelManager(object):
def __init__(self, partner_id, secret, host):
self.partner_id = partner_id
self.secret = secret
self.host = host
def create_account(self):
"""Creates a new authentication set of credentials. Returns None if
the request doesn't succeed. Returns a dictionary with the following
keys if it succeeds:
username: new username created.
password: password generated for the user.
status: This is the account status, should be "active".
started_at: time at which the account was started.
paused_at: time at which the account was paused.
endend_at: time at which the account was endned.
"""
url = BASE_URL.format(host=self.host, operation='create')
payload = {'secret': self.secret,
'partner_id': self.partner_id}
r = requests.post(url, data=payload)
if r.status_code == 200:
return r.json()
return None
def terminate_account(self, username):
"""Terminates an account for the specified username. Returns None if
the request doesn't succeed. Returns a dictionary with the
following keys if it succeeds:
username: username of the terminated account.
status: Account status after termintation, should be "inactive".
started_at: time at which the account was started.
paused_at: time at which the account was paused, should be None.
endend_at: time at which the account was endned.
"""
url = BASE_URL.format(host=self.host, operation='terminate')
payload = {'secret': self.secret,
'partner_id': self.partner_id,
'username': username}
r = requests.post(url, data=payload)
if r.status_code == 200:
return r.json()
return None
def find_account(self):
pass
def pause_account(self, username):
"""Pauses an account for the specified username, temporarily revoking
access to the tunnel. Returns None if the request doesn't
succeed. Returns a dictionary with the following keys if it
succeeds:
username: username of the paused account.
status: Account status after termintation, should be "paused".
started_at: time,in Unix epoch, at which the account was
started.
paused_at: time, in Unix epoch, at which the account was
paused, should be close to now.
ended_at: time, in Unix epoch at which the account was ended,
should be None.
"""
url = BASE_URL.format(host=self.host, operation='pause')
payload = {'secret': self.secret,
'partner_id': self.partner_id,
'username': username}
r = requests.post(url, data=payload)
if r.status_code == 200:
return r.json()
return None
def resume_account(self, username):
"""Resumes an account for the specified username, granting access to
the tunnel. Returns None if the request doesn't
succeed. Returns a dictionary with the following keys if it
succeeds:
username: username of the resumed account.
status: Account status after termintation, should be "active".
started_at: time,in Unix epoch, at which the account was
started.
paused_at: time, in Unix epoch, at which the account was
paused, should be None.
ended_at: time, in Unix epoch at which the account was ended,
should be None.
"""
url = BASE_URL.format(host=self.host, operation='resume')
payload = {'secret': self.secret,
'partner_id': self.partner_id,
'username': username}
r = requests.post(url, data=payload)
if r.status_code == 200:
return r.json()
return None
def update_password(self, username, current_password, new_password):
"""Updates the password for the account specified by username. Returns
None if the request doesn't succeed. Returns a dictionary with
the following keys if it succeeds:
username: username the for which password was changed.
status: Account status after termintation, should be "active".
started_at: time,in Unix epoch, at which the account was
started. should be None.
paused_at: time, in Unix epoch, at which the account was
paused, should be None.
ended_at: time, in Unix epoch at which the account was ended,
should be None.
current_password: Old password for the account.
new_password: New password for the account.
"""
url = BASE_URL.format(host=self.host, operation='update_password')
payload = {'secret': self.secret,
'partner_id': self.partner_id,
'username': username,
'current_password': current_password,
'new_password': new_password}
r = requests.post(url, data=payload)
if r.status_code == 200:
return r.json()
return None
def list_accounts(self):
pass
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import FileResponse
from django.http import FileResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from .models import Limit
......@@ -46,3 +47,14 @@ def ovpn_userfile(request):
response = FileResponse(open(filepath, 'rb'), content_type='application/octet-stream')
response['Content-Disposition'] = 'attachment; filename="purist.ovpn"'
return response
@login_required
def toggle_tunnel(request):
tunnel_limit = request.user.limit_set.filter(
service=settings.LM_SERVICES.TUNNEL).first()
if tunnel_limit.service_activable_by_user():
if not request.user.has_tunnel_account():
request.user.generate_tunnel_account()
tunnel_limit.is_active = not tunnel_limit.is_active
tunnel_limit.save()
return HttpResponseRedirect(reverse('profile'))
......@@ -205,3 +205,12 @@ CAPTCHA_TIMEOUT = 15
CAPTCHA_LENGTH = 4
CAPTCHA_TEST_MODE = True
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge'
#
# Tunnel
#
TUNNEL_IDENTITY = secret_config('TUNNEL_IDENTITY')
TUNNEL_SECRET = secret_config('TUNNEL_SECRET')
TUNNEL_HOST = config('TUNNEL_HOST')
TUNNEL_KEY = secret_config('TUNNEL_KEY')
......@@ -39,6 +39,8 @@ urlpatterns = [
url(r'^accounts/$', RedirectView.as_view(url='/')),
url(r'^accounts/profile/$', limitmonitor.views.userlimit, name='profile'),
url(r'^accounts/profile/purist.ovpn', limitmonitor.views.ovpn_userfile, name='ovpn_userfile'),
url(r'^accounts/profile/toggle_tunnel',
limitmonitor.views.toggle_tunnel, name='toggle_tunnel'),
url(r'^accounts/register/$', RegistrationView.as_view(form_class=RegistrationForm), name='registration_register'),
url(r'^accounts/', include('registration.backends.simple.urls')),
url(r'^download/', include('django_agpl.urls')),
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-03-06 11:24
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('purist', '0002_auto_20171208_1327'),
]
operations = [
migrations.AddField(
model_name='user',
name='tunnel_password',
field=models.CharField(default=None, max_length=150, null=True),
),
migrations.AddField(
model_name='user',
name='tunnel_user',
field=models.CharField(default=None, max_length=150, null=True),
),
]
......@@ -13,6 +13,8 @@ from django.utils.translation import ugettext_lazy as _
from ldapregister.models import LdapPerson
from limitmonitor import models as limitmonitor_models
from limitmonitor.task_resources import common as limitmonitor_common
from limitmonitor.tunnel import TunnelManager
from cryptography.fernet import Fernet
log = logging.getLogger(__name__)
......@@ -55,6 +57,9 @@ class User(AbstractUser):
},
)
tunnel_user = models.CharField(max_length=250, default=None, null=True)
tunnel_password = models.CharField(max_length=250, default=None, null=True)
@classmethod
def normalize_username(cls, username):
username = super(User, cls).normalize_username(username)
......@@ -135,3 +140,110 @@ class User(AbstractUser):
"""
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
return salted_hmac(key_salt, self.get_username()).hexdigest() # FIXME: should use LDAP password value!
def has_tunnel_account(self):
"""Returns True if the user has a tunnel account. Returns False if the
user does NOT have a tunnel account.
"""
return self.tunnel_user is not None
def generate_tunnel_account(self):
"""Creates a new tunnel account if the user does NOT have a tunnel
account. Returns True when the account is created and False
when it couldn't be created.
"""
if not self.has_tunnel_account():
tun = TunnelManager(settings.TUNNEL_IDENTITY,
settings.TUNNEL_SECRET,
settings.TUNNEL_HOST)
new_account = tun.create_account()
if new_account:
# Encrypt username and password with Fernet
fer = Fernet(settings.TUNNEL_KEY)
tun_user = fer.encrypt(new_account['username'].encode())
tun_passwd = fer.encrypt(new_account['password'].encode())
self.tunnel_user = tun_user
self.tunnel_password = tun_passwd
self.save()
return True
return False
def pause_tunnel_account(self):
"""Pauses the tunnel account for the user, setting it is_active to
False. Returns True when it was possible to pause the account
and False when it couldn't.
"""
if self.has_tunnel_account():
tun = TunnelManager(settings.TUNNEL_IDENTITY,
settings.TUNNEL_SECRET,
settings.TUNNEL_HOST)
fer = Fernet(settings.TUNNEL_KEY)
tun_user = fer.decrypt(self.tunnel_user).decode()
result = tun.pause_account(tun_user)
if result:
tunnel_limit = self.limit_set.filter(
service=settings.LM_SERVICES.TUNNEL).first()
tunnel_limit.is_active = False
tunnel_limit.save()
return True
return False
def resume_tunnel_account(self):
"""Resumes the tunnel account for the user, setting it is_active to
True. Returns True when it was possible to resume the account
and False when it was not.
"""
if self.has_tunnel_account():
tun = TunnelManager(settings.TUNNEL_IDENTITY,
settings.TUNNEL_SECRET,
settings.TUNNEL_HOST)
fer = Fernet(settings.TUNNEL_KEY)
tun_user = fer.decrypt(self.tunnel_user).decode()
result = tun.resume_account(tun_user)
if result:
tunnel_limit = self.limit_set.filter(
service=settings.LM_SERVICES.TUNNEL).first()
tunnel_limit.is_active = True
tunnel_limit.save()
return True
return False
def terminate_tunnel_account(self):
"""Terminates the tunnel account for the user. Returns True when it
was possible to terminate the account and False when it was
not.
"""
if self.has_tunnel_account():
tun = TunnelManager(settings.TUNNEL_IDENTITY,
settings.TUNNEL_SECRET,
settings.TUNNEL_HOST)
fer = Fernet(settings.TUNNEL_KEY)
tun_user = fer.decrypt(self.tunnel_user).decode()
result = tun.terminate_account(tun_user)
if result:
self.tunnel_user = None
self.tunnel_password = None
self.save()
tunnel_limit = self.limit_set.filter(
service=settings.LM_SERVICES.TUNNEL).first()
tunnel_limit.is_active = False
tunnel_limit.save()
return True
return False
def get_tunnel_credentials(self):
"""Returns tunnel account credentials for the user decrypting
them. Returns the tuple (tun_user, tun_passwd) if the user has
a tunnel account and is activated. Returns None in any other
case.
"""
if self.has_tunnel_account():
tunnel_limit = self.limit_set.filter(
service=settings.LM_SERVICES.TUNNEL).first()
if tunnel_limit.is_active:
fer = Fernet(settings.TUNNEL_KEY)
tun_user = fer.decrypt(self.tunnel_user.encode()).decode()
tun_passwd = fer.decrypt(self.tunnel_password.encode()
).decode()
return (tun_user, tun_passwd)
return None
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment