Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
CIRCLE
/
cloud
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
94
Merge Requests
10
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
0accbbd3
authored
Aug 08, 2016
by
Kálmán Viktor
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
dashboard: basic 2fa profile settings
parent
a8272957
Pipeline
#160
failed with stage
in 0 seconds
Changes
11
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
226 additions
and
6 deletions
+226
-6
circle/circle/settings/base.py
+2
-0
circle/dashboard/forms.py
+37
-1
circle/dashboard/migrations/0005_profile_two_factor_secret.py
+19
-0
circle/dashboard/models.py
+4
-0
circle/dashboard/static/dashboard/dashboard.less
+16
-0
circle/dashboard/static/dashboard/profile.js
+0
-1
circle/dashboard/templates/dashboard/disable-two-factor.html
+30
-0
circle/dashboard/templates/dashboard/enable-two-factor.html
+53
-0
circle/dashboard/templates/dashboard/profile_form.html
+22
-0
circle/dashboard/urls.py
+5
-0
circle/dashboard/views/user.py
+38
-4
No files found.
circle/circle/settings/base.py
View file @
0accbbd3
...
@@ -567,3 +567,5 @@ BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "")
...
@@ -567,3 +567,5 @@ BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "")
REQUEST_HOOK_URL
=
get_env_variable
(
"REQUEST_HOOK_URL"
,
""
)
REQUEST_HOOK_URL
=
get_env_variable
(
"REQUEST_HOOK_URL"
,
""
)
SSHKEY_EMAIL_ADD_KEY
=
False
SSHKEY_EMAIL_ADD_KEY
=
False
TWO_FACTOR_ISSUER
=
get_env_variable
(
"TWO_FACTOR_ISSUER"
,
"CIRCLE"
)
circle/dashboard/forms.py
View file @
0accbbd3
...
@@ -20,6 +20,8 @@ from __future__ import absolute_import
...
@@ -20,6 +20,8 @@ from __future__ import absolute_import
from
datetime
import
timedelta
from
datetime
import
timedelta
from
urlparse
import
urlparse
from
urlparse
import
urlparse
import
pyotp
from
django.forms
import
ModelForm
from
django.forms
import
ModelForm
from
django.contrib.auth.forms
import
(
from
django.contrib.auth.forms
import
(
AuthenticationForm
,
PasswordResetForm
,
SetPasswordForm
,
AuthenticationForm
,
PasswordResetForm
,
SetPasswordForm
,
...
@@ -1298,21 +1300,29 @@ class UserEditForm(forms.ModelForm):
...
@@ -1298,21 +1300,29 @@ class UserEditForm(forms.ModelForm):
instance_limit
=
forms
.
IntegerField
(
instance_limit
=
forms
.
IntegerField
(
label
=
_
(
'Instance limit'
),
label
=
_
(
'Instance limit'
),
min_value
=
0
,
widget
=
NumberInput
)
min_value
=
0
,
widget
=
NumberInput
)
two_factor_secret
=
forms
.
CharField
(
label
=
_
(
'Two-factor authentication secret'
),
help_text
=
_
(
"Remove the secret key to disable two-factor "
"authentication for this user."
),
required
=
False
)
def
__init__
(
self
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
UserEditForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
super
(
UserEditForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
"instance_limit"
]
.
initial
=
(
self
.
fields
[
"instance_limit"
]
.
initial
=
(
self
.
instance
.
profile
.
instance_limit
)
self
.
instance
.
profile
.
instance_limit
)
self
.
fields
[
"two_factor_secret"
]
.
initial
=
(
self
.
instance
.
profile
.
two_factor_secret
)
class
Meta
:
class
Meta
:
model
=
User
model
=
User
fields
=
(
'email'
,
'first_name'
,
'last_name'
,
'instance_limit'
,
fields
=
(
'email'
,
'first_name'
,
'last_name'
,
'instance_limit'
,
'is_active'
)
'is_active'
,
"two_factor_secret"
,
)
def
save
(
self
,
commit
=
True
):
def
save
(
self
,
commit
=
True
):
user
=
super
(
UserEditForm
,
self
)
.
save
()
user
=
super
(
UserEditForm
,
self
)
.
save
()
user
.
profile
.
instance_limit
=
(
user
.
profile
.
instance_limit
=
(
self
.
cleaned_data
[
'instance_limit'
]
or
None
)
self
.
cleaned_data
[
'instance_limit'
]
or
None
)
user
.
profile
.
two_factor_secret
=
(
self
.
cleaned_data
[
'two_factor_secret'
]
or
None
)
user
.
profile
.
save
()
user
.
profile
.
save
()
return
user
return
user
...
@@ -1650,3 +1660,29 @@ class MessageForm(ModelForm):
...
@@ -1650,3 +1660,29 @@ class MessageForm(ModelForm):
helper
=
FormHelper
()
helper
=
FormHelper
()
helper
.
add_input
(
Submit
(
"submit"
,
_
(
"Save"
)))
helper
.
add_input
(
Submit
(
"submit"
,
_
(
"Save"
)))
return
helper
return
helper
class
TwoFactorForm
(
ModelForm
):
class
Meta
:
model
=
Profile
fields
=
[
"two_factor_secret"
,
]
class
DisableTwoFactorForm
(
ModelForm
):
confirmation_code
=
forms
.
CharField
(
label
=
_
(
'Confirmation code'
),
help_text
=
_
(
"Get the code from your authenticator to disable "
"two-factor authentication."
))
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
DisableTwoFactorForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
'two_factor_secret'
]
.
initial
=
None
class
Meta
:
model
=
Profile
fields
=
(
'two_factor_secret'
,
'confirmation_code'
,
)
def
clean_confirmation_code
(
self
):
totp
=
pyotp
.
TOTP
(
self
.
instance
.
two_factor_secret
)
if
not
totp
.
verify
(
self
.
cleaned_data
.
get
(
'confirmation_code'
)):
raise
ValidationError
(
_
(
"Invalid confirmation code."
))
circle/dashboard/migrations/0005_profile_two_factor_secret.py
0 → 100644
View file @
0accbbd3
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
models
,
migrations
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'dashboard'
,
'0004_profile_desktop_notifications'
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
'profile'
,
name
=
'two_factor_secret'
,
field
=
models
.
CharField
(
max_length
=
32
,
null
=
True
,
verbose_name
=
'two factor secret key'
,
blank
=
True
),
),
]
circle/dashboard/models.py
View file @
0accbbd3
...
@@ -200,6 +200,10 @@ class Profile(Model):
...
@@ -200,6 +200,10 @@ class Profile(Model):
verbose_name
=
_
(
'disk quota'
),
verbose_name
=
_
(
'disk quota'
),
default
=
2048
*
1024
*
1024
,
default
=
2048
*
1024
*
1024
,
help_text
=
_
(
'Disk quota in mebibytes.'
))
help_text
=
_
(
'Disk quota in mebibytes.'
))
two_factor_secret
=
CharField
(
verbose_name
=
_
(
"two factor secret key"
),
max_length
=
32
,
null
=
True
,
blank
=
True
,
)
def
get_connect_commands
(
self
,
instance
,
use_ipv6
=
False
):
def
get_connect_commands
(
self
,
instance
,
use_ipv6
=
False
):
""" Generate connection command based on template."""
""" Generate connection command based on template."""
...
...
circle/dashboard/static/dashboard/dashboard.less
View file @
0accbbd3
...
@@ -1533,3 +1533,19 @@ textarea[name="new_members"] {
...
@@ -1533,3 +1533,19 @@ textarea[name="new_members"] {
#manage-access-select-all {
#manage-access-select-all {
cursor: pointer;
cursor: pointer;
}
}
#two-factor-qr {
text-align: center;
span, small {
display: block;
}
}
#two-factor-confirm {
text-align: center;
button {
margin-left: 15px;
}
}
circle/dashboard/static/dashboard/profile.js
View file @
0accbbd3
...
@@ -22,6 +22,5 @@ $(function() {
...
@@ -22,6 +22,5 @@ $(function() {
}
}
});
});
});
});
});
});
circle/dashboard/templates/dashboard/disable-two-factor.html
0 → 100644
View file @
0accbbd3
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
<div
class=
"row"
>
<div
class=
"col-md-12"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<h3
class=
"no-margin"
>
<i
class=
"fa fa-unlock"
></i>
{% trans "Disable two-factor authentication" %}
</h3>
</div>
<div
class=
"panel-body"
>
<form
action=
""
method=
"POST"
>
{% csrf_token %}
<input
type=
"hidden"
value=
""
name=
"{{ form.two_factor_secret.name }}"
/>
{{ form.confirmation_code|as_crispy_field }}
<button
type=
"submit"
class=
"btn btn-warning"
>
<i
class=
"fa fa-unlock"
></i>
{% trans "Disable" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
circle/dashboard/templates/dashboard/enable-two-factor.html
0 → 100644
View file @
0accbbd3
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div
class=
"row"
>
<div
class=
"col-md-12"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<h3
class=
"no-margin"
>
<i
class=
"fa fa-lock"
></i>
{% trans "Enable two-factor authentication" %}
</h3>
</div>
<div
class=
"panel-body"
>
{% blocktrans with lang=LANGUAGE_CODE %}
To use two-factor authentication you need to download Google Authenticator
and use the following qr code, secret key or link to set it up.
If you need help with the download or setup check out the
<a
href=
"https://support.google.com/accounts/answer/1066447?hl={{ lang }}"
>
official help page.
</a>
{% endblocktrans %}
<hr
/>
<div
id=
"two-factor-qr"
>
<span>
{% blocktrans with secret=secret %}
Your secret key is:
<strong>
{{ secret }}
</strong>
{% endblocktrans %}
</span>
<img
src=
"//chart.googleapis.com/chart?chs=255x255&chld=L|0&cht=qr&chl={{ uri }}"
/>
<small><a
href=
"{{ uri }}"
>
{{ uri }}
</a></small>
</div>
<hr
/>
<div
id=
"two-factor-confirm"
>
<form
action=
""
method=
"POST"
>
{% csrf_token %}
<input
type=
"hidden"
value=
"{{ secret }}"
name=
"{{ form.two_factor_secret.name }}"
/>
{% blocktrans %}
If you managed to set up the authenticator click enable to finalize two-factor
authentication for this account.
{% endblocktrans %}
<button
type=
"submit"
class=
"btn btn-success"
>
<i
class=
"fa fa-lock"
></i>
{% trans "Enable" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
circle/dashboard/templates/dashboard/profile_form.html
View file @
0accbbd3
...
@@ -23,6 +23,28 @@
...
@@ -23,6 +23,28 @@
<legend>
{% trans "Password change" %}
</legend>
<legend>
{% trans "Password change" %}
</legend>
{% crispy forms.change_password %}
{% crispy forms.change_password %}
</fieldset>
</fieldset>
<fieldset
style=
"margin-top: 25px;"
>
<legend>
{% trans "Two-factor authentication" %}
</legend>
{% if profile.two_factor_secret %}
{% blocktrans %}
Two-factor authentication is currently enabled on your account. To disable it
click the button
{% endblocktrans %}
<a
href=
"{% url "
dashboard
.
views
.
profile-disable-two-factor
"
%}"
class=
"btn btn-warning btn-xs"
>
<i
class=
"fa fa-unlock"
></i>
{% trans "Disable" %}
</a>
{% else %}
{% blocktrans %}
Two-factor authentication is currently disabled on your account. To enable it
click the button
{% endblocktrans %}
<a
href=
"{% url "
dashboard
.
views
.
profile-enable-two-factor
"
%}"
class=
"btn btn-success btn-xs"
>
<i
class=
"fa fa-lock"
></i>
{% trans "Enable" %}
</a>
{% endif %}
</fieldset>
</div>
</div>
<div
class=
"col-md-4"
style=
"margin-bottom: 50px;"
>
<div
class=
"col-md-4"
style=
"margin-bottom: 50px;"
>
<fieldset>
<fieldset>
...
...
circle/dashboard/urls.py
View file @
0accbbd3
...
@@ -55,6 +55,7 @@ from .views import (
...
@@ -55,6 +55,7 @@ from .views import (
UserList
,
UserList
,
StorageDetail
,
DiskDetail
,
StorageDetail
,
DiskDetail
,
MessageList
,
MessageDetail
,
MessageCreate
,
MessageDelete
,
MessageList
,
MessageDetail
,
MessageCreate
,
MessageDelete
,
EnableTwoFactorView
,
DisableTwoFactorView
,
)
)
from
.views.vm
import
vm_ops
,
vm_mass_ops
from
.views.vm
import
vm_ops
,
vm_mass_ops
from
.views.node
import
node_ops
from
.views.node
import
node_ops
...
@@ -179,6 +180,10 @@ urlpatterns = patterns(
...
@@ -179,6 +180,10 @@ urlpatterns = patterns(
url
(
r'^profile/(?P<username>[^/]+)/$'
,
ProfileView
.
as_view
(),
url
(
r'^profile/(?P<username>[^/]+)/$'
,
ProfileView
.
as_view
(),
name
=
"dashboard.views.profile"
),
name
=
"dashboard.views.profile"
),
url
(
r'^profile/(?P<username>[^/]+)/use_gravatar/$'
,
toggle_use_gravatar
),
url
(
r'^profile/(?P<username>[^/]+)/use_gravatar/$'
,
toggle_use_gravatar
),
url
(
r'^profile/two-factor/enable/$'
,
EnableTwoFactorView
.
as_view
(),
name
=
"dashboard.views.profile-enable-two-factor"
),
url
(
r'^profile/two-factor/disable/$'
,
DisableTwoFactorView
.
as_view
(),
name
=
"dashboard.views.profile-disable-two-factor"
),
url
(
r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$'
,
url
(
r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$'
,
GroupRemoveUserView
.
as_view
(),
GroupRemoveUserView
.
as_view
(),
...
...
circle/dashboard/views/user.py
View file @
0accbbd3
...
@@ -19,6 +19,8 @@ from __future__ import unicode_literals, absolute_import
...
@@ -19,6 +19,8 @@ from __future__ import unicode_literals, absolute_import
import
json
import
json
import
logging
import
logging
import
pyotp
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib
import
messages
from
django.contrib
import
messages
from
django.contrib.auth
import
login
from
django.contrib.auth
import
login
...
@@ -26,9 +28,7 @@ from django.contrib.auth.models import User, Group
...
@@ -26,9 +28,7 @@ from django.contrib.auth.models import User, Group
from
django.contrib.auth.views
import
login
as
login_view
from
django.contrib.auth.views
import
login
as
login_view
from
django.contrib.messages.views
import
SuccessMessageMixin
from
django.contrib.messages.views
import
SuccessMessageMixin
from
django.core
import
signing
from
django.core
import
signing
from
django.core.exceptions
import
(
from
django.core.exceptions
import
PermissionDenied
,
SuspiciousOperation
PermissionDenied
,
SuspiciousOperation
,
)
from
django.core.urlresolvers
import
reverse
,
reverse_lazy
from
django.core.urlresolvers
import
reverse
,
reverse_lazy
from
django.core.paginator
import
Paginator
,
InvalidPage
from
django.core.paginator
import
Paginator
,
InvalidPage
from
django.db.models
import
Q
from
django.db.models
import
Q
...
@@ -51,7 +51,7 @@ from vm.models import Instance, InstanceTemplate
...
@@ -51,7 +51,7 @@ from vm.models import Instance, InstanceTemplate
from
..forms
import
(
from
..forms
import
(
CircleAuthenticationForm
,
MyProfileForm
,
UserCreationForm
,
UnsubscribeForm
,
CircleAuthenticationForm
,
MyProfileForm
,
UserCreationForm
,
UnsubscribeForm
,
UserKeyForm
,
CirclePasswordChangeForm
,
ConnectCommandForm
,
UserKeyForm
,
CirclePasswordChangeForm
,
ConnectCommandForm
,
UserListSearchForm
,
UserEditForm
,
UserListSearchForm
,
UserEditForm
,
TwoFactorForm
,
DisableTwoFactorForm
)
)
from
..models
import
Profile
,
GroupProfile
,
ConnectCommand
from
..models
import
Profile
,
GroupProfile
,
ConnectCommand
from
..tables
import
(
from
..tables
import
(
...
@@ -555,3 +555,37 @@ class UserList(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView):
...
@@ -555,3 +555,37 @@ class UserList(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView):
qs
=
qs
.
filter
(
filters
)
qs
=
qs
.
filter
(
filters
)
return
qs
.
select_related
(
"profile"
)
return
qs
.
select_related
(
"profile"
)
class
EnableTwoFactorView
(
LoginRequiredMixin
,
UpdateView
):
model
=
Profile
form_class
=
TwoFactorForm
template_name
=
"dashboard/enable-two-factor.html"
success_url
=
reverse_lazy
(
"dashboard.views.profile-preferences"
)
def
get_object
(
self
,
queryset
=
None
):
if
self
.
request
.
user
.
is_anonymous
():
raise
PermissionDenied
return
self
.
request
.
user
.
profile
def
get_context_data
(
self
,
**
kwargs
):
ctx
=
super
(
EnableTwoFactorView
,
self
)
.
get_context_data
(
**
kwargs
)
random_base32
=
pyotp
.
random_base32
()
ctx
[
'uri'
]
=
pyotp
.
TOTP
(
random_base32
)
.
provisioning_uri
(
self
.
request
.
user
.
username
,
issuer_name
=
settings
.
TWO_FACTOR_ISSUER
)
ctx
[
'secret'
]
=
random_base32
return
ctx
class
DisableTwoFactorView
(
LoginRequiredMixin
,
UpdateView
):
model
=
Profile
form_class
=
DisableTwoFactorForm
template_name
=
"dashboard/disable-two-factor.html"
success_url
=
reverse_lazy
(
"dashboard.views.profile-preferences"
)
def
get_object
(
self
,
queryset
=
None
):
if
self
.
request
.
user
.
is_anonymous
():
raise
PermissionDenied
return
self
.
request
.
user
.
profile
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment