LDAP Integration

This commit is contained in:
amalelshihaby 2021-08-24 14:25:28 +02:00
parent 8ca5b2d104
commit 02ad7a9441
26 changed files with 424 additions and 190 deletions

View File

@ -3489,4 +3489,20 @@ body, html {
}
#card-errors {
color: #e41d25;
}
}
.bill-cancelled {
background-color: #e41d25 !important;
}
.bill-paid {
background-color: #28a745!important;
}
.bill-new {
background-color: #17a2b8!important;
}
.email_list .unverified {
color: #f7656e;
}
.email_list .primary {
color: #126567;
}

View File

@ -3,10 +3,15 @@ function fetch_pricing() {
var cores = $('#cores').val();
var memory = $('#memory').val();
var storage = $('#storage').val();
var country = $('select[name="country"]').val();
var data = { cores: cores, memory: memory, storage: storage};
if (country != undefined) {
data['country'] = country;
}
$.ajax({
type: 'GET',
url: url,
data: { cores: cores, memory: memory, storage: storage},
data: data,
dataType: 'json',
success: function (data) {
if (data && data['total']) {
@ -27,6 +32,20 @@ function fetch_pricing() {
});
};
function init_checkout_btn() {
var selected_opt = $('input[name="payment_card"]:checked').val();
if( selected_opt == 'new') {
$('#checkout-btn').hide();
$('#newcard').show();
} else if(selected_opt == undefined) {
$('#newcard').hide();
$('#checkout-btn').hide();
} else {
$('#newcard').hide();
$('#checkout-btn').show();
}
}
function incrementValue(e) {
var valueElement = $(e.target).parent().parent().find('input');
var step = $(valueElement).attr('step');
@ -147,15 +166,13 @@ $(document).ready(function () {
submitBillingForm();
});
init_checkout_btn();
$('input[name="payment_card"]').change(function(e) {
if($('input[name="payment_card"]:checked').val() == 'new') {
$('#checkout-btn').hide();
$('#newcard').show();
} else {
$('#newcard').hide();
$('#checkout-btn').show();
}
init_checkout_btn();
});
$('select[name="country"]').change(function(e) {
fetch_pricing();
});
});

View File

@ -22,7 +22,7 @@ def send_warning_email(bill, html_message):
next_run=timezone.now() + timedelta(hours=1))
def charge_open_bills():
un_paid_bills = Bill.objects.filter(is_closed=False)
un_paid_bills = Bill.objects.filter(status="new")
for bill in un_paid_bills:
date_diff = (date.today() - bill.due_date.date()).days
# If there is not enough money in the account 7 days before renewal, the system sends a warning
@ -47,8 +47,8 @@ def charge_open_bills():
if balance < 0:
payment = Payment.objects.create(owner=bill.owner, amount=balance, source='stripe')
if payment:
bill.close()
bill.close()
bill.close(status="paid")
bill.close(status="cancelled")
except Exception as e:
log.error(f"It seems that there is issue in payment for {bill.owner.name}", e)
# do nothing

View File

@ -1,4 +1,4 @@
{% load static i18n %} {% get_current_language as LANGUAGE_CODE %}
{% load static compress i18n %} {% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html lang="en">
@ -19,7 +19,9 @@
============================================= -->
<link href="{% static 'matrixhosting/css/bootstrap.min.css' %}" rel="stylesheet" />
<link href="{% static 'matrixhosting/css/fontawesome-all.min.css' %}" rel="stylesheet"type="text/css"/>
<link href="{% static 'matrixhosting/css/theme.css' %}" rel="stylesheet" type="text/css"/>
{% compress css %}
<link href="{% static 'matrixhosting/css/theme.css' %}" rel="stylesheet" type="text/css"/>
{% endcompress %}
<!-- Colors Css -->
</head>
<body>
@ -60,18 +62,31 @@
<div class="container my-4">
<div class="row">
<div class="col-11 col-lg-9 col-xl-8 mx-auto">
{% if messages %}
<div class="row">
{% for message in messages %}
{% if 'error' in message.tags %}
<div class="col-lg-12 alert alert-danger" role="alert">
<h4 class="alert-heading">{%trans "Error!" %}</h4>
<p class="mb-0">
{{ message|safe }}
</p>
</div>
{% else %}
<div class="col-lg-12 alert alert-success" role="alert">
<p class="mb-0 float-left">
{{ message|safe }}
</p>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% block content %}
{% endblock %}
{% if messages %}
<div>
<strong>Messages:</strong>
<ul>
{% for message in messages %}
<li>{{message}}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -0,0 +1,74 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "E-mail Addresses" %}{% endblock %}
{% block content %}
<h1>{% trans "E-mail Addresses" %}</h1>
{% if user.emailaddress_set.all %}
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
<form action="{% url 'account_email' %}" class="email_list" method="post">
{% csrf_token %}
<fieldset class="blockLabels">
{% for emailaddress in user.emailaddress_set.all %}
<div class="ctrlHolder">
<label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}">
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked"{%endif %} value="{{emailaddress.email}}"/>
{{ emailaddress.email }}
{% if emailaddress.verified %}
<span class="pl-2 verified">{% trans "Verified" %}</span>
{% else %}
<span class="pl-2 unverified">{% trans "Unverified" %}</span>
{% endif %}
{% if emailaddress.primary %}<span class="pl-2 primary">{% trans "Primary" %}</span>{% endif %}
</label>
</div>
{% endfor %}
<div class="buttonHolder">
<button class="btn btn-secendary" type="submit" name="action_primary" >{% trans 'Make Primary' %}</button>
<button class="btn btn-secendary" type="submit" name="action_send" >{% trans 'Re-send Verification' %}</button>
<!-- <button class="btn btn-primary" type="submit" name="action_remove" >{% trans 'Remove' %}</button> -->
</div>
</fieldset>
</form>
{% else %}
<p><strong>{% trans 'Warning:'%}</strong> {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}</p>
{% endif %}
<!-- {% if can_add_email %}
<h2>{% trans "Add E-mail Address" %}</h2>
<form method="post" action="{% url 'account_email' %}" class="add_email">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary" name="action_add" type="submit">{% trans "Add E-mail" %}</button>
</form>
{% endif %} -->
{% endblock %}
{% block extra_body %}
<script type="text/javascript">
(function() {
var message = "{% trans 'Do you really want to remove the selected e-mail address?' %}";
var actions = document.getElementsByName('action_remove');
if (actions.length) {
actions[0].addEventListener("click", function(e) {
if (! confirm(message)) {
e.preventDefault();
}
});
}
})();
</script>
{% endblock %}

