| @ -1,22 +0,0 @@ | |||||
| from rest_framework import authentication | |||||
| from edivorce.apps.core.models import BceidUser | |||||
| class BCeIDAuthentication(authentication.BaseAuthentication): | |||||
| """ | |||||
| Make the DRF user the BCeID user populated in our middleware, to avoid DRF | |||||
| overwriting our user for API calls. | |||||
| This relies on our middleware entirely for authentication. | |||||
| """ | |||||
| def authenticate(self, request): | |||||
| try: | |||||
| request.user = request._user # pylint: disable=protected-access | |||||
| except: | |||||
| request.user = request._request.user # pylint: disable=protected-access | |||||
| return (request.user, None) | |||||
| def get_user(self, pk): | |||||
| return BceidUser.objects.get(pk=pk) | |||||
| @ -1,156 +0,0 @@ | |||||
| import datetime | |||||
| from ipaddress import ip_address, ip_network | |||||
| from django.conf import settings | |||||
| from django.shortcuts import redirect | |||||
| from django.utils import timezone | |||||
| from django.utils.deprecation import MiddlewareMixin | |||||
| from ..models import BceidUser | |||||
| login_delta = datetime.timedelta(hours=2) | |||||
| class AnonymousUser: | |||||
| """ | |||||
| Anonymous user, present mainly to provide authentication checks in templates | |||||
| """ | |||||
| guid = None | |||||
| display_name = '' | |||||
| has_accepted_terms = False | |||||
| @property | |||||
| def is_authenticated(self): | |||||
| return False | |||||
| @property | |||||
| def is_anonymous(self): | |||||
| return True | |||||
| anonymous_user = AnonymousUser() | |||||
| class BceidMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods | |||||
| """ | |||||
| Simple authentication middleware for operating in the BC Government | |||||
| OpenShift environment, with SiteMinder integration. | |||||
| For our purposes, SiteMinder is configured to add the following headers: | |||||
| BCeID: | |||||
| - SMGOV_USERGUID | |||||
| - SMGOV_USERDISPLAYNAME | |||||
| - SM_USER | |||||
| BC Services Card: | |||||
| - SMGOV_USERGUID | |||||
| - SMGOV_GIVENNAMES | |||||
| - SMGOV_SURNAME | |||||
| - SM_USER | |||||
| The first two are provided on pages configured to be protected by | |||||
| SiteMinder, which is currently just /login. When a user goes to the login | |||||
| page, if the user is logged in, SiteMinder adds those headers with their | |||||
| BCeID values; if they're not logged in, it routes them through its | |||||
| login/signup page and then back to the login page, with those headers in | |||||
| place. For unprotected pages, those headers are stripped if present, | |||||
| preventing spoofing. | |||||
| The third header is populated on every request that's proxied through | |||||
| SiteMinder. For logged in users, it contains their ???; for anonymous | |||||
| users, it's empty. | |||||
| When we detect authentication by the presence of the first two headers, we | |||||
| store those values in the user's session. On all requests, we use them to | |||||
| access a local proxy object for the user (available as request.user). For | |||||
| users that are not logged in, an Anonymous User substitute is present. | |||||
| In a local development environment, we generate a guid based on the login | |||||
| name and treat that guid/login name as guid/display name. | |||||
| """ | |||||
| def process_request(self, request): # pylint: disable=too-many-branches | |||||
| """ | |||||
| Return None after populating request.user, or necessary redirects. | |||||
| If the request is not coming from inside the BC Government data centre, | |||||
| redirect the request through the proxy server. | |||||
| If the SiteMinder headers are present, indicating the user has just | |||||
| authenticated, save those headers to the session. | |||||
| Get the user's GUID and display name. If they're present, and the user | |||||
| has authenticated (or we're in a local development environment), add | |||||
| the local proxy user to the request; if not, store the anonymous user | |||||
| instance. | |||||
| """ | |||||
| # HTTP_SM_USER is available on both secure and unsecure pages. If it | |||||
| # has a value then we know that the user is still logged into BCeID. | |||||
| # This is an additional check to make sure we aren't letting users | |||||
| # access the site via their session variables after logging out of bceid | |||||
| # | |||||
| # Note: It's still possible that a user has logged out of one BCeID and | |||||
| # logged into another BCeID via www.bceid.ca without clicking the logout | |||||
| # link on our app or closing the browser. This is an extreme edge case, | |||||
| # and it's not pragmatic to code against it at this time. | |||||
| siteminder_user = request.META.get('HTTP_SM_USER', '') | |||||
| is_localdev = settings.DEPLOYMENT_TYPE in ['localdev', 'minishift'] | |||||
| update_user = False | |||||
| using_bc_services_card = False | |||||
| guid = request.META.get('HTTP_SMGOV_USERGUID', '') | |||||
| given_names = request.META.get('HTTP_SMGOV_GIVENNAMES', '') | |||||
| surname = request.META.get('HTTP_SMGOV_SURNAME', '') | |||||
| displayname = request.META.get('HTTP_SMGOV_USERDISPLAYNAME', '') | |||||
| # HTTP_SMGOV_USERDISPLAYNAME is not included when BC Services Card authentication is used. | |||||
| if not displayname and (surname or given_names): | |||||
| displayname = "{0} {1}".format(given_names, surname) | |||||
| using_bc_services_card = True | |||||
| # HTTP_SM_USER is typically '.' when BC Services Card authentication is used. | |||||
| if (not siteminder_user or siteminder_user == '.') and given_names and surname: | |||||
| siteminder_user = "{0}{1}".format(given_names.split(None, 1)[0], surname) | |||||
| if guid: | |||||
| request.session['smgov_userguid'] = guid | |||||
| else: | |||||
| guid = request.session.get('smgov_userguid') | |||||
| if displayname: | |||||
| request.session['smgov_userdisplayname'] = displayname | |||||
| else: | |||||
| displayname = request.session.get('smgov_userdisplayname') | |||||
| if is_localdev: | |||||
| guid = request.session.get('fake_bceid_guid') | |||||
| displayname = request.session.get('login_name') | |||||
| if guid and (siteminder_user or is_localdev): | |||||
| request.user, created = BceidUser.objects.get_or_create(user_guid=guid) | |||||
| if created: | |||||
| request.session['first_login'] = True | |||||
| if siteminder_user: | |||||
| if created or not request.user.sm_user: | |||||
| request.user.sm_user = siteminder_user | |||||
| update_user = True | |||||
| if request.user.is_bcsc != using_bc_services_card: | |||||
| request.user.is_bcsc = using_bc_services_card | |||||
| update_user = True | |||||
| if request.user.display_name != displayname: | |||||
| request.user.display_name = displayname | |||||
| update_user = True | |||||
| if (request.user.last_login is None or | |||||
| timezone.now() - request.user.last_login > login_delta): | |||||
| request.user.last_login = timezone.now() | |||||
| update_user = True | |||||
| if update_user: | |||||
| request.user.save() | |||||
| else: | |||||
| request.user = anonymous_user | |||||
| return None | |||||
| @ -0,0 +1,39 @@ | |||||
| from mozilla_django_oidc.auth import OIDCAuthenticationBackend | |||||
| from ..models import BceidUser | |||||
| class EDivorceKeycloakBackend(OIDCAuthenticationBackend): | |||||
| def verify_claims(self, claims): | |||||
| verified = super(EDivorceKeycloakBackend, self).verify_claims(claims) | |||||
| print(claims) | |||||
| return verified | |||||
| def create_user(self, claims): | |||||
| user = super(EDivorceKeycloakBackend, self).create_user(claims) | |||||
| user.first_name = claims.get('given_name', '') | |||||
| user.last_name = claims.get('family_name', '') | |||||
| user.display_name = "{} {}".format(user.first_name, user.last_name).strip() | |||||
| user.sm_user = claims.get('preferred_username', '') | |||||
| user.user_guid = claims.get('universal-id', '') | |||||
| user.save() | |||||
| return user | |||||
| def update_user(self, user, claims): | |||||
| user.first_name = claims.get('given_name', '') | |||||
| user.last_name = claims.get('family_name', '') | |||||
| user.display_name = "{} {}".format(user.first_name, user.last_name).strip() | |||||
| user.sm_user = claims.get('preferred_username', '') | |||||
| user.user_guid = claims.get('universal-id', '') | |||||
| user.save() | |||||
| return user | |||||
| def filter_users_by_claims(self, claims): | |||||
| user_guid = claims.get('universal-id') | |||||
| if not user_guid: | |||||
| return self.UserModel.objects.none() | |||||
| return self.UserModel.objects.filter(user_guid=user_guid) | |||||
| @ -0,0 +1,20 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # Generated by Django 1.11.29 on 2020-10-15 17:38 | |||||
| from __future__ import unicode_literals | |||||
| from django.db import migrations, models | |||||
| class Migration(migrations.Migration): | |||||
| dependencies = [ | |||||
| ('core', '0019_auto_20191008_2141'), | |||||
| ] | |||||
| operations = [ | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='is_bcsc', | |||||
| field=models.BooleanField(default=False), | |||||
| ), | |||||
| ] | |||||
| @ -0,0 +1,102 @@ | |||||
| # Generated by Django 2.2.15 on 2020-10-09 21:12 | |||||
| import django.contrib.auth.models | |||||
| import django.contrib.auth.validators | |||||
| from django.db import migrations, models | |||||
| import django.utils.timezone | |||||
| def set_username(apps, schema_editor): | |||||
| Series = apps.get_model('core', 'bceiduser') | |||||
| for series in Series.objects.all().iterator(): | |||||
| series.username = 'user' + str(series.id) | |||||
| series.save() | |||||
| def reverse_func(apps, schema_editor): | |||||
| pass # code for reverting migration, if any | |||||
| class Migration(migrations.Migration): | |||||
| dependencies = [ | |||||
| ('core', '0023_auto_20201006_1314'), | |||||
| ('auth', '0011_update_proxy_permissions'), | |||||
| ] | |||||
| operations = [ | |||||
| migrations.AlterModelOptions( | |||||
| name='bceiduser', | |||||
| options={'verbose_name': 'user', 'verbose_name_plural': 'users'}, | |||||
| ), | |||||
| migrations.AlterModelManagers( | |||||
| name='bceiduser', | |||||
| managers=[ | |||||
| ('objects', django.contrib.auth.models.UserManager()), | |||||
| ], | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='email', | |||||
| field=models.EmailField(blank=True, max_length=254, verbose_name='email address'), | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='first_name', | |||||
| field=models.CharField(blank=True, max_length=30, verbose_name='first name'), | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='groups', | |||||
| field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='is_superuser', | |||||
| field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'), | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='last_name', | |||||
| field=models.CharField(blank=True, max_length=150, verbose_name='last name'), | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='password', | |||||
| field=models.CharField(default='', max_length=128, verbose_name='password'), | |||||
| preserve_default=False, | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='user_permissions', | |||||
| field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='username', | |||||
| field=models.CharField(blank=True, default='', max_length=150), | |||||
| preserve_default=False, | |||||
| ), | |||||
| migrations.RunPython(set_username, reverse_func), | |||||
| migrations.AlterField( | |||||
| model_name='bceiduser', | |||||
| name='username', | |||||
| field=models.CharField(default='', error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), | |||||
| preserve_default=False, | |||||
| ), | |||||
| migrations.AlterField( | |||||
| model_name='bceiduser', | |||||
| name='date_joined', | |||||
| field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'), | |||||
| ), | |||||
| migrations.AlterField( | |||||
| model_name='bceiduser', | |||||
| name='last_login', | |||||
| field=models.DateTimeField(blank=True, null=True, verbose_name='last login'), | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='bceiduser', | |||||
| name='is_staff', | |||||
| field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'), | |||||
| ), | |||||
| ] | |||||
| @ -0,0 +1,19 @@ | |||||
| # Generated by Django 2.2.15 on 2020-10-19 16:42 | |||||
| from django.db import migrations | |||||
| class Migration(migrations.Migration): | |||||
| dependencies = [ | |||||
| ('core', '0024_auto_20201009_1235'), | |||||
| ('core', '0024_bceiduser_is_bcsc'), | |||||
| ('core', '0020_bceiduser_is_bcsc'), | |||||
| ] | |||||
| operations = [ | |||||
| migrations.RemoveField( | |||||
| model_name='bceiduser', | |||||
| name='is_bcsc', | |||||
| ), | |||||
| ] | |||||
| @ -1,155 +0,0 @@ | |||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <title>Government of British Columbia</title> | |||||
| <meta charset="utf-8"/> | |||||
| <meta http-equiv="X-UA-Compatible" content="IE=Edge"/> | |||||
| <meta name="keywords" content=""/> | |||||
| <meta name="description" content=""/> | |||||
| <meta name="Author" content=""/> | |||||
| <link rel="icon" href="https://logon7.gov.bc.ca/clp/images/favicon.ico"> | |||||
| <title>CLP</title> | |||||
| <!-- mobile settings --> | |||||
| <meta name="viewport" | |||||
| content="width=device-width, maximum-scale=1, initial-scale=1, user-scalable=0"/> | |||||
| <!-- JAVASCRIPT FILES --> | |||||
| <script type="text/javascript" src="https://logon7.gov.bc.ca/clp/plugins/jquery-2.1.4.min.js"></script> | |||||
| <script type="text/javascript" src="https://logon7.gov.bc.ca/clp/plugins/bootstrap/js/bootstrap.min.js"></script> | |||||
| <script type="text/javascript" src="https://logon7.gov.bc.ca/clp/plugins/datepicker/js/bootstrap-datepicker.min.js"></script> | |||||
| <script type="text/javascript" src="https://logon7.gov.bc.ca/clp/js/common.js"></script> | |||||
| <script type="text/javascript" src="https://logon7.gov.bc.ca/clp/js/brandexample.js"></script> | |||||
| <script type="text/javascript" src="https://logon7.gov.bc.ca/clp/js/form-validation.js"></script> | |||||
| <script type="text/javascript" src="https://logon7.gov.bc.ca/clp/js/footer.js"></script> | |||||
| <!-- CORE CSS --> | |||||
| <link href="https://logon7.gov.bc.ca/clp/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> | |||||
| <link href="https://logon7.gov.bc.ca/clp/css/font-awesome.css" rel="stylesheet" type="text/css"/> | |||||
| <link href="https://logon7.gov.bc.ca/clp/css/main.css" rel="stylesheet" type="text/css"/> | |||||
| <link href="https://logon7.gov.bc.ca/clp/plugins/datepicker/css/bootstrap-datepicker3.min.css" rel="stylesheet" type="text/css"/> | |||||
| </head> | |||||
| <body> | |||||
| <div id="wrapper" class="main"> | |||||
| <header id="bcGov" class="no-gov-brand"> | |||||
| <div class="container"> | |||||
| <div class="seperator"></div> | |||||
| <div id="login-to" class="site-title">FAKE BCeID LOGIN – FOR SOFTWARE DEVELOPMENT PURPOSES ONLY</div> | |||||
| </div> | |||||
| </header> | |||||
| <header id="client"> | |||||
| <div class="container"> | |||||
| <div> | |||||
| <img src="https://logon7.gov.bc.ca/clp/branding/bceid_default_logo.jpg" class="logo" alt=""/> | |||||
| </div> | |||||
| </div> | |||||
| </header> | |||||
| <form method="POST" name="login" id="login-form" class="login-bceid-form" novalidate> | |||||
| <section class="container"> | |||||
| <div class="row"> | |||||
| <div class="col-sm-7 col-md-8"> | |||||
| <div class="panel"> | |||||
| <div class="panel-heading"> | |||||
| Log in with | |||||
| <span id="bceidLogo" class="environment-logo bceid-environment-devv2"></span> | |||||
| </div> | |||||
| <div class="panel-body"> | |||||
| <div class="form-element form-group"> | |||||
| <label class="control-label label-with-instruction" | |||||
| for="username">User ID</label> | |||||
| <div class="instruction">Use a Basic BCeID</div> | |||||
| <input name="user" id="username" type="text" size="20" autofocus | |||||
| class="form-control input-200" autocomplete="off"/> | |||||
| </div> | |||||
| <div class="form-element form-group"> | |||||
| <label class="control-label" for="password">Password</label> | |||||
| <input name="password" type="password" class="form-control input-200" | |||||
| id="password" size="20" autocomplete="off"/> | |||||
| </div> | |||||
| <div class="bg-error hidden"> | |||||
| <div class="help-block field-help-block"> | |||||
| <i class="fa fa-fw fa-exclamation-circle"></i><span | |||||
| class="field-help-text"></span> | |||||
| </div> | |||||
| </div> | |||||
| <div class="login-form-action"> | |||||
| <input type="submit" name="btnSubmit" class="btn btn-primary" | |||||
| value="Continue"/> | |||||
| </div> | |||||
| <div class="forgot"> | |||||
| <a href="{% url 'register' %}" class="link-forgot">Forgot your user ID or password?</a> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="panel"> | |||||
| <div class="panel-body" style="padding-bottom: 15px;"> | |||||
| <div> | |||||
| <strong>No account?</strong> | |||||
| </div> | |||||
| <a href="{% url 'register' %}" | |||||
| style="text-decoration: underline; font-size: 16px; font-weight: 600;">Register for a | |||||
| BCeID</a> | |||||
| </div> | |||||
| </div> | |||||
| <div> | |||||
| ◀ <a href="./" class="link-cancel">Cancel | |||||
| and return to localdev</a> | |||||
| </div> | |||||
| <hr class="visible-xs"> | |||||
| </div> | |||||
| <div class="col-sm-5 col-md-4"> | |||||
| </div> | |||||
| </div> | |||||
| </section> | |||||
| </form> | |||||
| </div> | |||||
| <footer class="footer-stick-to-bottom"> | |||||
| <div class="container" id="AccessMessage"> | |||||
| <div class="row"> | |||||
| <div class="col-sm-12"> | |||||
| <p>Access to or unauthorized use of data on this computer system by any person other than the authorized | |||||
| employee(s) or owner(s) of an account is strictly prohibited and may result in legal action against | |||||
| such person.</p> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div id="footerWrapper"> | |||||
| <div class="container"> | |||||
| <div class="row"> | |||||
| <div class="col-sm-12"> | |||||
| <ul class="inline"> | |||||
| <li data-order="1"><a href="http://www.gov.bc.ca/com/disclaimer.html">Disclaimer</a></li> | |||||
| <li data-order="2"><a href="http://www.gov.bc.ca/com/privacy.html">Privacy</a></li> | |||||
| <li data-order="3"><a href="http://www.gov.bc.ca/com/accessibility.html">Accessibility</a></li> | |||||
| <li data-order="4"><a href="http://www.gov.bc.ca/com/copyright.html">Copyright</a></li> | |||||
| </ul> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </footer> | |||||
| </body> | |||||
| </html> | |||||
| @ -1,16 +0,0 @@ | |||||
| <html> | |||||
| <head> | |||||
| <title>Debug</title> | |||||
| </head> | |||||
| <body> | |||||
| {% for k, v in request.META.items %} | |||||
| {% if k.isupper %} | |||||
| {{ k }} = {{ v }}<br/> | |||||
| {% endif %} | |||||
| {% endfor %} | |||||
| </body> | |||||
| </html> | |||||
| @ -1,17 +0,0 @@ | |||||
| <html> | |||||
| <head> | |||||
| <title>Register</title> | |||||
| </head> | |||||
| <body> | |||||
| <h1>localdev registration is not implemented</h1> | |||||
| <p>Enter <strong>any user id</strong> with the password <em><strong>divorce</strong></em> to | |||||
| simulate a login in your localdev envirommnent.</p> | |||||
| <a href="{% url 'login' %}">Goto Login</a> | |||||
| </body> | |||||
| </html> | |||||
| @ -1,32 +0,0 @@ | |||||
| import uuid | |||||
| import binascii | |||||
| from encodings.utf_8 import decode | |||||
| from django.conf import settings | |||||
| from django.shortcuts import render, redirect | |||||
| from django.views.decorators.csrf import csrf_exempt | |||||
| @csrf_exempt | |||||
| def bceid(request): | |||||
| """ fake bceid login for developer workstation environment """ | |||||
| if request.method == "POST": | |||||
| login_name = request.POST.get('user', '') | |||||
| password = request.POST.get('password', '') | |||||
| # just in case anyone from the general public discovers the dev server | |||||
| # make sure they don't accidentally login and think this is production | |||||
| if password.lower() != 'divorce': | |||||
| return redirect(settings.PROXY_BASE_URL + settings.FORCE_SCRIPT_NAME[:-1] + '/bceid') | |||||
| # convert the login name to a guid | |||||
| hex_name = decode(binascii.hexlify(str.encode(login_name)))[0] | |||||
| fake_guid = hex_name.rjust(32, '0') | |||||
| # save the guid in a session variable | |||||
| request.session['login_name'] = login_name | |||||
| request.session['fake_bceid_guid'] = fake_guid | |||||
| return redirect(settings.PROXY_BASE_URL + settings.FORCE_SCRIPT_NAME[:-1] + '/login') | |||||
| else: | |||||
| return render(request, 'localdev/bceid.html') | |||||
| @ -1,12 +1,12 @@ | |||||
| from django.conf.urls import url | from django.conf.urls import url | ||||
| from django.contrib.auth.decorators import login_required | |||||
| from edivorce.apps.poc import views | from edivorce.apps.poc import views | ||||
| from ..core.decorators import bceid_required | |||||
| urlpatterns = [ | urlpatterns = [ | ||||
| url(r'scan', bceid_required(views.UploadScan.as_view()), name="poc-scan"), | |||||
| url(r'hub', bceid_required(views.EfilingHubUpload.as_view()), name="poc-hub"), | |||||
| url(r'storage/doc/(?P<document_id>\d+)', bceid_required(views.view_document_file), name="poc-storage-download"), | |||||
| url(r'storage/delete/(?P<pk>\d+)', bceid_required(views.UploadStorageDelete.as_view()), name="poc-storage-delete"), | |||||
| url(r'storage', bceid_required(views.UploadStorage.as_view()), name="poc-storage"), | |||||
| url(r'scan', login_required(views.UploadScan.as_view()), name="poc-scan"), | |||||
| url(r'hub', login_required(views.EfilingHubUpload.as_view()), name="poc-hub"), | |||||
| url(r'storage/doc/(?P<document_id>\d+)', login_required(views.view_document_file), name="poc-storage-download"), | |||||
| url(r'storage/delete/(?P<pk>\d+)', login_required(views.UploadStorageDelete.as_view()), name="poc-storage-delete"), | |||||
| url(r'storage', login_required(views.UploadStorage.as_view()), name="poc-storage"), | |||||
| ] | ] | ||||