diff --git a/auth/scopes.py b/auth/scopes.py
index 6dbda6c72..30d88169d 100644
--- a/auth/scopes.py
+++ b/auth/scopes.py
@@ -1,25 +1,33 @@
READ_REPO = {
'scope': 'repo:read',
- 'description': ('Grants read-only access to all repositories for which the granting user or '
- ' robot has access.')
+ 'icon': 'fa-hdd-o',
+ '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 = {
'scope': 'repo:write',
- 'description': ('Grants read-write access to all repositories for which the granting user or '
- 'robot has access, and is a superset of repo:read.')
+ 'icon': 'fa-hdd-o',
+ '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 = {
'scope': 'repo:admin',
- 'description': ('Grants administrator access to all repositories for which the granting user or '
- 'robot has access, and is a superset of repo:read and repo:write.')
+ 'icon': 'fa-hdd-o',
+ '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 = {
'scope': 'repo:create',
- 'description': ('Grants create repository access to all namespaces for which the granting user '
- 'or robot is allowed to create repositories.')
+ 'icon': 'fa-plus',
+ '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)}
@@ -32,3 +40,25 @@ def scopes_from_scope_string(scopes):
def validate_scope_string(scopes):
decoded = scopes_from_scope_string(scopes)
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
diff --git a/data/database.py b/data/database.py
index 840265fae..55e1e0471 100644
--- a/data/database.py
+++ b/data/database.py
@@ -275,7 +275,10 @@ class OAuthApplication(BaseModel):
client_id = CharField(index=True, default=random_string_generator(length=20))
client_secret = CharField(default=random_string_generator(length=40))
redirect_uri = CharField()
+ application_uri = CharField()
organization = ForeignKeyField(User)
+ name = CharField()
+ description = TextField(default='')
class OAuthAuthorizationCode(BaseModel):
diff --git a/data/model/oauth.py b/data/model/oauth.py
index 5f64f79f9..475893b4d 100644
--- a/data/model/oauth.py
+++ b/data/model/oauth.py
@@ -11,11 +11,13 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
raise NotImplementedError('Subclasses must fill in the ability to get the authorized_user.')
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:
- OAuthApplication.get(client_id=client_id)
- return True
+ return OAuthApplication.get(client_id=client_id)
except OAuthApplication.DoesNotExist:
- return False
+ return None
def validate_client_secret(self, client_id, client_secret):
try:
@@ -33,12 +35,35 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
except OAuthApplication.DoesNotExist:
return False
- def validate_scope(self, client_id, scope):
- return scopes.validate_scope_string(scope)
+ def validate_scope(self, client_id, scopes_string):
+ return scopes.validate_scope_string(scopes_string)
def validate_access(self):
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):
try:
found = (OAuthAuthorizationCode
@@ -109,7 +134,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
is_valid_client_id = self.validate_client_id(client_id)
is_valid_access = self.validate_access()
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
if not is_valid_client_id:
@@ -120,7 +145,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
err = 'access_denied'
return self._make_redirect_error_response(redirect_uri, err)
- if not is_valid_scope:
+ if not are_valid_scopes:
err = 'invalid_scope'
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)
-def create_application(org, redirect_uri, **kwargs):
- return OAuthApplication.create(organization=org, redirect_uri=redirect_uri, **kwargs)
+def create_application(org, name, application_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):
try:
@@ -150,4 +175,4 @@ def validate_access_token(access_token):
.get())
return found
except OAuthAccessToken.DoesNotExist:
- return None
\ No newline at end of file
+ return None
diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py
index 087b51c1c..b2b7f3ba5 100644
--- a/endpoints/api/discovery.py
+++ b/endpoints/api/discovery.py
@@ -102,7 +102,7 @@ def swagger_route_data():
swagger_data = {
'apiVersion': 'v1',
'swaggerVersion': '1.2',
- 'basePath': 'http://ci.devtable.com:5000',
+ 'basePath': 'http://localhost:5000',
'resourcePath': '/',
'info': {
'title': 'Quay.io API',
@@ -119,7 +119,7 @@ def swagger_route_data():
"implicit": {
"tokenName": "access_token",
"loginEndpoint": {
- "url": "http://ci.devtable.com:5000/oauth/authorize",
+ "url": "http://localhost:5000/oauth/authorize",
},
},
},
diff --git a/endpoints/web.py b/endpoints/web.py
index 5455ec573..5ead821da 100644
--- a/endpoints/web.py
+++ b/endpoints/web.py
@@ -14,8 +14,10 @@ from auth.permissions import AdministerOrganizationPermission
from util.invoice import renderInvoiceToPdf
from util.seo import render_snapshot
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.gravatar import compute_hash
+from auth import scopes
logger = logging.getLogger(__name__)
@@ -239,6 +241,27 @@ class FlaskAuthorizationProvider(DatabaseAuthorizationProvider):
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'])
@no_cache
def request_authorization_code():
@@ -248,6 +271,31 @@ def request_authorization_code():
redirect_uri = request.args.get('redirect_uri', 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':
return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope)
else:
diff --git a/initdb.py b/initdb.py
index 3cbd8008b..77fa9f162 100644
--- a/initdb.py
+++ b/initdb.py
@@ -339,7 +339,12 @@ def populate_database():
org.stripe_id = TEST_STRIPE_ID
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)
diff --git a/static/css/quay.css b/static/css/quay.css
index bde24600a..f953428a2 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -3392,4 +3392,85 @@ pre.command:before {
.slideinout.ng-hide-add, .slideinout.ng-hide-remove {
display: block !important;
-}
\ No newline at end of file
+}
+
+.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;
+}
diff --git a/templates/base.html b/templates/base.html
index 5bbda45f3..852302f90 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -47,22 +47,22 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
{% block added_dependencies %}
@@ -73,10 +73,10 @@
window.__token = '{{ csrf_token() }}';
-
-
-
-
+
+
+
+