View File

@ -5,7 +5,6 @@
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
{% block content %}
<h1>{% trans "Confirm E-mail Address" %}</h1>
@ -17,7 +16,7 @@
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
{% csrf_token %}
<button type="submit">{% trans 'Confirm' %}</button>
<button class="btn btn-primary" type="submit">{% trans 'Confirm' %}</button>
</form>
{% else %}

View File

@ -13,8 +13,22 @@
{% if form %}
<form method="POST" action="{{ action_url }}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="action" value="{% trans 'change password' %}"/>
{% if form.non_field_errors %}
<div class="p-2 my-4">
{{ form.non_field_errors }}
</div>
{% endif %}
<div class="form-group">
<label for="password1">{% trans "New Password (again)" %}</label>
<input type="password" class="form-control" name="password1" id="password1" {% if form.password1.value != None %}value="{{ form.password1.value }}"{% endif %} required placeholder="{% trans 'New Passowrd' %}">
{{ form.login.errors }}
</div>
<div class="form-group">
<label for="password2">{% trans "New Password" %}</label>
<input type="password" class="form-control" name="password2" id="password2" {% if form.password2.value != None %}value="{{ form.password2.value }}"{% endif %} required placeholder="{% trans 'New Passowrd (again)' %}">
{{ form.login.errors }}
</div>
<input class="btn btn-primary" type="submit" name="action" value="{% trans 'change password' %}"/>
</form>
{% else %}
<p>{% trans 'Your password is now changed.' %}</p>

View File

@ -3,10 +3,10 @@
{% load i18n %}
{% load account socialaccount %}
{% block head_title %}{% trans "Sign In" %}{% endblock %}
{% block head_title %}{% trans "Sign Up" %}{% endblock %}
{% block content %}
<h3 class="font-weight-400 mb-4">Log In</h3>
<h3 class="font-weight-400 mb-4">{% trans "Sign Up" %}</h3>
<form id="signup_form" method="post" action="{% url 'account_signup' %}">
{% csrf_token %}
{% if form.non_field_errors %}
@ -19,8 +19,22 @@
<input type="text" class="form-control" name="username" id="username" {% if form.username.value != None %}value="{{ form.username.value }}"{% endif %} required placeholder="{% trans 'Enter Username' %}">
{{ form.username.errors }}
</div>
<div class="form-row">
<div class="col-lg-6">
<div class="form-group">
<label for="first_name">{% trans "First Name" %}</label>
<input id="first_name" name="first_name" type="text" class="form-control" {% if form.first_name.value != None %}value="{{ form.first_name.value }}"{% endif %}placeholder="{% trans 'First Name' %}">
</div>
</div>
<div class="col-lg-6">
<div class="form-group">
<label for="first_name">{% trans "Last Name" %}</label>
<input id="last_name" name="last_name" type="text" class="form-control" {% if form.last_name.value != None %}value="{{ form.last_name.value }}"{% endif %}placeholder="{% trans 'Last Name' %}">
</div>
</div>
</div>
<div class="form-group">
<label for="email">{% trans "E-mail (optional)" %}</label>
<label for="email">{% trans "E-mail" %}</label>
<input type="text" class="form-control" name="email" id="email" {% if form.email.value != None %}value="{{ form.email.value }}"{% endif %} placeholder="{% trans 'Enter Email Address' %}">
{{ form.email.errors }}
</div>

