Commit b019296c authored by Noe Nieto's avatar Noe Nieto 💬

Merge

parents 4807405e dbe229db
Pipeline #15304 failed with stage
......@@ -2,7 +2,7 @@
Keel (LDH middleware)
<https://source.puri.sm/liberty/ldh_middleware>
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
......
......@@ -20,7 +20,7 @@ python-decouple = "==3.1"
python-ldap = "==3.0.0"
strictyaml = "==0.11.10"
confusable_homoglyphs = "==3.0.0"
Django = "==1.11.18"
Django = "==1.11.20"
django_celery_beat = "==1.1.0"
Jinja2 = "==2.10"
WooCommerce = "==1.2.1"
......@@ -29,29 +29,10 @@ djangorestframework = "==3.9.2"
django-password-reset = "==2.0"
[dev-packages]
# self:
# purist_middleware = {path = ".", editable = true}
# Documentation depencencies:
# alabaster==0.7.10
# Babel==2.5.3
# certifi==2017.11.5
# chardet==3.0.4
# docutils==0.14
# idna==2.6
# imagesize==0.7.1
# Jinja2==2.10
# MarkupSafe==1.0
# Pygments==2.2.0
# pytz==2017.3
# requests==2.18.4
# six==1.11.0
# snowballstemmer==1.2.1
# Sphinx==1.6.6
# sphinxcontrib-websupport==1.0.1
# urllib3==1.22
twine = "==1.12.1"
pylint = "*"
django_extensions = "*"
pydotplus = "*"
[requires]
python_version = "3"
......@@ -35,9 +35,12 @@ Models
Model diagram generated with:
./manage.py graph_models --all-applications --group-models \
./ldh_middleware graph_models --all-applications --group-models \
--verbose-name --output models.png
Requires GraphViz (`apt install graphviz`) and dev dependencies
(`pipenv install --dev`).
Build wheel package (and optionally upload)
-------------------------------------------
......@@ -127,7 +130,7 @@ Sharing and contributions
Keel (LDH middleware)
<https://source.puri.sm/liberty/ldh_middleware>
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
SPDX-License-Identifier: AGPL-3.0-or-later
Shared under AGPL-3.0-or-later. We adhere to the Community Covenant
......
# Django
# 3rd party
from captcha.fields import CaptchaField
from django import forms
from django.conf import settings
......@@ -14,12 +12,10 @@ from django.utils.translation import ugettext_lazy as _
from registration.backends.simple.views import RegistrationView
from registration.forms import validators
# Others
from cart.models import ChosenReward
from ldapregister.forms import RegistrationForm
from purist.models import AccountType, get_woo_connection
User = get_user_model()
......@@ -34,7 +30,7 @@ def woo_email_available_validator(value):
wc = get_woo_connection()
result = wc.get('customers?email={}'.format(value))
if result.ok and len(result.json()) > 0:
raise ValidationError(_('This recovery email is already in use'))
raise ValidationError(_('This recovery email is already in use.'))
class CartRegistrationForm(RegistrationForm):
......@@ -50,12 +46,11 @@ class CartRegistrationForm(RegistrationForm):
email = forms.EmailField(
label=_('Recovery email address'),
help_text=_('Optional. Enter an email address were we can send you '
help_text=_('Enter an email address where we can send you '
'recovery information.'),
validators=[
validators.validate_confusables_email, woo_email_available_validator
],
required=False,
)
password1 = forms.CharField(
......
......@@ -13,7 +13,6 @@ DEBUG_CHANGE_PASSWORD=False
DEBUG_REGISTER_STATUS = True
DEBUG_SKIP_ACTIVATION_COMMAND=True
DEBUG_SKIP_VALIDATE_ON_AUTHENTICATION = False
DEBUG_LOCAL_MAIL = False
DEBUG_REMOVE_CAPTCHA = False
# change to false after initial setup
ALLOWED_HOSTS=localhost
......@@ -70,7 +69,7 @@ EMAIL_HOST_USER = jhon.doe@example.com
EMAIL_PORT = 465
EMAIL_USE_TLS = False
EMAIL_USE_SSL = True
DEFAULT_FROM_EMAIL = "Do not replay <noreply@example.com>"
DEFAULT_FROM_EMAIL = "Do not reply <noreply@example.com>"
# This is the link to the subscribe page, used in different parts of the middleware
SUBSCRIBE_HREF = /#bundle
......
; stored as /etc/supervisor/conf.d/purist_middleware_monitor.conf
; Copyright 2017-2018 Purism SPC
; Copyright 2017-2019 Purism SPC
; SPDX-License-Identifier: AGPL-3.0-or-later
; Adapted from extra/supervisord/celerybeat.conf in Celery
......
......@@ -57,7 +57,7 @@ master_doc = 'index'
# General information about the project.
project = 'Keel (LDH middleware)'
copyright = '2017-2018 Purism SPC'
copyright = '2017-2019 Purism SPC'
author = 'Purism SPC'
# The version info for the project you're documenting, acts as replacement for
......
......@@ -2,7 +2,7 @@
{% load i18n %} {% load static %}
<!--
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
https://source.puri.sm/liberty/ldh_middleware
SPDX-License-Identifier: AGPL-3.0
-->
......
......@@ -7,5 +7,12 @@
{% block byline %}{% trans 'You have been logged out' %}{% endblock %}
{% block content %}
{% if messages %}
<div style="padding: 20px;">
{% for message in messages %}
<p>{{ message }}
{% endfor %}
</div>
{% endif %}
{% endblock %}
......@@ -2,9 +2,10 @@ import logging
import datetime
from django.conf import settings
from django.utils import timezone
from purist.models import get_woo_connection
from purist.limitmonitor import ParserContainer
from limitmonitor.models import ExternalCredit, ExternalBundle
from django.contrib.auth.hashers import make_password
from purist.models import get_woo_connection, AccountType
from purist.limitmonitor import ParserContainer, ServicesContainer
from limitmonitor.models import ExternalCredit, ExternalBundle, Limit
from limitmonitor.tunnel import TunnelManager
logger = logging.getLogger(__name__)
......@@ -41,10 +42,12 @@ def parse_subscription(json_entry):
% (quantity, id, ))
# calculate next renewal date
next_renewal_naive = datetime.datetime.strptime(
json_entry["next_payment_date"], "%Y-%m-%dT%H:%M:%S")
next_renewal = timezone.make_aware(next_renewal_naive)
if json_entry["next_payment_date"] != "":
next_renewal_naive = datetime.datetime.strptime(
json_entry["next_payment_date"], "%Y-%m-%dT%H:%M:%S")
next_renewal = timezone.make_aware(next_renewal_naive)
else:
next_renewal = datetime.datetime(1900, 1, 1, 0, 0)
# get account name
username = get_username_from_woo_customer_id(json_entry["customer_id"])
......@@ -56,8 +59,26 @@ def parse_subscription(json_entry):
else:
bundle_key = line_item.get('product_id')
# create result
# get account_type from subscription
line_item_meta = line_item["meta"]
for meta in line_item_meta:
if meta["key"] == "service-bundle":
acct_value = meta["value"]
break
else:
acct_value = None
account_type = AccountType.UNDEFINED
if acct_value:
acct_value = acct_value.lower()
if 'family' in acct_value:
account_type = AccountType.GROUP
elif 'complete' in acct_value:
account_type = AccountType.COMPLETE
elif 'basic' in acct_value:
account_type = AccountType.BASIC
# create result
result = {
"parser": ParserContainer.WOO_SUBSCRIPTION_V1,
"external_key": subscription_id,
......@@ -67,7 +88,8 @@ def parse_subscription(json_entry):
"quantity": 1,
"account": username,
"next_renewal": next_renewal,
"status": json_entry["status"]
"status": json_entry["status"],
"account_type": account_type
}
return result
......@@ -143,8 +165,10 @@ def forced_update(user):
parsed_sub = parse_subscription(sub)
if parsed_sub['bundle_key'] not in settings.WOOSUB1_PRODUCT_LIST:
logger.info('Subscription {} does not belong '
'to Librem One'.format(parsed_sub['bundle_key']))
logger.info('Subscription {} for user {} does not belong '
'to {}'.format(parsed_sub['bundle_key'],
user.username,
settings.SITE_TITLE))
continue # Skip this subscription as is not a product we care of
ext_cred = subscription_was_processed(parsed_sub, overwrite=True)
......@@ -189,7 +213,91 @@ def forced_update(user):
ext_cred.error_message = error
ext_cred.is_converted = is_converted
ext_cred.save()
if is_converted:
# Set account_type as specified by subscription
# when conversion succeeds.
user.account_type = parsed_sub['account_type']
user.save()
# TODO: process other subscription statuses
else:
logger.info('Subscription {} for user {} status is: {}'.format(
parsed_sub['label'], user.username, parsed_sub['status']))
def delete_account(user, purge_n=0, purge=False):
"""When called with purge=False it will remove user from all LDAP
groups, terminate tunnel account, delete WooCommerce account, mark
the user as inactive, set a gibberish LDAP password and set purge
value to purge_n.
When called with purge=True it will fully delete the user from
LDAP and from Django DB.
"""
# Get lowercase names of LDAP groups available from services
ldap_groups = [s.lower() for i, s in ServicesContainer.choices()]
# Remove undefined as there is no LDAP group with that name
ldap_groups.remove('undefined')
# Remove user from all LDAP groups
for ldap_group in ldap_groups:
user.remove_ldap_group(ldap_group)
# Terminate tunnel account
if user.has_tunnel_account():
try:
user.terminate_tunnel_account()
except TunnelManager.TerminateAccountError as e:
logger.error(repr(e))
# Delete WooCommerce account
user.delete_woocommerce_account()
# Set unusable password for LDAP
user.set_ldap_password(make_password(None))
# Mark user as deleted in Django
user.is_active = False
# Set purge value
user.purge = purge_n
user.save()
if purge:
ldap_user = user.get_ldap()
ldap_user.delete()
user.delete()
def get_users_expired_subs(date_limit=None):
"""Returns the list of users for which the service renewal_date is
older than date_limit. If date_limit is not specified
timezone.now() will be used. It will omit invited users, inactive
users and users created with magic carts.
"""
magic_carts = [0, 999, 5000, 5999]
if date_limit is None:
limit = timezone.now()
else:
limit = date_limit
limits = Limit.objects.filter(renewal_date__lte=limit)
users = []
for limit in limits:
user = limit.user
if hasattr(user, 'chosenreward'):
is_magic = user.chosenreward.reward in magic_carts
else:
is_magic = False
if (
user not in users and user.is_active
and user.account_type != AccountType.INVITED
and not is_magic
):
users.append(user)
return users
......@@ -2,7 +2,7 @@
<!--
Keel (LDH middleware)
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
https://source.puri.sm/liberty/ldh_middleware
SPDX-License-Identifier: AGPL-3.0
-->
......@@ -156,7 +156,8 @@ SPDX-License-Identifier: AGPL-3.0
<ul>
<li><a href="{% url 'password_change' %}">{% trans "Change password" %}</a></li>
<li><a href="{% url 'profile_configure' username %}">{% trans "Profile settings" %}</a></li>
<li><a href="{% url 'profile_configure' %}">{% trans "Profile settings" %}</a></li>
<li><a href="{% url 'delete_account' %}">{% trans "Delete Account" %}</a></li>
</ul>
</article>
......
......@@ -138,8 +138,10 @@ class Limit(models.Model):
def enable(self, ren_date):
if self.service == settings.LM_SERVICES.TUNNEL:
# TUNNEL special case requires user manual activation
self.is_active = False
# TUNNEL special case. It requires user manual activation
# the first time to trigger tunnel_account creation
if not self.user.has_tunnel_account():
self.is_active = False
elif self.service == settings.LM_SERVICES.CHAT:
self.user.set_ldap_group("chat")
self.is_active = True
......
......@@ -2,7 +2,8 @@ from django.conf import settings
from celery.utils.log import get_task_logger
from cart.models import ChosenReward
from limitmonitor.common import parse_subscription, get_user_subscriptions, \
subscription_was_processed, get_services_from_bundle
subscription_was_processed, get_services_from_bundle, forced_update, \
get_users_expired_subs
logger = get_task_logger(__name__)
......@@ -69,3 +70,10 @@ def process_pending_registrations():
for reg in pending_regs:
process_pending_registrations_user(reg.user)
def process_renewals():
expired_users = get_users_expired_subs()
for user in expired_users:
forced_update(user)
......@@ -38,3 +38,8 @@ def purge_users_pending_cart_task():
@shared_task
def process_pending_registrations():
subscription.process_pending_registrations()
@shared_task
def process_renewals():
subscription.process_renewals()
......@@ -34,7 +34,11 @@ def userlimit(request):
# Prepare context variables for Group service
invitations = {'list': []}
inv_all = request.user.invitation_set.all()
invitations['full'] = len(inv_all) >= 4
used_inv = 0
for inv in inv_all:
if inv.consumed or (not inv.consumed and not inv.expired()):
used_inv += 1
invitations['full'] = used_inv >= 4
invitations['no_members'] = True
invitations['links_not_used'] = False
for inv in inv_all:
......@@ -100,8 +104,14 @@ def toggle_tunnel(request):
@login_required
def new_invitation(request):
if request.user.account_type == AccountType.GROUP:
invs_count = len(request.user.invitation_set.all())
if invs_count < 4:
all_invs = request.user.invitation_set.all()
# Counter for invitations consumed and not consumed but
# not expired
used = 0
for inv in all_invs:
if inv.consumed or (not inv.consumed and not inv.expired()):
used += 1
if used < 4:
new_inv = Invitation.create(request.user)
new_inv.save()
return HttpResponseRedirect(reverse('profile'))
# Copyright 2017-2018 Purism SPC
# Copyright 2017-2019 Purism SPC
# SPDX-License-Identifier: AGPL-3.0-or-later
# Original file from Celery 4.0.2 documentation
......
......@@ -36,7 +36,6 @@ DEBUG_CHANGE_PASSWORD = config("DEBUG_CHANGE_PASSWORD", cast=bool)
DEBUG_SKIP_ACTIVATION_COMMAND = config("DEBUG_SKIP_ACTIVATION_COMMAND", cast=bool)
DEBUG_SKIP_VALIDATE_ON_AUTHENTICATION = config("DEBUG_SKIP_VALIDATE_ON_AUTHENTICATION", cast=bool)
DEBUG_REGISTER_STATUS = config("DEBUG_REGISTER_STATUS", cast=bool)
DEBUG_LOCAL_MAIL = config("DEBUG_LOCAL_MAIL", cast=bool)
DEBUG_REMOVE_CAPTCHA = config("DEBUG_REMOVE_CAPTCHA", cast=bool)
......@@ -161,7 +160,7 @@ SITE_BYLINE = config("SITE_BYLINE")
SITE_DOMAIN = config("SITE_DOMAIN")
SITE_PROVIDER = config("SITE_PROVIDER")
SITE_PROVIDER_LINK = config("SITE_PROVIDER_LINK")
EMAIL_DOMAIN = config("EMAIL_DOMAIN")
#
# WOOCOMMERCE
#
......@@ -289,4 +288,5 @@ EMAIL_USE_SSL = config('EMAIL_USE_SSL', False, cast=bool)
# This is the link to the subscribe page, used in different parts of the middleware
SUBSCRIBE_HREF = config('SUBSCRIBE_HREF')
# User Account last name
DEFAUL_USER_LASTNAME = config('DEFAUL_USER_LASTNAME', SITE_TITLE)
......@@ -25,8 +25,9 @@ from ldapregister.views import LdhLoginView
from cart.views import CartRegistrationView
from purist.views import Recovery, PasswordChange, \
PasswordChangeDone, ProfileConfigureView
PasswordChangeDone, ProfileConfigureView, RecoveryPasswordReset
from invitation.views import InvitationRegistrationView
from password_reset.views import reset_done, recover_done
#
# Set admin titles for this site
......@@ -49,16 +50,24 @@ urlpatterns = [
limitmonitor.views.toggle_tunnel, name='toggle_tunnel'),
url(r'^accounts/profile/new_invitation',
limitmonitor.views.new_invitation, name='new_invitation'),
url(r'^accounts/profile/configure/(?P<username>.+)/$',
url(r'^accounts/profile/configure',
ProfileConfigureView.as_view(), name='profile_configure'),
url(r'^accounts/profile/delete_account',
purist.views.DeleteAccountView.as_view(), name='delete_account'),
# url(r'^accounts/register/$', RegistrationView.as_view(form_class=RegistrationForm), name='registration_register'),
url(r'^accounts/login/$', LdhLoginView.as_view(), name='auth_login'),
url(r'^accounts/recover/$', Recovery.as_view(), name='password_reset_recover'),
url(r'^accounts/recover/(?P<signature>.+)/$', recover_done,
name='password_reset_sent'),
url(r'^accounts/recover/$', Recovery.as_view(),
name='password_reset_recover'),
url(r'^accounts/reset/done/$', reset_done, name='password_reset_done'),
url(r'^accounts/reset/(?P<token>[\w:-]+)/$',
RecoveryPasswordReset.as_view(),
name='password_reset_reset'),
url(r'^accounts/password_change/$', PasswordChange.as_view(),
name='password_change'),
url(r'^accounts/password_change_done/$', PasswordChangeDone.as_view(),
name='password_change_done'),
url(r'^accounts/', include('password_reset.urls')),
url(r'^accounts/', include('registration.backends.simple.urls')),
url(r'^download/', include('django_agpl.urls')),
url(r'^jslicense/$', purist.views.jslicense, name='jslicense'),
......
models.png

279 KB | W: | H:

models.png

356 KB | W: | H:

models.png
models.png
models.png
models.png
  • 2-up
  • Swipe
  • Onion skin
import logging
import re
from django.conf import settings
from django.contrib.auth.password_validation import MinimumLengthValidator as BaseValidator
from django.utils.translation import ugettext_lazy as _
from django_auth_ldap.backend import LDAPBackend as BaseBackend
from django.core.exceptions import ValidationError
from woocommerce import API as WOOCOMMERCE_API
from .models import User, UsernameValidator
......@@ -101,3 +103,13 @@ class PassphraseValidator(BaseValidator):
def get_help_text(self):
return _("A good passphrase is made of at least three long words.")
def validate(self, password, user=None):
super(PassphraseValidator, self).validate(password, user)
pattern = r'[\\\'\"]'
result = re.search(pattern, password)
if result is not None:
raise ValidationError(
_('\\ \' and \" are not valid characters'),
code='invalid_character',
)
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.forms import PasswordChangeForm \
as BasePasswordChangeForm
from django.contrib.auth import authenticate
from password_reset.forms import PasswordRecoveryForm as BasePasswordRecoveryForm
from django.contrib.auth.password_validation import validate_password
from password_reset.forms import PasswordRecoveryForm \
as BasePasswordRecoveryForm
from password_reset.forms import PasswordResetForm as BasePasswordResetForm
from .models import User
from captcha.fields import CaptchaField
class PasswordRecoveryForm(BasePasswordRecoveryForm):
def _validate_domain(self, value):
(user, domain) = value.split('@')
if domain != settings.SITE_DOMAIN:
raise ValidationError(
_('Only @{} addresses are allowed'.format(
settings.SITE_DOMAIN)),
params={'value': value},
)
def __init__(self, *args, **kwargs):
super(PasswordRecoveryForm, self).__init__(*args, **kwargs)
self.fields['username_or_email'] = forms.CharField(
label = _('Enter your recovery email')
self.fields['username_or_email'] = forms.EmailField(
label=_('Enter your {} address'.format(settings.SITE_TITLE)),
validators=[self._validate_domain],
)
# Override clean_username_or_email to search for username even
# when we are expecting email address in the form filed.
def clean_username_or_email(self):
email = self.cleaned_data['username_or_email']
username = email.split('@')[0]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise forms.ValidationError(_('That account is invalid or was not'
' found.'),
code='not_found')
self.cleaned_data['user'] = user
return username
class CaptchaPasswordRecoveryForm(PasswordRecoveryForm):
captcha = CaptchaField(
label=_('Please solve this math problem'),
help_text=_('Prove you are not a robot. Solve this addition,'
' subtraction or multiplication problem.'),
)
class RecoveryPasswordResetForm(BasePasswordResetForm):
def __init__(self, *args, **kwargs):
super(RecoveryPasswordResetForm, self).__init__(*args, **kwargs)
self.fields['password1'] = forms.CharField(
label=_('New password'),
widget=forms.PasswordInput,
validators=[validate_password],
)
......@@ -59,3 +108,18 @@ class ProfileConfigureForm(forms.ModelForm):
def save(self, commit=True):
self.instance.set_woocommerce_billing_email()
return super(ProfileConfigureForm, self).save(commit)
class DeleteAccountForm(forms.Form):
address = forms.EmailField(
label=_('If you are sure, please type in your {} address and '
'confirm:'.format(settings.SITE_TITLE)),
)
def clean_address(self):
address = self.cleaned_data['address']
if self.user.get_identity() != address:
raise forms.ValidationError(_('The address provided is not the '
'identity of the current user.'))
return address
<!DOCTYPE html>
<!--
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
https://source.puri.sm/liberty/ldh_middleware
SPDX-License-Identifier: AGPL-3.0
-->
......
<!DOCTYPE html>
<!--
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
https://source.puri.sm/liberty/ldh_middleware
SPDX-License-Identifier: AGPL-3.0
-->
......
from django.core.management.base import BaseCommand
from django.core.validators import EmailValidator, ValidationError
from django.conf import settings
from limitmonitor.common import delete_account
from purist.models import User
class Command(BaseCommand):
help = """Removes user from all LDAP groups, terminates tunnel account,
deletes WooCommerce account, marks the user as inactive, sets a
gibberish LDAP password and sets purge value to default value 0."""
def add_arguments(self, parser):
parser.add_argument('email', type=str,
help='This is the email of the user that will get \
deleted.')
parser.add_argument('--purge_n',
type=int,
help='Purge number to be used for the user. \
Default is 0.',
default=0)
parser.add_argument('--full',
action='store_true',
help='Also deletes the user from middleware DB \
and LDAP.')
def handle(self, *args, **options):
email = options['email']
purge_n = options['purge_n']
full = options['full']
ev = EmailValidator()
try:
ev(email)
except ValidationError as e:
self.stdout.write(repr(e))
return
(username, domain) = email.split('@')
if domain != settings.SITE_DOMAIN:
msg = '{} does not belong to {} domain'
self.stdout.write(msg.format(email, settings.SITE_DOMAIN))
return
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
msg = 'User {} does not exist.'.format(email)
self.stdout.write(msg)
return
msg = 'Deleting user {}...'.format(email)
self.stdout.write(msg)
delete_account(user, purge_n=purge_n, purge=full)
if full:
msg = 'User {} fully deleted.'.format(email)
else:
msg = 'User {} deleted and marked for purge {}.'.format(email,
purge_n)
self.stdout.write(msg)
from django.core.management.base import BaseCommand
from purist.models import User
import time
class Command(BaseCommand):
help = "Populate billing_email value for users that don't have it set. If \
the user has a WooCommerce email this value will be used. \
Otherwise the User.get_identity() identity will be used."
def add_arguments(self, parser):
parser.add_argument('--wait',
type=int,
help='Number of seconds to wait between users. \
Default is 0.',
default=0)
def handle(self, *args, **options):
wait_time = options['wait']
# Get all users with billing_email = ''
usr_empty = User.objects.filter(billing_email='')
self.stdout.write('Processing users with no billing_email')