Preliminary OAuth2 for new API call authentication.

This commit is contained in:
Max Goodman
2011-10-23 19:56:07 -07:00
parent 3d55cea886
commit 5df8723c19
17 changed files with 676 additions and 9 deletions

View File

@@ -224,6 +224,10 @@ def make_map(global_conf={}, app_conf={}):
requirements=dict(action="new_captcha"))
mc('/api/:action', controller='api')
mc("/api/v1/:action", controller="oauth2frontend", requirements=dict(action="authorize"))
mc("/api/v1/:action", controller="oauth2access", requirements=dict(action="access_token"))
mc("/api/v1/:action", controller="apiv1")
mc("/button_info", controller="api", action="info", limit = 1)
mc('/captcha/:iden', controller='captcha', action='captchaimg')

View File

@@ -61,6 +61,9 @@ except ImportError:
from api import ApiController
from api import ApiminimalController
from apiv1 import APIv1Controller as Apiv1Controller
from oauth2 import OAuth2FrontendController as Oauth2frontendController
from oauth2 import OAuth2AccessController as Oauth2accessController
from admin import AdminController
from redirect import RedirectController
from ipn import IpnController

View File

@@ -0,0 +1,33 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from pylons import c
from r2.controllers.oauth2 import OAuth2ResourceController, require_oauth2_scope
from r2.lib.jsontemplates import IdentityJsonTemplate
class APIv1Controller(OAuth2ResourceController):
def try_pagecache(self):
pass
@require_oauth2_scope("identity")
def GET_me(self):
resp = IdentityJsonTemplate().data(c.oauth_user)
return self.api_wrapper(resp)

View File

@@ -85,6 +85,8 @@ error_list = dict((
('TOO_OLD', _("that's a piece of history now; it's too late to reply to it")),
('BAD_CSS_NAME', _('invalid css name')),
('TOO_MUCH_FLAIR_CSS', _('too many flair css classes')),
('OAUTH2_INVALID_CLIENT', _('invalid client id')),
('OAUTH2_ACCESS_DENIED', _('access denied by the user')),
))
errors = Storage([(e, e) for e in error_list.keys()])

215
r2/r2/controllers/oauth2.py Normal file
View File

@@ -0,0 +1,215 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from urllib import urlencode
import base64
import simplejson
from pylons import c, g, request
from pylons.controllers.util import abort
from pylons.i18n import _
from r2.config.extensions import set_extension
from reddit_base import RedditController, MinimalController, require_https
from r2.lib.db.thing import NotFound
from r2.models import Account
from r2.models.oauth2 import OAuth2Client, OAuth2AuthorizationCode, OAuth2AccessToken
from r2.controllers.errors import errors
from validator import validate, VRequired, VOneOf, VUrl, VUser, VModhash
from r2.lib.pages import OAuth2AuthorizationPage
from r2.lib.require import RequirementException, require, require_split
scope_info = {
"identity": {
"id": "identity",
"name": _("My Identity"),
"description": _("Access my reddit username and signup date.")
}
}
class VClientID(VRequired):
default_param = "client_id"
def __init__(self, param=None, *a, **kw):
VRequired.__init__(self, param, errors.OAUTH2_INVALID_CLIENT, *a, **kw)
def run(self, client_id):
if not client_id:
return self.error()
client = OAuth2Client.get_token(client_id)
if client:
return client
else:
return self.error()
class OAuth2FrontendController(RedditController):
def pre(self):
RedditController.pre(self)
require_https()
def _check_redirect_uri(self, client, redirect_uri):
if not redirect_uri or not client or redirect_uri != client.redirect_uri:
abort(403)
def _error_response(self, resp):
if (errors.OAUTH2_INVALID_CLIENT, "client_id") in c.errors:
resp["error"] = "unauthorized_client"
elif (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors:
resp["error"] = "access_denied"
elif (errors.BAD_HASH, None) in c.errors:
resp["error"] = "access_denied"
elif (errors.INVALID_OPTION, "response_type") in c.errors:
resp["error"] = "unsupported_response_type"
elif (errors.INVALID_OPTION, "scope") in c.errors:
resp["error"] = "invalid_scope"
else:
resp["error"] = "invalid_request"
@validate(VUser(),
response_type = VOneOf("response_type", ("code",)),
client = VClientID(),
redirect_uri = VUrl("redirect_uri", allow_self=False, lookup=False),
scope = VOneOf("scope", scope_info.keys()),
state = VRequired("state", errors.NO_TEXT))
def GET_authorize(self, response_type, client, redirect_uri, scope, state):
self._check_redirect_uri(client, redirect_uri)
resp = {}
if not c.errors:
c.deny_frames = True
return OAuth2AuthorizationPage(client, redirect_uri, scope_info[scope], state).render()
else:
self._error_response(resp)
return self.redirect(redirect_uri+"?"+urlencode(resp), code=302)
@validate(VUser(),
VModhash(fatal=False),
client = VClientID(),
redirect_uri = VUrl("redirect_uri", allow_self=False, lookup=False),
scope = VOneOf("scope", scope_info.keys()),
state = VRequired("state", errors.NO_TEXT),
authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED))
def POST_authorize(self, authorize, client, redirect_uri, scope, state):
self._check_redirect_uri(client, redirect_uri)
resp = {}
if state:
resp["state"] = state
if not c.errors:
code = OAuth2AuthorizationCode._new(client._id, redirect_uri, c.user._id, scope)
resp["code"] = code._id
else:
self._error_response(resp)
return self.redirect(redirect_uri+"?"+urlencode(resp), code=302)
class OAuth2AccessController(MinimalController):
def pre(self):
set_extension(request.environ, "json")
MinimalController.pre(self)
require_https()
c.oauth2_client = self._get_client_auth()
def _get_client_auth(self):
auth = request.headers.get("Authorization")
try:
auth_scheme, auth_token = require_split(auth, 2)
require(auth_scheme.lower() == "basic")
try:
auth_data = base64.b64decode(auth_token)
except TypeError:
raise RequirementException
client_id, client_secret = require_split(auth_data, 2, ":")
client = OAuth2Client.get_token(client_id)
require(client)
require(client.secret == client_secret)
return client
except RequirementException:
abort(401, headers=[("WWW-Authenticate", 'Basic realm="reddit"')])
@validate(grant_type = VOneOf("grant_type", ("authorization_code",)),
code = VRequired("code", errors.NO_TEXT),
redirect_uri = VUrl("redirect_uri", allow_self=False, lookup=False))
def POST_access_token(self, grant_type, code, redirect_uri):
resp = {}
if not c.errors:
auth_token = OAuth2AuthorizationCode.use_token(code, c.oauth2_client._id, redirect_uri)
if auth_token:
access_token = OAuth2AccessToken._new(auth_token.user_id, auth_token.scope)
resp["access_token"] = access_token._id
resp["token_type"] = access_token.token_type
resp["expires_in"] = access_token._ttl
resp["scope"] = auth_token.scope
else:
resp["error"] = "invalid_grant"
else:
if (errors.INVALID_OPTION, "grant_type") in c.errors:
resp["error"] = "unsupported_grant_type"
elif (errors.INVALID_OPTION, "scope") in c.errors:
resp["error"] = "invalid_scope"
else:
resp["error"] = "invalid_request"
return self.api_wrapper(resp)
class OAuth2ResourceController(MinimalController):
def pre(self):
set_extension(request.environ, "json")
MinimalController.pre(self)
require_https()
try:
access_token = self._get_bearer_token()
require(access_token)
c.oauth2_access_token = access_token
account = Account._byID(access_token.user_id, data=True)
require(account)
require(not account._deleted)
c.oauth_user = account
except RequirementException:
self._auth_error(401, "invalid_token")
handler = self._get_action_handler()
if handler:
oauth2_perms = getattr(handler, "oauth2_perms", None)
if oauth2_perms:
if access_token.scope not in oauth2_perms["allowed_scopes"]:
self._auth_error(403, "insufficient_scope")
else:
self._auth_error(400, "invalid_request")
def _auth_error(self, code, error):
abort(code, headers=[("WWW-Authenticate", 'Bearer realm="reddit", error="%s"' % error)])
def _get_bearer_token(self):
auth = request.headers.get("Authorization")
try:
auth_scheme, bearer_token = require_split(auth, 2)
require(auth_scheme.lower() == "bearer")
return OAuth2AccessToken.get_token(bearer_token)
except RequirementException:
self._auth_error(400, "invalid_request")
def require_oauth2_scope(*scopes):
def oauth2_scope_wrap(fn):
fn.oauth2_perms = {"allowed_scopes": scopes}
return fn
return oauth2_scope_wrap