View File

@ -9,7 +9,7 @@
<div class="row">
<div class="col-md-3">
<ul class="nav nav-tabs flex-column" id="myTabVertical" role="tablist">
<li class="nav-item"> <a class="nav-link" id="first-tab" href="{% url 'matrixhosting:billing' %}">{% trans "Payment Moves"%}</a> </li>
<li class="nav-item"> <a class="nav-link" id="first-tab" href="{% url 'matrixhosting:billing' %}">{% trans "Transaction History"%}</a> </li>
<li class="nav-item"> <a class="nav-link active" id="second-tab" href="{% url 'matrixhosting:bills' %}">{% trans "Bills"%}</a> </li>
<li class="nav-item"> <a class="nav-link" id="fourth-tab" href="{% url 'matrixhosting:cards' %}">{% trans "Payment Methods"%}</a> </li>
</ul>
@ -55,8 +55,8 @@
<div class="col-2 col-sm-2 text-center"><span class="">{% trans "Creation Date"%}</span></div>
<div class="col-2 col-sm-2 text-center">{% trans "Amount"%}</div>
<div class="col-2 col-sm-2 text-center">{% trans "Due Date"%}</div>
<div class="col-1 col-sm-1 text-center">{% trans "Closed"%}</div>
<div class="col-2 col-sm-2 text-center">{% trans "Download"%}</div>
<div class="col-2 col-sm-2 text-center">{% trans "Status"%}</div>
<div class="col-1 col-sm-1 text-center"></div>
</div>
</div>
<!-- Title End -->
@ -74,14 +74,10 @@
<div class="col-2 col-sm-2 text-center"> <span class="text-2 font-weight-300">{{bill.creation_date|date:"Y-m-d"}}</span></div>
<div class="col-2 col-sm-2 text-center"><span class="text-2 font-weight-300">{{bill.sum}}</span><span class="text-1 text-uppercase"> {{bill.currency}}</span></div>
<div class="col-2 col-sm-2 text-center"> <span class="text-2 font-weight-300">{{bill.due_date|date:"Y-m-d"}}</span> </div>
<div class="col-1 col-sm-1 text-center text-3">
{% if bill.is_closed %}
<span class="text-success" data-toggle="tooltip" data-original-title="Closed or Paid"><i class="fas fa-check-circle"></i></span>
{%else%}
<span class="text-danger" data-toggle="tooltip" data-original-title="Pending"><i class="text-danger fas fa-times-circle"></i></span>
{%endif%}
<div class="col-2 col-sm-2 text-center text-1">
<span class="px-2 py-1 text-white bill-{{bill.status}}" data-toggle="tooltip" data-original-title="{{bill.status}}">{{bill.get_status_display}}</span>
</div>
<div class="col-2 col-sm-2 text-center text-3">
<div class="col-1 col-sm-1 text-center text-3">
<form method="get" action="{% url 'matrix:invoice_download' bill_id=bill.id %}">
<button class="download-bill border border-primary" type="submit"><span class="text-primary" data-toggle="tooltip" data-original-title="Download"><i class="text-primary fas fa-file-download"></i></span></button>
</form>

View File

@ -9,7 +9,7 @@
<div class="row">
<div class="col-md-3">
<ul class="nav nav-tabs flex-column" id="myTabVertical" role="tablist">
<li class="nav-item"> <a class="nav-link" id="first-tab" href="{% url 'matrixhosting:billing' %}">{% trans "Payment Moves"%}</a> </li>
<li class="nav-item"> <a class="nav-link" id="first-tab" href="{% url 'matrixhosting:billing' %}">{% trans "Transaction History"%}</a> </li>
<li class="nav-item"> <a class="nav-link" id="second-tab" href="{% url 'matrixhosting:bills' %}">{% trans "Bills"%}</a> </li>
<li class="nav-item"> <a class="nav-link active" id="fourth-tab" href="{% url 'matrixhosting:cards' %}">{% trans "Payment Methods"%}</a> </li>
</ul>
@ -179,7 +179,9 @@
$.ajax({
type: 'DELETE',
url: url,
data: {csrfmiddlewaretoken: '{{ csrf_token }}'},
beforeSend: function(xhr) {
xhr.setRequestHeader("X-CSRFToken", '{{ csrf_token }}');
},
dataType: 'json',
success: function (result) {
location.reload();

View File

@ -4,7 +4,7 @@
<div class="bg-white rounded shadow-md pt-3">
<div class="price-text text-white bg-dark-3 text-center mt-3 p-3">
<p class="mb-0">
{% if matrix_vm_pricing.set_up_fees %}<span class="mr-1">Setup Fees</span>{{ matrix_vm_pricing.set_up_fees }} CHF<br>{% endif %}
{% if matrix_vm_pricing.set_up_fees %}<span class="mr-1">Setup Fees</span>{{ matrix_vm_pricing.set_up_fees }} CHF included<br>{% endif %}
{% if matrix_vm_pricing.discount_amount %}
{% trans "Discount" %} <span class="text-primary ml-1">{{ matrix_vm_pricing.discount_amount }}</span> CHF
{% endif %}
@ -54,8 +54,8 @@
{% if matrix_vm_pricing.discount_amount %}
<p class="text-muted mb-1">{% trans "You save" %} <span class="text-dark">{{ matrix_vm_pricing.discount_amount }}</span> CHF</p>
{% endif %}
<p class="text-muted mb-1">{% trans "Subtotal" %} <span id="subtotal" class="text-dark"></span> CHF/{% trans "month" %}</p>
<p class="text-muted text-3 mb-2">{% trans "Total" %}<span id="total" class="font-weight-500 text-primary pl-2"></span><span class="text-2 p-1">CHF/{% trans "month" %}</span></p>
<p class="text-muted mb-1">{% trans "Subtotal" %} <span id="subtotal" class="text-dark"></span> CHF</p>
<p class="text-muted text-3 mb-2">{% trans "Total" %}<span id="total" class="font-weight-500 text-primary pl-2"></span><span class="text-2 p-1">CHF</span></p>
<input type="hidden" name="pricing_name" id="pricing_name" value="{% if matrix_vm_pricing.name %}{{matrix_vm_pricing.name}}{% else %}unknown{% endif%}"></input>
<input type="submit" class="btn btn-primary btn-block" value="{% trans 'Continue' %}"></input>
</form>

View File

@ -186,9 +186,9 @@
{% with cards_len=cards|length %}
<p class="text-muted">
{% if cards_len > 0 %}
{% blocktrans %}You haven't enough balance in your wallet, Please select one of the cards that you used before or fill in your credit card information below.{% endblocktrans %}
{% blocktrans %}There is not enough balance in your account to proceed with this order. You can select a card or add a new card to fill up your account balance to proceed with the order.{% endblocktrans %}
{% else %}
{% blocktrans %}You haven't enough balance in your wallet, Please fill in your credit card information below.{% endblocktrans %}
{% blocktrans %}There is not enough balance in your account to proceed with this order. Please fill in your credit card information below.{% endblocktrans %}
{% endif %}
</p>
<div>
@ -229,7 +229,7 @@
</div>
<div id="has-enough-balance" {% if show_cards %}style="display:none;"{% endif %}>
<p class="text-muted">
{% blocktrans %}Your wallet has enough balance, Press Continue to fill the VM instance settings.{% endblocktrans %}
{% blocktrans %}You can use your account balance to make the payment. Press Continue to select the domain settings. You can review and confirm your order and payment in the next page.{% endblocktrans %}
</p>
<div class="text-right">
<button id="continue-btn" class="btn btn-primary btn-wide">{%trans "Continue" %}</button>

View File

@ -9,7 +9,7 @@
<div class="row">
<div class="col-md-3">
<ul class="nav nav-tabs flex-column" id="myTabVertical" role="tablist">
<li class="nav-item"> <a class="nav-link active" id="first-tab" href="{% url 'matrixhosting:billing' %}">{% trans "Payment Moves"%}</a> </li>
<li class="nav-item"> <a class="nav-link active" id="first-tab" href="{% url 'matrixhosting:billing' %}">{% trans "Transaction History"%}</a> </li>
<li class="nav-item"> <a class="nav-link" id="second-tab" href="{% url 'matrixhosting:bills' %}">{% trans "Bills"%}</a> </li>
<li class="nav-item"> <a class="nav-link" id="fourth-tab" href="{% url 'matrixhosting:cards' %}">{% trans "Payment Methods"%}</a> </li>
</ul>

View File

@ -24,6 +24,6 @@ def finalize_order(request, customer, billing_address,
if payment:
#Close the bill as the payment has been added
VMInstance.create_instance(order)
bill.close()
bill.close(status="paid")
return order, bill

View File

@ -30,6 +30,7 @@ import uncloud_pay.stripe as uncloud_stripe
from .models import VMInstance
from .serializers import *
from .utils import *
from ldap.ldapobject import LDAPObject
logger = logging.getLogger(__name__)

View File

@ -1,6 +1,6 @@
ALLOWED_HOSTS=
STRIPE_KEY=
STRIPE_PUBLIC_KEY=p
STRIPE_PUBLIC_KEY=
DATABASE_ENGINE=django.db.backends.sqlite3
DATABASE_NAME=
DATABASE_HOST=
@ -16,4 +16,14 @@ GITLAB_PROJECT_ID=
GITLAB_OAUTH_TOKEN=
GITLAB_AUTHOR_EMAIL=
GITLAB_AUTHOR_NAME=
WKHTMLTOPDF_CMD=/usr/local/bin/wkhtmltopdf
WKHTMLTOPDF_CMD=
LDAP_DEFAULT_START_UID=
AUTH_LDAP_SERVER_HOST=
AUTH_LDAP_SERVER_URI=
AUTH_LDAP_BIND_DN=
AUTH_LDAP_BIND_PASSWORD=
LDAP_ADMIN_DN=
LDAP_ADMIN_PASSWORD=
LDAP_CUSTOMER_GROUP_ID=
LDAP_CUSTOMER_DN=

View File

@ -1,8 +1,8 @@
from django import forms
from django.contrib.auth.models import User
class UserDeleteForm(forms.ModelForm):
class Meta:
model = User
fields = []

View File

@ -19,8 +19,19 @@ import environ
from django.core.management.utils import get_random_secret_key
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
LOGGING = {}
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'root': {
'handlers': ['console'],
'level': 'DEBUG',
},
}
# Initialise environment variables
env = environ.Env()
@ -135,25 +146,40 @@ AUTH_PASSWORD_VALIDATORS = [
# Authall Settings
ACCOUNT_AUTHENTICATION_METHOD = "username"
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1
ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = "optional"
ACCOUNT_UNIQUE_EMAIL = False
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
MAX_EMAIL_ADDRESSES = 1
################################################################################
# AUTH/LDAP
AUTH_LDAP_SERVER_URI = ""
AUTH_LDAP_BIND_DN = ""
AUTH_LDAP_BIND_PASSWORD = ""
AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com",
LDAP_ENABLED = True
AUTH_LDAP_SERVER_HOST = env('AUTH_LDAP_SERVER_HOST')
AUTH_LDAP_SERVER_URI = env('AUTH_LDAP_SERVER_URI')
AUTH_LDAP_BIND_DN = env('AUTH_LDAP_BIND_DN')
AUTH_LDAP_BIND_PASSWORD = env('AUTH_LDAP_BIND_PASSWORD')
AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=customers,dc=ungleich,dc=ch"
AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=customers,dc=ungleich,dc=ch",
ldap.SCOPE_SUBTREE,
"(uid=%(user)s)")
# BIND_AS_AUTHENTICATING_USER = True
START_TLS = True
LDAP_ADMIN_DN = env("LDAP_ADMIN_DN")
LDAP_ADMIN_PASSWORD = env("LDAP_ADMIN_PASSWORD")
LDAP_CUSTOMER_GROUP_ID = env("LDAP_CUSTOMER_GROUP_ID")
LDAP_CUSTOMER_DN=env("LDAP_CUSTOMER_DN")
#AUTH_LDAP_USER_QUERY_FIELD = "email"
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"first_name": "cn",
"last_name": "sn",
"email": "mail"
}
LDAP_DEFAULT_START_UID = int(env('LDAP_DEFAULT_START_UID'))
LDAP_MAX_UID_FILE_PATH = os.environ.get('LDAP_MAX_UID_FILE_PATH',
os.path.join(os.path.abspath(os.path.dirname(__file__)), 'ldap_max_uid_file')
)
################################################################################
# AUTH/Django
AUTHENTICATION_BACKENDS = [
@ -162,9 +188,16 @@ AUTHENTICATION_BACKENDS = [
'allauth.account.auth_backends.AuthenticationBackend',
]
AUTH_USER_MODEL = 'uncloud_auth.User'
AUTH_USER_MODEL = 'uncloud_auth.User'
ACCOUNT_FORMS = {
'signup': 'uncloud_auth.forms.MySignupForm',
'change_password': 'uncloud_auth.forms.MyChangePasswordForm',
'set_password': 'uncloud_auth.forms.MySetPasswordForm',
'reset_password_from_key': 'uncloud_auth.forms.MyResetPasswordKeyForm',
}
################################################################################
# AUTH/REST
REST_FRAMEWORK = {
@ -233,26 +266,14 @@ UNCLOUD_ADMIN_NAME = "uncloud-admin"
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
# replace these in local_settings.py
AUTH_LDAP_SERVER_URI = "ldaps://ldap1.example.com,ldaps://ldap2.example.com"
AUTH_LDAP_BIND_DN="uid=django,ou=system,dc=example,dc=com"
AUTH_LDAP_BIND_PASSWORD="a very secure ldap password"
AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com",
ldap.SCOPE_SUBTREE,
"(uid=%(user)s)")
# where to create customers
LDAP_CUSTOMER_DN="ou=customer,dc=example,dc=com"
EMAIL_USE_TLS = True
EMAIL_HOST = env('EMAIL_HOST')
EMAIL_PORT = 25
EMAIL_HOST_USER = DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = 'support@ungleich.ch'
RENEWAL_FROM_EMAIL = 'support@ungleich.ch'
# Should be removed in production
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
##############
# Jobs

69
uncloud_auth/forms.py Normal file
View File

@ -0,0 +1,69 @@
import logging
from allauth.account.forms import SignupForm, ChangePasswordForm, ResetPasswordKeyForm, SetPasswordForm
from django import forms as d_forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from .ungleich_ldap import LdapManager
logger = logging.getLogger(__name__)
class MySignupForm(SignupForm):
first_name = d_forms.CharField(max_length=30)
last_name = d_forms.CharField(max_length=30)
def custom_signup(self, request, user):
user.first_name = self.cleaned_data["first_name"]
user.last_name = self.cleaned_data["last_name"]
user.save()
if settings.LDAP_ENABLED:
ldap_manager = LdapManager()
try:
user_exists_in_ldap, entries = ldap_manager.check_user_exists(user.username)
except Exception:
logger.exception("Exception occur while searching for user in LDAP")
else:
if not user_exists_in_ldap:
ldap_manager.create_user(user.username, user.password, user.first_name, user.last_name, user.email)
class MyResetPasswordKeyForm(ResetPasswordKeyForm):
def save(self):
ldap_manager = LdapManager()
try:
user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.user.username)
if not user_exists_in_ldap:
super(MyResetPasswordKeyForm, self).save()
else:
if ldap_manager.change_password(entries[0].entry_dn, self.cleaned_data["password1"]):
super(MyResetPasswordKeyForm, self).save()
except Exception:
logger.exception("Exception occur while searching for user in LDAP")
raise d_forms.ValidationError(_("An error occurred, please try again later"))
class MyChangePasswordForm(ChangePasswordForm):
def save(self):
ldap_manager = LdapManager()
try:
user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.user.username)
if not user_exists_in_ldap:
super(MyChangePasswordForm, self).save()
else:
if ldap_manager.change_password(self.user.username, self.cleaned_data["password1"]):
super(MyChangePasswordForm, self).save()
except Exception:
logger.exception("Exception occur while searching for user in LDAP")
raise d_forms.ValidationError(_("An error occurred, please try again later"))
class MySetPasswordForm(SetPasswordForm):
def save(self):
ldap_manager = LdapManager()
try:
user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.user.username)
if not user_exists_in_ldap:
super(MySetPasswordForm, self).save()
else:
if ldap_manager.change_password(self.user.username, self.cleaned_data["password1"]):
super(MySetPasswordForm, self).save()
except Exception:
logger.exception("Exception occur while searching for user in LDAP")
raise d_forms.ValidationError(_("An error occurred, please try again later"))

