Add an oauth authorization page
This commit is contained in:
parent
ab60a10a93
commit
d469b41899
10 changed files with 287 additions and 41 deletions
|
@ -1,25 +1,33 @@
|
||||||
READ_REPO = {
|
READ_REPO = {
|
||||||
'scope': 'repo:read',
|
'scope': 'repo:read',
|
||||||
'description': ('Grants read-only access to all repositories for which the granting user or '
|
'icon': 'fa-hdd-o',
|
||||||
' robot has access.')
|
'title': 'View all visible repositories',
|
||||||
|
'description': ('This application will be able to view and pull all repositories visible to the '
|
||||||
|
'granting user or robot account')
|
||||||
}
|
}
|
||||||
|
|
||||||
WRITE_REPO = {
|
WRITE_REPO = {
|
||||||
'scope': 'repo:write',
|
'scope': 'repo:write',
|
||||||
'description': ('Grants read-write access to all repositories for which the granting user or '
|
'icon': 'fa-hdd-o',
|
||||||
'robot has access, and is a superset of repo:read.')
|
'title': 'Read/Write to any accessible repositories',
|
||||||
|
'description': ('This application will be able to view, push and pull to all repositories to which the '
|
||||||
|
'granting user or robot account has write access')
|
||||||
}
|
}
|
||||||
|
|
||||||
ADMIN_REPO = {
|
ADMIN_REPO = {
|
||||||
'scope': 'repo:admin',
|
'scope': 'repo:admin',
|
||||||
'description': ('Grants administrator access to all repositories for which the granting user or '
|
'icon': 'fa-hdd-o',
|
||||||
'robot has access, and is a superset of repo:read and repo:write.')
|
'title': 'Administer Repositories',
|
||||||
|
'description': ('This application will have administrator access to all repositories to which the '
|
||||||
|
'granting user or robot account has access')
|
||||||
}
|
}
|
||||||
|
|
||||||
CREATE_REPO = {
|
CREATE_REPO = {
|
||||||
'scope': 'repo:create',
|
'scope': 'repo:create',
|
||||||
'description': ('Grants create repository access to all namespaces for which the granting user '
|
'icon': 'fa-plus',
|
||||||
'or robot is allowed to create repositories.')
|
'title': 'Create Repositories',
|
||||||
|
'description': ('This application will be able to create repositories in to any namespaces that '
|
||||||
|
'the granting user or robot account is allowed to create repositories')
|
||||||
}
|
}
|
||||||
|
|
||||||
ALL_SCOPES = {scope['scope']:scope for scope in (READ_REPO, WRITE_REPO, ADMIN_REPO, CREATE_REPO)}
|
ALL_SCOPES = {scope['scope']:scope for scope in (READ_REPO, WRITE_REPO, ADMIN_REPO, CREATE_REPO)}
|
||||||
|
@ -32,3 +40,25 @@ def scopes_from_scope_string(scopes):
|
||||||
def validate_scope_string(scopes):
|
def validate_scope_string(scopes):
|
||||||
decoded = scopes_from_scope_string(scopes)
|
decoded = scopes_from_scope_string(scopes)
|
||||||
return None not in decoded and len(decoded) > 0
|
return None not in decoded and len(decoded) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def is_subset_string(full_string, expected_string):
|
||||||
|
""" Returns true if the scopes found in expected_string are also found
|
||||||
|
in full_string.
|
||||||
|
"""
|
||||||
|
full_scopes = scopes_from_scope_string(full_string)
|
||||||
|
expected_scopes = scopes_from_scope_string(expected_string)
|
||||||
|
return expected_scopes.issubset(full_scopes)
|
||||||
|
|
||||||
|
def get_scope_information(scopes_string):
|
||||||
|
scopes = scopes_from_scope_string(scopes_string)
|
||||||
|
scope_info = []
|
||||||
|
for scope in scopes:
|
||||||
|
scope_info.append({
|
||||||
|
'title': ALL_SCOPES[scope]['title'],
|
||||||
|
'scope': ALL_SCOPES[scope]['scope'],
|
||||||
|
'description': ALL_SCOPES[scope]['description'],
|
||||||
|
'icon': ALL_SCOPES[scope]['icon'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return scope_info
|
||||||
|
|
|
@ -275,7 +275,10 @@ class OAuthApplication(BaseModel):
|
||||||
client_id = CharField(index=True, default=random_string_generator(length=20))
|
client_id = CharField(index=True, default=random_string_generator(length=20))
|
||||||
client_secret = CharField(default=random_string_generator(length=40))
|
client_secret = CharField(default=random_string_generator(length=40))
|
||||||
redirect_uri = CharField()
|
redirect_uri = CharField()
|
||||||
|
application_uri = CharField()
|
||||||
organization = ForeignKeyField(User)
|
organization = ForeignKeyField(User)
|
||||||
|
name = CharField()
|
||||||
|
description = TextField(default='')
|
||||||
|
|
||||||
|
|
||||||
class OAuthAuthorizationCode(BaseModel):
|
class OAuthAuthorizationCode(BaseModel):
|
||||||
|
|
|
@ -11,11 +11,13 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
raise NotImplementedError('Subclasses must fill in the ability to get the authorized_user.')
|
raise NotImplementedError('Subclasses must fill in the ability to get the authorized_user.')
|
||||||
|
|
||||||
def validate_client_id(self, client_id):
|
def validate_client_id(self, client_id):
|
||||||
|
return self.get_application_for_client_id(client_id) is not None
|
||||||
|
|
||||||
|
def get_application_for_client_id(self, client_id):
|
||||||
try:
|
try:
|
||||||
OAuthApplication.get(client_id=client_id)
|
return OAuthApplication.get(client_id=client_id)
|
||||||
return True
|
|
||||||
except OAuthApplication.DoesNotExist:
|
except OAuthApplication.DoesNotExist:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
def validate_client_secret(self, client_id, client_secret):
|
def validate_client_secret(self, client_id, client_secret):
|
||||||
try:
|
try:
|
||||||
|
@ -33,12 +35,35 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
except OAuthApplication.DoesNotExist:
|
except OAuthApplication.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def validate_scope(self, client_id, scope):
|
def validate_scope(self, client_id, scopes_string):
|
||||||
return scopes.validate_scope_string(scope)
|
return scopes.validate_scope_string(scopes_string)
|
||||||
|
|
||||||
def validate_access(self):
|
def validate_access(self):
|
||||||
return self.get_authorized_user() is not None
|
return self.get_authorized_user() is not None
|
||||||
|
|
||||||
|
def lookup_access_token(self, client_id):
|
||||||
|
try:
|
||||||
|
found = (OAuthAccessToken
|
||||||
|
.select()
|
||||||
|
.join(OAuthApplication)
|
||||||
|
.where(OAuthApplication.client_id == client_id)
|
||||||
|
.get())
|
||||||
|
return found
|
||||||
|
except OAuthAccessToken.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_has_scopes(self, client_id, scope):
|
||||||
|
access_token = self.lookup_access_token(client_id)
|
||||||
|
if not access_token:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Make sure the token is not expired.
|
||||||
|
if access_token.expires_at <= datetime.now():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Make sure the token contains the given scopes (at least).
|
||||||
|
return scopes.is_subset_string(access_token.scope, scope)
|
||||||
|
|
||||||
def from_authorization_code(self, client_id, code, scope):
|
def from_authorization_code(self, client_id, code, scope):
|
||||||
try:
|
try:
|
||||||
found = (OAuthAuthorizationCode
|
found = (OAuthAuthorizationCode
|
||||||
|
@ -109,7 +134,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
is_valid_client_id = self.validate_client_id(client_id)
|
is_valid_client_id = self.validate_client_id(client_id)
|
||||||
is_valid_access = self.validate_access()
|
is_valid_access = self.validate_access()
|
||||||
scope = params.get('scope', '')
|
scope = params.get('scope', '')
|
||||||
is_valid_scope = self.validate_scope(client_id, scope)
|
are_valid_scopes = self.validate_scope(client_id, scope)
|
||||||
|
|
||||||
# Return proper error responses on invalid conditions
|
# Return proper error responses on invalid conditions
|
||||||
if not is_valid_client_id:
|
if not is_valid_client_id:
|
||||||
|
@ -120,7 +145,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
err = 'access_denied'
|
err = 'access_denied'
|
||||||
return self._make_redirect_error_response(redirect_uri, err)
|
return self._make_redirect_error_response(redirect_uri, err)
|
||||||
|
|
||||||
if not is_valid_scope:
|
if not are_valid_scopes:
|
||||||
err = 'invalid_scope'
|
err = 'invalid_scope'
|
||||||
return self._make_redirect_error_response(redirect_uri, err)
|
return self._make_redirect_error_response(redirect_uri, err)
|
||||||
|
|
||||||
|
@ -138,8 +163,8 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
|
|
||||||
return self._make_response(headers={'Location': url}, status_code=302)
|
return self._make_response(headers={'Location': url}, status_code=302)
|
||||||
|
|
||||||
def create_application(org, redirect_uri, **kwargs):
|
def create_application(org, name, application_uri, redirect_uri, **kwargs):
|
||||||
return OAuthApplication.create(organization=org, redirect_uri=redirect_uri, **kwargs)
|
return OAuthApplication.create(organization=org, name=name, application_uri=application_uri, redirect_uri=redirect_uri, **kwargs)
|
||||||
|
|
||||||
def validate_access_token(access_token):
|
def validate_access_token(access_token):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -102,7 +102,7 @@ def swagger_route_data():
|
||||||
swagger_data = {
|
swagger_data = {
|
||||||
'apiVersion': 'v1',
|
'apiVersion': 'v1',
|
||||||
'swaggerVersion': '1.2',
|
'swaggerVersion': '1.2',
|
||||||
'basePath': 'http://ci.devtable.com:5000',
|
'basePath': 'http://localhost:5000',
|
||||||
'resourcePath': '/',
|
'resourcePath': '/',
|
||||||
'info': {
|
'info': {
|
||||||
'title': 'Quay.io API',
|
'title': 'Quay.io API',
|
||||||
|
@ -119,7 +119,7 @@ def swagger_route_data():
|
||||||
"implicit": {
|
"implicit": {
|
||||||
"tokenName": "access_token",
|
"tokenName": "access_token",
|
||||||
"loginEndpoint": {
|
"loginEndpoint": {
|
||||||
"url": "http://ci.devtable.com:5000/oauth/authorize",
|
"url": "http://localhost:5000/oauth/authorize",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,8 +14,10 @@ from auth.permissions import AdministerOrganizationPermission
|
||||||
from util.invoice import renderInvoiceToPdf
|
from util.invoice import renderInvoiceToPdf
|
||||||
from util.seo import render_snapshot
|
from util.seo import render_snapshot
|
||||||
from util.cache import no_cache
|
from util.cache import no_cache
|
||||||
from endpoints.common import common_login, render_page_template
|
from endpoints.common import common_login, render_page_template, generate_csrf_token
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
from util.gravatar import compute_hash
|
||||||
|
from auth import scopes
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -239,6 +241,27 @@ class FlaskAuthorizationProvider(DatabaseAuthorizationProvider):
|
||||||
return make_response(body, status_code, headers)
|
return make_response(body, status_code, headers)
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/oauth/authorizeapp', methods=['POST'])
|
||||||
|
def authorize_application():
|
||||||
|
if not current_user.is_authenticated():
|
||||||
|
abort(401)
|
||||||
|
return
|
||||||
|
|
||||||
|
provider = FlaskAuthorizationProvider()
|
||||||
|
client_id = request.form.get('client_id', None)
|
||||||
|
redirect_uri = request.form.get('redirect_uri', None)
|
||||||
|
scope = request.form.get('scope', None)
|
||||||
|
csrf = request.form.get('csrf', None)
|
||||||
|
|
||||||
|
# Verify the csrf token.
|
||||||
|
if csrf != generate_csrf_token():
|
||||||
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add the access token.
|
||||||
|
return provider.get_token_response('token', client_id, redirect_uri, scope=scope)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/oauth/authorize', methods=['GET'])
|
@web.route('/oauth/authorize', methods=['GET'])
|
||||||
@no_cache
|
@no_cache
|
||||||
def request_authorization_code():
|
def request_authorization_code():
|
||||||
|
@ -248,6 +271,31 @@ def request_authorization_code():
|
||||||
redirect_uri = request.args.get('redirect_uri', None)
|
redirect_uri = request.args.get('redirect_uri', None)
|
||||||
scope = request.args.get('scope', None)
|
scope = request.args.get('scope', None)
|
||||||
|
|
||||||
|
if not provider.validate_has_scopes(client_id, scope):
|
||||||
|
if not provider.validate_redirect_uri(client_id, redirect_uri):
|
||||||
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load the scope information.
|
||||||
|
scope_info = scopes.get_scope_information(scope)
|
||||||
|
|
||||||
|
# Load the application information.
|
||||||
|
oauth_app = provider.get_application_for_client_id(client_id)
|
||||||
|
oauth_app_view = {
|
||||||
|
'name': oauth_app.name,
|
||||||
|
'description': oauth_app.description,
|
||||||
|
'url': oauth_app.application_uri,
|
||||||
|
'organization': {
|
||||||
|
'name': oauth_app.organization.username,
|
||||||
|
'gravatar': compute_hash(oauth_app.organization.email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show the authorization page.
|
||||||
|
return render_page_template('oauthorize.html', scopes=scope_info, application=oauth_app_view,
|
||||||
|
enumerate=enumerate, client_id=client_id, redirect_uri=redirect_uri,
|
||||||
|
scope=scope, csrf_token_val=generate_csrf_token())
|
||||||
|
|
||||||
if response_type == 'token':
|
if response_type == 'token':
|
||||||
return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope)
|
return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -339,7 +339,12 @@ def populate_database():
|
||||||
org.stripe_id = TEST_STRIPE_ID
|
org.stripe_id = TEST_STRIPE_ID
|
||||||
org.save()
|
org.save()
|
||||||
|
|
||||||
oauth.create_application(org, 'http://localhost:8000/o2c.html', client_id='deadbeef')
|
oauth.create_application(org, 'Some Test App', 'http://localhost:8000', 'http://localhost:8000/o2c.html',
|
||||||
|
client_id='deadbeef')
|
||||||
|
|
||||||
|
oauth.create_application(org, 'Some Other Test App', 'http://quay.io', 'http://localhost:8000/o2c.html',
|
||||||
|
client_id='deadpork',
|
||||||
|
description = 'This is another test application')
|
||||||
|
|
||||||
model.create_robot('neworgrobot', org)
|
model.create_robot('neworgrobot', org)
|
||||||
|
|
||||||
|
|
|
@ -3393,3 +3393,84 @@ pre.command:before {
|
||||||
.slideinout.ng-hide-add, .slideinout.ng-hide-remove {
|
.slideinout.ng-hide-add, .slideinout.ng-hide-remove {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-header img {
|
||||||
|
float: left;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .reason {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes ul {
|
||||||
|
margin-top: 10px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes li {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .scope {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .scope-container:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .panel-default {
|
||||||
|
border: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .panel-default:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .panel-heading {
|
||||||
|
border: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .scope .title {
|
||||||
|
min-width: 300px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .scope .title:hover {
|
||||||
|
color: #428bca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .scope .description {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-scopes .scope i {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container .button-bar {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container .button-bar button {
|
||||||
|
margin: 6px;
|
||||||
|
}
|
||||||
|
|
|
@ -47,22 +47,22 @@
|
||||||
<script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0"></script>
|
<script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0"></script>
|
||||||
<!-- ,typeahead.js@0.10.1 -->
|
<!-- ,typeahead.js@0.10.1 -->
|
||||||
|
|
||||||
<script src="static/lib/loading-bar.js"></script>
|
<script src="/static/lib/loading-bar.js"></script>
|
||||||
<script src="static/lib/angular-strap.min.js"></script>
|
<script src="/static/lib/angular-strap.min.js"></script>
|
||||||
<script src="static/lib/angulartics.js"></script>
|
<script src="/static/lib/angulartics.js"></script>
|
||||||
<script src="static/lib/angulartics-mixpanel.js"></script>
|
<script src="/static/lib/angulartics-mixpanel.js"></script>
|
||||||
<script src="static/lib/angulartics-google-analytics.js"></script>
|
<script src="/static/lib/angulartics-google-analytics.js"></script>
|
||||||
<script src="static/lib/angular-md5.js"></script>
|
<script src="/static/lib/angular-md5.js"></script>
|
||||||
<script src="static/lib/bindonce.min.js"></script>
|
<script src="/static/lib/bindonce.min.js"></script>
|
||||||
<script src="static/lib/ansi2html.js"></script>
|
<script src="/static/lib/ansi2html.js"></script>
|
||||||
<script src="static/lib/typeahead.bundle.min.js"></script>
|
<script src="/static/lib/typeahead.bundle.min.js"></script>
|
||||||
|
|
||||||
<script src="static/lib/angular-moment.min.js"></script>
|
<script src="/static/lib/angular-moment.min.js"></script>
|
||||||
<script src="static/lib/angular-cookies.min.js"></script>
|
<script src="/static/lib/angular-cookies.min.js"></script>
|
||||||
|
|
||||||
<script src="static/lib/pagedown/Markdown.Converter.js"></script>
|
<script src="/static/lib/pagedown/Markdown.Converter.js"></script>
|
||||||
<script src="static/lib/pagedown/Markdown.Editor.js"></script>
|
<script src="/static/lib/pagedown/Markdown.Editor.js"></script>
|
||||||
<script src="static/lib/pagedown/Markdown.Sanitizer.js"></script>
|
<script src="/static/lib/pagedown/Markdown.Sanitizer.js"></script>
|
||||||
|
|
||||||
{% block added_dependencies %}
|
{% block added_dependencies %}
|
||||||
|
|
||||||
|
@ -73,10 +73,10 @@
|
||||||
window.__token = '{{ csrf_token() }}';
|
window.__token = '{{ csrf_token() }}';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="static/js/tour.js"></script>
|
<script src="/static/js/tour.js"></script>
|
||||||
<script src="static/js/app.js"></script>
|
<script src="/static/js/app.js"></script>
|
||||||
<script src="static/js/controllers.js"></script>
|
<script src="/static/js/controllers.js"></script>
|
||||||
<script src="static/js/graphing.js"></script>
|
<script src="/static/js/graphing.js"></script>
|
||||||
|
|
||||||
<!-- start Mixpanel --><script type="text/javascript">
|
<!-- start Mixpanel --><script type="text/javascript">
|
||||||
var isProd = document.location.hostname === 'quay.io';
|
var isProd = document.location.hostname === 'quay.io';
|
||||||
|
|
54
templates/oauthorize.html
Normal file
54
templates/oauthorize.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Authorize {{ application.name }} · Quay.io</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body_content %}
|
||||||
|
<div class="container auth-container">
|
||||||
|
<div class="auth-header">
|
||||||
|
<img src="//www.gravatar.com/avatar/{{ application.organization.gravatar }}?s=48&d=identicon">
|
||||||
|
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
|
||||||
|
<h4>
|
||||||
|
{{ application.organization.name }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-scopes">
|
||||||
|
<div class="reason">This application would like permission to:</div>
|
||||||
|
<div class="panel-group">
|
||||||
|
{% for index, scope in enumerate(scopes) %}
|
||||||
|
<div class="scope panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<div class="title-container">
|
||||||
|
<i class="fa {{ scope.icon }} fa-lg"></i>
|
||||||
|
<div class="title" data-toggle="collapse" data-parent="#scopeGroup" data-target="#description-{{ index }}">
|
||||||
|
<a data-toggle="collapse" href="#collapseOne">
|
||||||
|
{{ scope.title }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="description-{{ index }}" class="panel-collapse collapse out">
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ scope.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-bar">
|
||||||
|
<form method="post" action="/oauth/authorizeapp">
|
||||||
|
<input type="hidden" name="client_id" value="{{ client_id }}">
|
||||||
|
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
|
||||||
|
<input type="hidden" name="scope" value="{{ scope }}">
|
||||||
|
<input type="hidden" name="csrf" value="{{ csrf_token_val }}">
|
||||||
|
<button type="submit" class="btn btn-success">Authorize Application</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Binary file not shown.
Reference in a new issue