View File

@@ -504,6 +504,10 @@ def cross_domain(origin_check=is_trusted_origin, **options):
return cross_domain_handler
return cross_domain_wrap
def require_https():
if not c.secure:
abort(403)
class MinimalController(BaseController):
allow_stylesheets = False
@@ -539,6 +543,8 @@ class MinimalController(BaseController):
c.domain_prefix = request.environ.get("reddit-domain-prefix",
g.domain_prefix)
c.secure = request.host in g.secure_domains
#check if user-agent needs a dose of rate-limiting
if not c.error_page:
ratelimit_agents()
@@ -697,7 +703,6 @@ class MinimalController(BaseController):
return c.response
class RedditController(MinimalController):
@staticmethod
@@ -728,7 +733,6 @@ class RedditController(MinimalController):
#can't handle broken cookies
request.environ['HTTP_COOKIE'] = ''
c.secure = request.host in g.secure_domains
c.firsttime = firsttime()
# the user could have been logged in via one of the feeds

View File

@@ -201,6 +201,7 @@ class ThingBase(object):
# updated columns will be present.) This is an expected convention
# and is not enforced.
_ttl = None
_warn_on_partial_ttl = True
# A per-class dictionary of default TTLs that new columns of this
# class should have
@@ -463,7 +464,7 @@ class ThingBase(object):
if self._id is None:
raise TdbException("Can't commit %r without an ID" % (self,))
if self._committed and self._ttl:
if self._committed and self._ttl and self._warn_on_partial_ttl:
log.warning("Using a full-TTL object %r in a mutable fashion"
% (self,))

View File

@@ -211,16 +211,19 @@ class SubredditJsonTemplate(ThingJsonTemplate):
else:
return ThingJsonTemplate.thing_attr(self, thing, attr)
class AccountJsonTemplate(ThingJsonTemplate):
class IdentityJsonTemplate(ThingJsonTemplate):
_data_attrs_ = ThingJsonTemplate.data_attrs(name = "name",
link_karma = "safe_karma",
comment_karma = "comment_karma",
has_mail = "has_mail",
has_mod_mail = "has_mod_mail",
is_mod = "is_mod",
is_gold = "gold"
)
class AccountJsonTemplate(IdentityJsonTemplate):
_data_attrs_ = IdentityJsonTemplate.data_attrs(has_mail = "has_mail",
has_mod_mail = "has_mod_mail",
is_mod = "is_mod",
)
def thing_attr(self, thing, attr):
from r2.models import Subreddit
if attr == "has_mail":

View File