View File

@ -1,42 +0,0 @@
import ldap
# from django.conf import settings
AUTH_LDAP_SERVER_URI = "ldaps://ldap1.ungleich.ch,ldaps://ldap2.ungleich.ch"
AUTH_LDAP_BIND_DN="uid=django-create,ou=system,dc=ungleich,dc=ch"
AUTH_LDAP_BIND_PASSWORD="kS#e+v\zjKn]L!,RIu2}V+DUS"
# AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=ungleich,dc=ch",
# ldap.SCOPE_SUBTREE,
# "(uid=%(user)s)")
ldap_object = ldap.initialize(AUTH_LDAP_SERVER_URI)
cancelid = ldap_object.bind(AUTH_LDAP_BIND_DN, AUTH_LDAP_BIND_PASSWORD)
res = ldap_object.search_s("dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=nico)")
print(res)
# class LDAP(object):
# """
# Managing users in LDAP
# Requires the following settings?
# LDAP_USER_DN: where to create users in the tree
# LDAP_ADMIN_DN: which DN to use for managing users
# LDAP_ADMIN_PASSWORD: which password to used
# This module will reuse information from djagno_auth_ldap, including:
# AUTH_LDAP_SERVER_URI
# """
# def __init__(self):
# pass
# def create_user(self):
# pass
# def change_password(self):
# pass