@@ -25,6 +25,7 @@ from r2.models import FakeSubreddit, Subreddit, Ad, AdSR
from r2.models import Friends, All, Sub, NotFound, DomainSR, Random, Mod, RandomNSFW, MultiReddit
from r2.models import Link, Printable, Trophy, bidding, PromotionWeights, Comment
from r2.models import Flair, FlairTemplate, FlairTemplateBySubredditIndex
from r2.models.oauth2 import OAuth2Client
from r2.config import cache
from r2.lib.tracking import AdframeInfo
from r2.lib.jsonresponse import json_respond
@@ -683,11 +684,20 @@ class LoginPage(BoringPage):
title = _("login or register")
BoringPage.__init__(self, title, **context)
if self.dest:
u = UrlParser(self.dest)
# Display a preview message for OAuth2 client authorizations
if u.path == '/api/v1/authorize':
client_id = u.query_dict.get("client_id")
self.client = client_id and OAuth2Client.get_token(client_id)
self.infobar = self.client and ClientInfoBar(self.client, strings.oauth_login_msg)
def content(self):
kw = {}
for x in ('user_login', 'user_reg'):
kw[x] = getattr(self, x) if hasattr(self, x) else ''
return self.login_template(dest = self.dest, **kw)
login_content = self.login_template(dest = self.dest, **kw)
return self.content_stack((self.infobar, login_content))
@classmethod
def login_template(cls, **kw):
@@ -707,7 +717,19 @@ class Login(Templated):
class Register(Login):
pass
class OAuth2AuthorizationPage(BoringPage):
def __init__(self, client, redirect_uri, scope, state):
content = OAuth2Authorization(client=client,
redirect_uri=redirect_uri,
scope=scope,
state=state)
BoringPage.__init__(self, _("request for permission"),
show_sidebar=False, content=content)
class OAuth2Authorization(Templated):
pass
class SearchPage(BoringPage):
"""Search results page"""
searchbox = False
@@ -1213,6 +1235,12 @@ class InfoBar(Templated):
def __init__(self, message = '', extra_class = ''):
Templated.__init__(self, message = message, extra_class = extra_class)
class ClientInfoBar(InfoBar):
"""Draws the message the top of a login page before OAuth2 authorization"""
def __init__(self, client, *args, **kwargs):
kwargs.setdefault("extra_class", "client-info")
InfoBar.__init__(self, *args, **kwargs)
self.client = client
class RedditError(BoringPage):
site_tracking = False

19
r2/r2/lib/require.py Normal file
View File

@@ -0,0 +1,19 @@
class RequirementException(Exception):
pass
def require(val):
"""A safe version of assert
Assert can be stripped out if python is run in an optimized
mode. This function implements assertions in a way that is
guaranteed to execute.
"""
if not val:
raise RequirementException
return val
def require_split(s, length, sep=None):
require(s)
res = s.split(sep)
require(len(res) == length)
return res

View File