View File

@ -4,7 +4,9 @@ import logging
import random
import ldap3
from ldap3 import ALL
from django.conf import settings
from django.contrib.auth.hashers import make_password
logger = logging.getLogger(__name__)
@ -21,7 +23,7 @@ class LdapManager:
Initialize the LDAP subsystem.
"""
self.rng = random.SystemRandom()
self.server = ldap3.Server(settings.AUTH_LDAP_SERVER)
self.server = ldap3.Server(settings.AUTH_LDAP_SERVER_HOST, use_ssl=True, get_info=ALL)
def get_admin_conn(self):
@ -32,6 +34,7 @@ class LdapManager:
conn = self.get_conn(user=settings.LDAP_ADMIN_DN,
password=settings.LDAP_ADMIN_PASSWORD,
raise_exceptions=True)
conn.start_tls()
conn.bind()
return conn
@ -58,7 +61,6 @@ class LdapManager:
base64 or decoding it to unicode from ``ascii``.
"""
SALT_BYTES = 15
sha1 = hashlib.sha1()
salt = self.rng.getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES,
"little")
@ -67,10 +69,11 @@ class LdapManager:
digest = sha1.digest()
passwd = b"{SSHA}" + base64.b64encode(digest + salt)
return passwd
def create_user(self, user, password, firstname, lastname, email):
def create_user(self, username, password, firstname, lastname, email):
conn = self.get_admin_conn()
uidNumber = self._get_max_uid() + 1
logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber))
@ -91,7 +94,7 @@ class LdapManager:
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
self._set_max_uid(uidNumber)
try:
uid = user # user.encode("utf-8")
uid = username
conn.add("uid={uid},{customer_dn}".format(
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
),
@ -105,54 +108,43 @@ class LdapManager:
"uidNumber": [str(uidNumber)],
"gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)],
"loginShell": ["/bin/bash"],
"homeDirectory": ["/home/{}".format(user).encode("utf-8")],
"homeDirectory": ["/home/{}".format(username).encode("utf-8")],
"mail": email.encode("utf-8"),
"userPassword": [self._ssha_password(
password.encode("utf-8")
)]
"userPassword": [self._ssha_password(password.encode("utf-8"))]
}
)
logger.debug('Created user %s %s' % (user.encode('utf-8'),
logger.debug('Created user %s %s' % (username.encode('utf-8'),
uidNumber))
except Exception as ex:
logger.debug('Could not create user %s' % user.encode('utf-8'))
logger.debug('Could not create user %s' % username.encode('utf-8'))
logger.error("Exception: " + str(ex))
raise
finally:
conn.unbind()
def change_password(self, uid, new_password):
def change_password(self, entry_dn, new_password):
"""
Changes the password of the user identified by user_dn
:param uid: str The uid that identifies the user
:param entry_dn: str The dn that identifies the user
:param new_password: str The new password string
:return: True if password was changed successfully False otherwise
"""
conn = self.get_admin_conn()
# Make sure the user exists first to change his/her details
user_exists, entries = self.check_user_exists(
uid=uid,
search_base=settings.ENTIRE_SEARCH_BASE
)
return_val = False
if user_exists:
try:
return_val = conn.modify(
entries[0].entry_dn,
{
"userpassword": (
ldap3.MODIFY_REPLACE,
[self._ssha_password(new_password.encode("utf-8"))]
)
}
)
except Exception as ex:
logger.error("Exception: " + str(ex))
else:
logger.error("User {} not found".format(uid))
try:
return_val = conn.modify(
entry_dn,
{
"userpassword": (
ldap3.MODIFY_REPLACE,
[self._ssha_password(new_password.encode("utf-8"))]
)
}
)
except Exception as ex:
logger.error("Exception: " + str(ex))
conn.unbind()
return return_val
@ -173,7 +165,7 @@ class LdapManager:
# Make sure the user exists first to change his/her details
user_exists, entries = self.check_user_exists(
uid=uid,
search_base=settings.ENTIRE_SEARCH_BASE
search_base=settings.LDAP_CUSTOMER_DN
)
return_val = False

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.4 on 2021-08-19 13:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('uncloud_pay', '0030_pricingplan_monthly_maintenance_fees'),
]
operations = [
migrations.RemoveField(
model_name='bill',
name='is_closed',
),
migrations.AddField(
model_name='bill',
name='status',
field=models.CharField(choices=[('new', 'New'), ('cancelled', 'Cancelled'), ('paid', 'Paid')], default='new', max_length=32),
),
]

View File

@ -27,6 +27,12 @@ from .services import *
# Used to generate bill due dates.
BILL_PAYMENT_DELAY=datetime.timedelta(days=settings.BILL_PAYMENT_DELAY)
EU_COUNTRIES = ['at', 'be', 'bg', 'ch', 'cy', 'cz', 'hr', 'dk',
'ee', 'fi', 'fr', 'mc', 'de', 'gr', 'hu', 'ie', 'it',
'lv', 'lu', 'mt', 'nl', 'po', 'pt', 'ro','sk', 'si', 'es',
'se', 'gb']
# Initialize logger.
logger = logging.getLogger(__name__)
@ -254,41 +260,26 @@ class VATRate(models.Model):
description = models.TextField(blank=True, default='')
@staticmethod
def get_for_country(country_code):
vat_rate = None
try:
vat_rate = VATRate.objects.get(
territory_codes=country_code, start_date__isnull=False, stop_date=None
)
return vat_rate.rate
except VATRate.DoesNotExist as dne:
logger.debug(str(dne))
logger.debug("Did not find VAT rate for %s, returning 0" % country_code)
return 0
@staticmethod
def get_vat_rate(billing_address, when=None):
def get_vat_rate_for_country(country, when=None):
"""
Returns the VAT rate for business to customer.
B2B is always 0% with the exception of trading within the own country
"""
country = billing_address.country
# Need to have a provider country
providers = UncloudProvider.objects.all()
vatrate = filter_for_when(VATRate.objects.filter(territory_codes=country), when).first()
if not providers and not vatrate:
return 0
uncloud_provider = filter_for_when(providers).get()
# By default we charge VAT. This affects:
# - Same country sales (VAT applied)
# - B2C to EU (VAT applied)
rate = vatrate.rate if vatrate else 0
if not country.lower().strip() in EU_COUNTRIES:
rate = 0
return rate
@staticmethod
def get_vat_rate(billing_address, when=None):
rate = VATRate.get_vat_rate_for_country(billing_address.country, when)
# Exception: if...
# - the billing_address is in EU,
@ -296,7 +287,7 @@ class VATRate(models.Model):
# - the vat_number has been verified
# Then we do not charge VAT
if uncloud_provider.country != country and billing_address.vat_number and billing_address.vat_number_verified:
if rate != 0 and billing_address.vat_number and billing_address.vat_number_verified:
rate = 0
return rate
@ -408,7 +399,7 @@ class Product(models.Model):
def __str__(self):
return f"{self.name} - {self.description}"
return f"{self.name}"
@property
def recurring_orders(self):
@ -1010,7 +1001,11 @@ class Bill(models.Model):
# FIXME: editable=True -> is in the admin, but also editable in DRF
# Maybe filter fields in the serializer?
is_closed = models.BooleanField(default=False)
status = models.CharField(max_length=32, choices= (
('new', 'New'),
('cancelled', 'Cancelled'),
('paid', 'Paid')
), null=False, blank=False, default="new")
class Meta:
constraints = [
@ -1020,11 +1015,11 @@ class Bill(models.Model):
name='one_bill_per_month_per_user')
]
def close(self):
def close(self, status):
"""
Close/finish a bill
"""
self.is_closed = True
self.status = status
if not self.ending_date:
self.ending_date = timezone.now()
self.save()
@ -1120,7 +1115,7 @@ class Bill(models.Model):
# Get date & bill from previous bill, if it exists
if last_bill:
if not last_bill.is_closed:
if last_bill.status == 'new':
bill = last_bill
starting_date = last_bill.starting_date
ending_date = bill.ending_date

View File

@ -122,7 +122,7 @@ class BillSerializer(serializers.ModelSerializer):
model = Bill
fields = ['owner', 'sum', 'vat_rate',
'due_date', 'creation_date', 'starting_date', 'ending_date',
'records', 'is_closed', 'billing_address']
'records', 'status', 'billing_address']
# We do not want users to mutate the country / VAT number of an address, as it
# will change VAT on existing bills.

View File

@ -425,7 +425,7 @@ class BillTestCase(TestCase):
self.assertEqual(record.quantity, 1)
self.assertEqual(record.sum, 35)
#close the bill as it has been paid
bill.close()
bill.close(status="paid")
bill2 = Bill.create_next_bill_for_user_address(self.user_addr)
self.assertNotEqual(bill.id, bill2.id)
self.assertEqual(order.billrecord_set.count(), 2)
@ -483,7 +483,7 @@ class BillTestCase(TestCase):
for ending_date in self.bill_dates:
b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date)
b.close()
b.close(status="paid")
bill_count = Bill.objects.filter(owner=self.recurring_user).count()
@ -519,14 +519,28 @@ class VATRatesTestCase(TestCase):
street="unknown",
city="unknown",
postal_code="unknown",
country="CH",
active=True)
UncloudNetwork.populate_db_defaults()
UncloudProvider.populate_db_defaults()
VATRate.objects.create(territory_codes="CH", currency_code="CHF", rate=7.7,
starting_date=timezone.make_aware(datetime.datetime(2000,1,1)))
def test_get_rate_for_user(self):
"""
Raise an error, when there is no address
"""
rate = VATRate.get_vat_rate(self.user_addr)
self.assertEqual(rate, 7.7)
self.user_addr.vat_number_verified = True
self.user_addr.vat_number = "11111"
rate1 = VATRate.get_vat_rate(self.user_addr)
self.assertEqual(rate1, 0)
rate2 = VATRate.get_vat_rate_for_country('CH')
self.assertEqual(rate, 7.7)
rate2 = VATRate.get_vat_rate_for_country('EG')
self.assertEqual(rate2, 0)

View File

@ -41,11 +41,16 @@ class PricingView(View):
vat_rate = False
vat_validation_status = False
address = False
selected_country = request.GET.get('country', False)
if self.request.user and self.request.user.is_authenticated:
address = get_billing_address_for_user(self.request.user)
if address:
address = get_billing_address_for_user(self.request.user)
if address and (address.country == selected_country or not selected_country):
vat_rate = VATRate.get_vat_rate(address)
vat_validation_status = "verified" if address.vat_number_validated_on and address.vat_number_verified else False
elif selected_country:
vat_rate = VATRate.get_vat_rate_for_country(selected_country)
vat_validation_status = False
pricing = get_order_total_with_vat(
request.GET.get('cores'),
request.GET.get('memory'),