@@ -73,6 +73,8 @@ string_dict = dict(
cover_msg = _("you'll need to login or register to do that"),
cover_disclaim = _("(don't worry, it only takes a few seconds)"),
oauth_login_msg = _("Log in or register to connect your reddit account with [%(app_name)s](%(app_about_url)s)."),
legal = _("I understand and agree that registration on or use of this site constitutes agreement to its %(user_agreement)s and %(privacy_policy)s."),
friends = _('to view reddit with only submissions from your friends, use [reddit.com/r/friends](%s)'),

View File

@@ -35,3 +35,4 @@ from bidding import *
from mail_queue import Email, has_opted_out, opt_count
from gold import *
from admintools import *
from oauth2 import *

136
r2/r2/models/oauth2.py Normal file
View File

@@ -0,0 +1,136 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from os import urandom
from base64 import urlsafe_b64encode
from r2.lib.db import tdb_cassandra
def generate_token(size):
return urlsafe_b64encode(urandom(size)).rstrip('=')
class OAuth2Token(tdb_cassandra.Thing):
"""An OAuth2 authorization code for completing authorization flow"""
@classmethod
def _new(cls, **kwargs):
if "_id" not in kwargs:
kwargs["_id"] = cls._generate_unique_token()
client = cls(**kwargs)
client._commit()
return client
@classmethod
def _generate_unique_token(cls):
for i in range(3):
token = generate_token(cls.token_size)
try:
cls._byID(token)
except tdb_cassandra.NotFound:
return token
else:
continue
raise ValueError
@classmethod
def get_token(cls, _id):
try:
return cls._byID(_id)
except tdb_cassandra.NotFound:
return False
class OAuth2Client(OAuth2Token):
"""A client registered for OAuth2 access"""
token_size = 10
client_secret_size = 20
_defaults = dict(name="",
description="",
about_url="",
icon_url="",
secret="",
redirect_uri="",
)
_use_db = True
_use_new_ring = True
@classmethod
def _new(cls, **kwargs):
if "secret" not in kwargs:
kwargs["secret"] = generate_token(cls.client_secret_size)
return super(OAuth2Client, cls)._new(**kwargs)
class OAuth2AuthorizationCode(OAuth2Token):
"""An OAuth2 authorization code for completing authorization flow"""
token_size = 20
_ttl = 10*60
_defaults = dict(client_id="",
redirect_uri="",
scope="",
used=False,
)
_bool_props = ("used",)
_int_props = ("user_id",)
_warn_on_partial_ttl = False
_use_db = True
_use_new_ring = True
@classmethod
def _new(cls, client_id, redirect_uri, user_id, scope):
return super(OAuth2AuthorizationCode, cls)._new(
client_id=client_id,
redirect_uri=redirect_uri,
user_id=user_id,
scope=scope)
@classmethod
def get_token(cls, _id):
token = super(OAuth2AuthorizationCode, cls).get_token(_id)
if token and not token.used:
return token
else:
return False
@classmethod
def use_token(cls, _id, client_id, redirect_uri):
token = cls.get_token(_id)
if token and token.client_id == client_id and token.redirect_uri == redirect_uri:
token.used = True
token._commit()
return token
else:
return False
class OAuth2AccessToken(OAuth2Token):
"""An OAuth2 access token for accessing protected resources"""
token_size = 20
_ttl = 10*60
_defaults = dict(scope="",
token_type="bearer",
)
_int_props = ("user_id",)
_use_db = True
_use_new_ring = True
@classmethod
def _new(cls, user_id, scope):
return super(OAuth2AccessToken, cls)._new(
user_id=user_id,
scope=scope)

View File

@@ -4755,3 +4755,140 @@ tr.gold-accent + tr > td {
box-shadow: inset 0px 1px 1px hsla(0,0%,0%,.3), 0px 1px 0px hsla(0,0%,100%,.6);
}
.infobar.client-info {
position: relative;
margin: 10px 2%;
width: 94%;
height: 48px;
}
.infobar.client-info .icon img {
position: absolute;
left: 10px;
width: 48px;
height: 48px;
}
.infobar.client-info .md {
line-height: 48px;
margin-left: 56px;
}
.infobar.client-info .md p {
margin: 0;
}
.oauth2-authorize {
position: relative;
background: url(/static/snoo-tray.png) no-repeat;
width: 415px;
height: 235px;
margin: 40px auto 0;
padding-left: 280px;
padding-top: 18px;
}
.oauth2-authorize h1 a {
display: block;
font-weight: bold;
font-size: 1.5em;
letter-spacing: -.04em;
}
.oauth2-authorize .icon {
position: absolute;
left: 160px;
top: 77px;
}
.oauth2-authorize .access,
.infobar.client-info {
background: #f7f7f7;
border: 1px solid #b3b3b3;
}
.oauth2-authorize .access {
position: relative;
margin: 0 -12px;
padding: 10px 15px;
font-size: 1.5em;
line-height: 1.5em;
}
.oauth2-authorize .access:before {
position: absolute;
display: block;
content: '';
border-width: 9px;
border-style: solid solid outset; /* mitigates firefox drawing a thicker arrow */
border-color: transparent #b3b3b3 transparent transparent;
left: -19px;
top: 13px;
}
.oauth2-authorize .access:after {
position: absolute;
display: block;
content: '';
border: 9px solid;
border-color: transparent #f7f7f7 transparent transparent;
left: -18px;
top: 13px;
}
.oauth2-authorize h2 {
font-size: 1em;
font-weight: normal;
color: black;
}
.oauth2-authorize ul {
list-style-type: disc;
padding-left: 25px;
}
.oauth2-authorize .notice {
color: #333;
font-size: .85em;
margin: .5em 0;
}
.oauth2-authorize .fancybutton {
margin: 0;
margin-right: 1em;
cursor: pointer;
}
.oauth2-authorize .fancybutton.allow {
color: white;
background: #ff4500;
border-color: #541700;
box-shadow: inset 0px 1px 0px rgba(255, 255, 255, .25);
text-shadow: 0px 1px 0px rgba(0, 0, 0, .7);
}
.oauth2-authorize .fancybutton.allow:hover {
background: #ff571a;
}
.oauth2-authorize .fancybutton.allow:active {
background: #eb3f00;
box-shadow: inset 0px -1px 0px rgba(255, 255, 255, .25);
}
.oauth2-authorize .fancybutton.decline {
color: black;
background: #eee;
border-color: #555;
box-shadow: inset 0px 1px 0px rgba(255, 255, 255, .5);
text-shadow: 0px 1px 0px rgba(255, 255, 255, .7);
}
.oauth2-authorize .fancybutton.decline:hover {
background: #f7f7f7;
}
.oauth2-authorize .fancybutton.decline:active {
background: #e4e4e4;
box-shadow: inset 0px -1px 0px white;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,30 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be consistent
## with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is Reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is CondeNet, Inc.
##
## All portions of the code written by CondeNet are Copyright (c) 2006-2010
## CondeNet, Inc. All Rights Reserved.
################################################################################
<%!
from r2.lib.template_helpers import s3_https_if_secure
from r2.lib.filters import safemarkdown
%>
<%namespace file="utils.html" import="img_link"/>
<div class="infobar ${thing.extra_class}">
${img_link(thing.client.name, s3_https_if_secure(thing.client.icon_url), thing.client.about_url, _class="icon")}
${unsafe(safemarkdown(thing.message % dict(app_name=thing.client.name, app_about_url=thing.client.about_url)))}
</div>

View File

@@ -0,0 +1,49 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be consistent
## with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is Reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is CondeNet, Inc.
##
## All portions of the code written by CondeNet are Copyright (c) 2006-2010
## CondeNet, Inc. All Rights Reserved.
################################################################################
<%!
from r2.lib.template_helpers import static, s3_https_if_secure
from r2.lib.filters import safemarkdown
%>
<div class="content oauth2-authorize">
<img class="icon" src="${s3_https_if_secure(thing.client.icon_url)}" alt="${thing.client.name} icon" />
${unsafe(safemarkdown(_("#[%(app_name)s](%(app_about_url)s) requests to connect with your reddit account.")
% dict(app_name=thing.client.name, app_about_url=thing.client.about_url), wrap=False))}
<div class="access">
<h2>${_("Allow %(app_name)s to:") % dict(app_name=thing.client.name)}</h2>
<ul>
<li>${thing.scope["description"]}</li>
</ul>
<p class="notice">${_("%(app_name)s will not be able to access your reddit password.") % dict(app_name=thing.client.name)}</p>
<form method="post" action="/api/v1/authorize" class="pretty-form">
<input type="hidden" name="client_id" value="${thing.client._id}" />
<input type="hidden" name="redirect_uri" value="${thing.redirect_uri}" />
<input type="hidden" name="scope" value="${thing.scope['id']}" />
<input type="hidden" name="state" value="${thing.state}" />
<input type="hidden" name="uh" value="${c.modhash}"/>
<div>
<input type="submit" class="fancybutton allow" name="authorize" value="${_("Allow")}" />
<input type="submit" class="fancybutton decline" value="${_("Decline")}" />
</div>
</form>
</div>
</div>