From 53fb7f4136efe2bc0d6c457a866ceb6faaf625e3 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 19 Aug 2014 19:05:28 -0400 Subject: [PATCH 1/4] Add documentation for all path parameters --- endpoints/api/__init__.py | 3 +++ endpoints/api/billing.py | 5 ++++- endpoints/api/build.py | 8 +++++++- endpoints/api/discovery.py | 4 ++-- endpoints/api/image.py | 7 ++++++- endpoints/api/logs.py | 4 +++- endpoints/api/organization.py | 13 ++++++++++++- endpoints/api/permission.py | 8 +++++++- endpoints/api/prototype.py | 5 ++++- endpoints/api/repoemail.py | 5 ++++- endpoints/api/repository.py | 6 +++++- endpoints/api/repositorynotification.py | 8 +++++++- endpoints/api/repotoken.py | 5 ++++- endpoints/api/search.py | 3 ++- endpoints/api/superuser.py | 3 ++- endpoints/api/tag.py | 7 ++++++- endpoints/api/team.py | 10 +++++++++- endpoints/api/trigger.py | 18 +++++++++++++++++- endpoints/api/user.py | 4 +++- 19 files changed, 107 insertions(+), 19 deletions(-) diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index e8dab28dc..f6c3ecc3f 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -155,6 +155,9 @@ internal_only = add_method_metadata('internal', True) def path_param(name, description): def add_param(func): + if not func: + return func + if '__api_path_params' not in dir(func): func.__api_path_params = {} func.__api_path_params[name] = { diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index 3e13df6b6..89c94963b 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -4,7 +4,7 @@ from flask import request from app import billing from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin, show_if, hide_if) + require_user_admin, show_if, hide_if, path_param) from endpoints.api.subscribe import subscribe, subscription_view from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user @@ -135,6 +135,7 @@ class UserCard(ApiResource): @resource('/v1/organization//card') +@path_param('orgname', 'The name of the organization') @internal_only @related_user_resource(UserCard) @show_if(features.BILLING) @@ -242,6 +243,7 @@ class UserPlan(ApiResource): @resource('/v1/organization//plan') +@path_param('orgname', 'The name of the organization') @internal_only @related_user_resource(UserPlan) @show_if(features.BILLING) @@ -323,6 +325,7 @@ class UserInvoiceList(ApiResource): @resource('/v1/organization//invoices') +@path_param('orgname', 'The name of the organization') @internal_only @related_user_resource(UserInvoiceList) @show_if(features.BILLING) diff --git a/endpoints/api/build.py b/endpoints/api/build.py index 21d554069..7fa11cc15 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -6,7 +6,8 @@ from flask import request from app import app, userfiles as user_files, build_logs from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource, require_repo_read, require_repo_write, validate_json_request, - ApiResource, internal_only, format_date, api, Unauthorized, NotFound) + ApiResource, internal_only, format_date, api, Unauthorized, NotFound, + path_param) from endpoints.common import start_build from endpoints.trigger import BuildTrigger from data import model @@ -86,6 +87,7 @@ def build_status_view(build_obj, can_write=False): @resource('/v1/repository//build/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryBuildList(RepositoryParamResource): """ Resource related to creating and listing repository builds. """ schemas = { @@ -190,6 +192,8 @@ class RepositoryBuildList(RepositoryParamResource): @resource('/v1/repository//build//status') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('build_uuid', 'The UUID of the build') class RepositoryBuildStatus(RepositoryParamResource): """ Resource for dealing with repository build status. """ @require_repo_read @@ -205,6 +209,8 @@ class RepositoryBuildStatus(RepositoryParamResource): @resource('/v1/repository//build//logs') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('build_uuid', 'The UUID of the build') class RepositoryBuildLogs(RepositoryParamResource): """ Resource for loading repository build logs. """ @require_repo_write diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index ee8702636..47181dc9f 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -100,7 +100,7 @@ def swagger_route_data(include_internal=False, compact=False): if not compact: new_operation.update({ 'type': 'void', - 'summary': method.__doc__ if method.__doc__ else '', + 'summary': method.__doc__.strip() if method.__doc__ else '', 'parameters': parameters, }) @@ -125,7 +125,7 @@ def swagger_route_data(include_internal=False, compact=False): swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule) new_resource = { 'path': swagger_path, - 'description': view_class.__doc__ if view_class.__doc__ else "", + 'description': view_class.__doc__.strip() if view_class.__doc__ else "", 'operations': operations, 'name': fully_qualified_name(view_class), } diff --git a/endpoints/api/image.py b/endpoints/api/image.py index 3060053ad..6da110eff 100644 --- a/endpoints/api/image.py +++ b/endpoints/api/image.py @@ -4,7 +4,7 @@ from collections import defaultdict from app import storage as store from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource, - format_date, NotFound) + format_date, NotFound, path_param) from data import model from util.cache import cache_control_flask_restful @@ -29,6 +29,7 @@ def image_view(image): @resource('/v1/repository//image/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryImageList(RepositoryParamResource): """ Resource for listing repository images. """ @require_repo_read @@ -54,6 +55,8 @@ class RepositoryImageList(RepositoryParamResource): @resource('/v1/repository//image/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('image_id', 'The Docker image ID') class RepositoryImage(RepositoryParamResource): """ Resource for handling repository images. """ @require_repo_read @@ -68,6 +71,8 @@ class RepositoryImage(RepositoryParamResource): @resource('/v1/repository//image//changes') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('image_id', 'The Docker image ID') class RepositoryImageChanges(RepositoryParamResource): """ Resource for handling repository image change lists. """ diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index 2ce2bbb30..606c8b198 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args, RepositoryParamResource, require_repo_admin, related_user_resource, format_date, Unauthorized, NotFound, require_user_admin, - internal_only) + internal_only, path_param) from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from data import model @@ -63,6 +63,7 @@ def get_logs(start_time, end_time, performer_name=None, repository=None, namespa @resource('/v1/repository//logs') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') @internal_only class RepositoryLogs(RepositoryParamResource): """ Resource for fetching logs for the specific repository. """ @@ -103,6 +104,7 @@ class UserLogs(ApiResource): @resource('/v1/organization//logs') +@path_param('orgname', 'The name of the organization') @internal_only @related_user_resource(UserLogs) class OrgLogs(ApiResource): diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index f6a381ace..c4c870fec 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -5,7 +5,7 @@ from flask import request from app import billing as stripe from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin, log_action, show_if) + require_user_admin, log_action, show_if, path_param) from endpoints.api.team import team_view from endpoints.api.user import User, PrivateRepositories from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission, @@ -97,6 +97,7 @@ class OrganizationList(ApiResource): @resource('/v1/organization/') +@path_param('orgname', 'The name of the organization') @internal_only @related_user_resource(User) class Organization(ApiResource): @@ -163,6 +164,7 @@ class Organization(ApiResource): @resource('/v1/organization//private') +@path_param('orgname', 'The name of the organization') @internal_only @related_user_resource(PrivateRepositories) @show_if(features.BILLING) @@ -199,6 +201,7 @@ class OrgPrivateRepositories(ApiResource): @resource('/v1/organization//members') +@path_param('orgname', 'The name of the organization') @internal_only class OrgnaizationMemberList(ApiResource): """ Resource for listing the members of an organization. """ @@ -232,6 +235,8 @@ class OrgnaizationMemberList(ApiResource): @resource('/v1/organization//members/') +@path_param('orgname', 'The name of the organization') +@path_param('membername', 'The username of the organization member') @internal_only class OrganizationMember(ApiResource): """ Resource for managing individual organization members. """ @@ -265,6 +270,7 @@ class OrganizationMember(ApiResource): @resource('/v1/app/') +@path_param('client_id', 'The OAuth client ID') class ApplicationInformation(ApiResource): """ Resource that returns public information about a registered application. """ @nickname('getApplicationInformation') @@ -302,6 +308,7 @@ def app_view(application): @resource('/v1/organization//applications') +@path_param('orgname', 'The name of the organization') @internal_only class OrganizationApplications(ApiResource): """ Resource for managing applications defined by an organizations. """ @@ -386,6 +393,8 @@ class OrganizationApplications(ApiResource): @resource('/v1/organization//applications/') +@path_param('orgname', 'The name of the organization') +@path_param('client_id', 'The OAuth client ID') @internal_only class OrganizationApplicationResource(ApiResource): """ Resource for managing an application defined by an organizations. """ @@ -498,6 +507,8 @@ class OrganizationApplicationResource(ApiResource): @resource('/v1/organization//applications//resetclientsecret') +@path_param('orgname', 'The name of the organization') +@path_param('client_id', 'The OAuth client ID') @internal_only class OrganizationApplicationResetClientSecret(ApiResource): """ Custom verb for resetting the client secret of an application. """ diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py index 601a549e3..f98c8c54e 100644 --- a/endpoints/api/permission.py +++ b/endpoints/api/permission.py @@ -3,7 +3,7 @@ import logging from flask import request from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, - log_action, request_error, validate_json_request) + log_action, request_error, validate_json_request, path_param) from data import model @@ -26,6 +26,7 @@ def wrap_role_view_org(role_json, user, org_members): @resource('/v1/repository//permissions/team/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryTeamPermissionList(RepositoryParamResource): """ Resource for repository team permissions. """ @require_repo_admin @@ -41,6 +42,7 @@ class RepositoryTeamPermissionList(RepositoryParamResource): @resource('/v1/repository//permissions/user/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryUserPermissionList(RepositoryParamResource): """ Resource for repository user permissions. """ @require_repo_admin @@ -80,6 +82,8 @@ class RepositoryUserPermissionList(RepositoryParamResource): @resource('/v1/repository//permissions/user/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('username', 'The username of the user to which the permission applies') class RepositoryUserPermission(RepositoryParamResource): """ Resource for managing individual user permissions. """ schemas = { @@ -175,6 +179,8 @@ class RepositoryUserPermission(RepositoryParamResource): @resource('/v1/repository//permissions/team/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('teamname', 'The name of the team to which the permission applies') class RepositoryTeamPermission(RepositoryParamResource): """ Resource for managing individual team permissions. """ schemas = { diff --git a/endpoints/api/prototype.py b/endpoints/api/prototype.py index bedc19832..8e43bbf42 100644 --- a/endpoints/api/prototype.py +++ b/endpoints/api/prototype.py @@ -1,7 +1,7 @@ from flask import request from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, - log_action, Unauthorized, NotFound, internal_only) + log_action, Unauthorized, NotFound, internal_only, path_param) from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from data import model @@ -54,6 +54,7 @@ def log_prototype_action(action_kind, orgname, prototype, **kwargs): @resource('/v1/organization//prototypes') +@path_param('orgname', 'The name of the organization') @internal_only class PermissionPrototypeList(ApiResource): """ Resource for listing and creating permission prototypes. """ @@ -179,6 +180,8 @@ class PermissionPrototypeList(ApiResource): @resource('/v1/organization//prototypes/') +@path_param('orgname', 'The name of the organization') +@path_param('prototypeid', 'The ID of the prototype') @internal_only class PermissionPrototype(ApiResource): """ Resource for managingin individual permission prototypes. """ diff --git a/endpoints/api/repoemail.py b/endpoints/api/repoemail.py index 6585bbc49..0b7c66917 100644 --- a/endpoints/api/repoemail.py +++ b/endpoints/api/repoemail.py @@ -3,7 +3,8 @@ import logging from flask import request, abort from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, - log_action, validate_json_request, NotFound, internal_only) + log_action, validate_json_request, NotFound, internal_only, + path_param) from app import tf from data import model @@ -26,6 +27,8 @@ def record_view(record): @internal_only @resource('/v1/repository//authorizedemail/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('email', 'The e-mail address') class RepositoryAuthorizedEmail(RepositoryParamResource): """ Resource for checking and authorizing e-mail addresses to receive repo notifications. """ @require_repo_admin diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 17a35fea1..b7ff899f6 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -7,7 +7,9 @@ from data import model from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request, require_repo_read, require_repo_write, require_repo_admin, RepositoryParamResource, resource, query_param, parse_args, ApiResource, - request_error, require_scope, Unauthorized, NotFound, InvalidRequest) + request_error, require_scope, Unauthorized, NotFound, InvalidRequest, + path_param) + from auth.permissions import (ModifyRepositoryPermission, AdministerRepositoryPermission, CreateRepositoryPermission, ReadRepositoryPermission) from auth.auth_context import get_authenticated_user @@ -140,6 +142,7 @@ class RepositoryList(ApiResource): @resource('/v1/repository/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class Repository(RepositoryParamResource): """Operations for managing a specific repository.""" schemas = { @@ -233,6 +236,7 @@ class Repository(RepositoryParamResource): @resource('/v1/repository//changevisibility') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryVisibility(RepositoryParamResource): """ Custom verb for changing the visibility of the repository. """ schemas = { diff --git a/endpoints/api/repositorynotification.py b/endpoints/api/repositorynotification.py index 1fab89dd0..3a518736e 100644 --- a/endpoints/api/repositorynotification.py +++ b/endpoints/api/repositorynotification.py @@ -4,7 +4,8 @@ from flask import request, abort from app import notification_queue from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, - log_action, validate_json_request, api, NotFound, request_error) + log_action, validate_json_request, api, NotFound, request_error, + path_param) from endpoints.notificationevent import NotificationEvent from endpoints.notificationmethod import (NotificationMethod, CannotValidateNotificationMethodException) @@ -28,6 +29,7 @@ def notification_view(notification): @resource('/v1/repository//notification/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryNotificationList(RepositoryParamResource): """ Resource for dealing with listing and creating notifications on a repository. """ schemas = { @@ -95,6 +97,8 @@ class RepositoryNotificationList(RepositoryParamResource): @resource('/v1/repository//notification/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('uuid', 'The UUID of the notification') class RepositoryNotification(RepositoryParamResource): """ Resource for dealing with specific notifications. """ @require_repo_admin @@ -122,6 +126,8 @@ class RepositoryNotification(RepositoryParamResource): @resource('/v1/repository//notification//test') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('uuid', 'The UUID of the notification') class TestRepositoryNotification(RepositoryParamResource): """ Resource for queuing a test of a notification. """ @require_repo_admin diff --git a/endpoints/api/repotoken.py b/endpoints/api/repotoken.py index b430d2b4e..a6b28275b 100644 --- a/endpoints/api/repotoken.py +++ b/endpoints/api/repotoken.py @@ -3,7 +3,7 @@ import logging from flask import request from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, - log_action, validate_json_request, NotFound) + log_action, validate_json_request, NotFound, path_param) from data import model @@ -19,6 +19,7 @@ def token_view(token_obj): @resource('/v1/repository//tokens/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryTokenList(RepositoryParamResource): """ Resource for creating and listing repository tokens. """ schemas = { @@ -66,6 +67,8 @@ class RepositoryTokenList(RepositoryParamResource): @resource('/v1/repository//tokens/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('code', 'The token code') class RepositoryToken(RepositoryParamResource): """ Resource for managing individual tokens. """ schemas = { diff --git a/endpoints/api/search.py b/endpoints/api/search.py index 7cb1a1fda..83a398b6b 100644 --- a/endpoints/api/search.py +++ b/endpoints/api/search.py @@ -1,5 +1,5 @@ from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource, - require_scope) + require_scope, path_param) from data import model from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission, ReadRepositoryPermission, UserAdminPermission, @@ -12,6 +12,7 @@ from util.gravatar import compute_hash @resource('/v1/entities/') class EntitySearch(ApiResource): """ Resource for searching entities. """ + @path_param('prefix', 'The prefix of the entities being looked up') @parse_args @query_param('namespace', 'Namespace to use when querying for org entities.', type=str, default='') diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 3ade5f1ed..3eacd2f97 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -8,7 +8,7 @@ from flask import request from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, NotFound, require_user_admin, format_date, InvalidToken, require_scope, format_date, hide_if, show_if, parse_args, - query_param, abort) + query_param, abort, path_param) from endpoints.api.logs import get_logs @@ -86,6 +86,7 @@ class SuperUserList(ApiResource): @resource('/v1/superuser/users/') +@path_param('username', 'The username of the user being managed') @internal_only @show_if(features.SUPER_USERS) class SuperUserManagement(ApiResource): diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index f9210881c..751fd6f60 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -1,13 +1,16 @@ from flask import request from endpoints.api import (resource, nickname, require_repo_read, require_repo_write, - RepositoryParamResource, log_action, NotFound, validate_json_request) + RepositoryParamResource, log_action, NotFound, validate_json_request, + path_param) from endpoints.api.image import image_view from data import model from auth.auth_context import get_authenticated_user @resource('/v1/repository//tag/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('tag', 'The name of the tag') class RepositoryTag(RepositoryParamResource): """ Resource for managing repository tags. """ schemas = { @@ -73,6 +76,8 @@ class RepositoryTag(RepositoryParamResource): @resource('/v1/repository//tag//images') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('tag', 'The name of the tag') class RepositoryTagImages(RepositoryParamResource): """ Resource for listing the images in a specific repository tag. """ @require_repo_read diff --git a/endpoints/api/team.py b/endpoints/api/team.py index 0631cc028..bfbeae650 100644 --- a/endpoints/api/team.py +++ b/endpoints/api/team.py @@ -1,7 +1,8 @@ from flask import request from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, - log_action, Unauthorized, NotFound, internal_only, require_scope) + log_action, Unauthorized, NotFound, internal_only, require_scope, + path_param) from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission from auth.auth_context import get_authenticated_user from auth import scopes @@ -28,6 +29,8 @@ def member_view(member): @resource('/v1/organization//team/') +@path_param('orgname', 'The name of the organization') +@path_param('teamname', 'The name of the team') @internal_only class OrganizationTeam(ApiResource): """ Resource for manging an organization's teams. """ @@ -111,6 +114,8 @@ class OrganizationTeam(ApiResource): @resource('/v1/organization//team//members') +@path_param('orgname', 'The name of the organization') +@path_param('teamname', 'The name of the team') @internal_only class TeamMemberList(ApiResource): """ Resource for managing the list of members for a team. """ @@ -137,6 +142,9 @@ class TeamMemberList(ApiResource): @resource('/v1/organization//team//members/') +@path_param('orgname', 'The name of the organization') +@path_param('teamname', 'The name of the team') +@path_param('membername', 'The username of the team member') class TeamMember(ApiResource): """ Resource for managing individual members of a team. """ @require_scope(scopes.ORG_ADMIN) diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 4ec20bfdc..857b5116d 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -8,7 +8,8 @@ from urlparse import urlunparse from app import app from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, log_action, request_error, query_param, parse_args, internal_only, - validate_json_request, api, Unauthorized, NotFound, InvalidRequest) + validate_json_request, api, Unauthorized, NotFound, InvalidRequest, + path_param) from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus, get_trigger_config) from endpoints.common import start_build @@ -30,6 +31,7 @@ def _prepare_webhook_url(scheme, username, password, hostname, path): @resource('/v1/repository//trigger/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class BuildTriggerList(RepositoryParamResource): """ Resource for listing repository build triggers. """ @@ -44,6 +46,8 @@ class BuildTriggerList(RepositoryParamResource): @resource('/v1/repository//trigger/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('trigger_uuid', 'The UUID of the build trigger') class BuildTrigger(RepositoryParamResource): """ Resource for managing specific build triggers. """ @@ -90,6 +94,8 @@ class BuildTrigger(RepositoryParamResource): @resource('/v1/repository//trigger//subdir') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('trigger_uuid', 'The UUID of the build trigger') @internal_only class BuildTriggerSubdirs(RepositoryParamResource): """ Custom verb for fetching the subdirs which are buildable for a trigger. """ @@ -137,6 +143,8 @@ class BuildTriggerSubdirs(RepositoryParamResource): @resource('/v1/repository//trigger//activate') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('trigger_uuid', 'The UUID of the build trigger') @internal_only class BuildTriggerActivate(RepositoryParamResource): """ Custom verb for activating a build trigger once all required information has been collected. @@ -237,6 +245,8 @@ class BuildTriggerActivate(RepositoryParamResource): @resource('/v1/repository//trigger//analyze') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('trigger_uuid', 'The UUID of the build trigger') @internal_only class BuildTriggerAnalyze(RepositoryParamResource): """ Custom verb for analyzing the config for a build trigger and suggesting various changes @@ -372,6 +382,8 @@ class BuildTriggerAnalyze(RepositoryParamResource): @resource('/v1/repository//trigger//start') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('trigger_uuid', 'The UUID of the build trigger') class ActivateBuildTrigger(RepositoryParamResource): """ Custom verb to manually activate a build trigger. """ @@ -408,6 +420,8 @@ class ActivateBuildTrigger(RepositoryParamResource): @resource('/v1/repository//trigger//builds') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('trigger_uuid', 'The UUID of the build trigger') class TriggerBuildList(RepositoryParamResource): """ Resource to represent builds that were activated from the specified trigger. """ @require_repo_admin @@ -425,6 +439,8 @@ class TriggerBuildList(RepositoryParamResource): @resource('/v1/repository//trigger//sources') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('trigger_uuid', 'The UUID of the build trigger') @internal_only class BuildTriggerSources(RepositoryParamResource): """ Custom verb to fetch the list of build sources for the trigger config. """ diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 3d79a806d..8ba2c0507 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -7,7 +7,7 @@ from flask.ext.principal import identity_changed, AnonymousIdentity from app import app, billing as stripe, authentication from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, - log_action, internal_only, NotFound, require_user_admin, + log_action, internal_only, NotFound, require_user_admin, path_param, InvalidToken, require_scope, format_date, hide_if, show_if, license_error) from endpoints.api.subscribe import subscribe from endpoints.common import common_login @@ -412,6 +412,7 @@ class UserNotificationList(ApiResource): @resource('/v1/user/notifications/') +@path_param('uuid', 'The uuid of the user notification') @internal_only class UserNotification(ApiResource): schemas = { @@ -482,6 +483,7 @@ class UserAuthorizationList(ApiResource): @resource('/v1/user/authorizations/') +@path_param('access_token_uuid', 'The uuid of the access token') @internal_only class UserAuthorization(ApiResource): @require_user_admin From 4fd249589d9191b3d615a1f08bcfa3198aa7e346 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 19 Aug 2014 19:21:41 -0400 Subject: [PATCH 2/4] Add scopes to many org admin methods and remove the internal_only on ones we can now expose --- endpoints/api/billing.py | 8 ++++++-- endpoints/api/logs.py | 6 +++--- endpoints/api/organization.py | 26 ++++++++++++++++++-------- endpoints/api/prototype.py | 10 +++++++--- endpoints/api/team.py | 7 +++++-- endpoints/api/trigger.py | 1 - 6 files changed, 39 insertions(+), 19 deletions(-) diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index 89c94963b..f5b022ca6 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -4,10 +4,11 @@ from flask import request from app import billing from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin, show_if, hide_if, path_param) + require_user_admin, show_if, hide_if, path_param, require_scope) from endpoints.api.subscribe import subscribe, subscription_view from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user +from auth import scopes from data import model from data.billing import PLANS @@ -158,6 +159,7 @@ class OrganizationCard(ApiResource): }, } + @require_scope(scopes.ORG_ADMIN) @nickname('getOrgCard') def get(self, orgname): """ Get the organization's credit card. """ @@ -270,6 +272,7 @@ class OrganizationPlan(ApiResource): }, } + @require_scope(scopes.ORG_ADMIN) @nickname('updateOrgSubscription') @validate_json_request('OrgSubscription') def put(self, orgname): @@ -284,6 +287,7 @@ class OrganizationPlan(ApiResource): raise Unauthorized() + @require_scope(scopes.ORG_ADMIN) @nickname('getOrgSubscription') def get(self, orgname): """ Fetch any existing subscription for the org. """ @@ -326,11 +330,11 @@ class UserInvoiceList(ApiResource): @resource('/v1/organization//invoices') @path_param('orgname', 'The name of the organization') -@internal_only @related_user_resource(UserInvoiceList) @show_if(features.BILLING) class OrgnaizationInvoiceList(ApiResource): """ Resource for listing an orgnaization's invoices. """ + @require_scope(scopes.ORG_ADMIN) @nickname('listOrgInvoices') def get(self, orgname): """ List the invoices for the specified orgnaization. """ diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index 606c8b198..6a45a497f 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -5,10 +5,11 @@ from datetime import datetime, timedelta from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args, RepositoryParamResource, require_repo_admin, related_user_resource, format_date, Unauthorized, NotFound, require_user_admin, - internal_only, path_param) + internal_only, path_param, require_scope) from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from data import model +from auth import scopes def log_view(log): @@ -64,7 +65,6 @@ def get_logs(start_time, end_time, performer_name=None, repository=None, namespa @resource('/v1/repository//logs') @path_param('repository', 'The full path of the repository. e.g. namespace/name') -@internal_only class RepositoryLogs(RepositoryParamResource): """ Resource for fetching logs for the specific repository. """ @require_repo_admin @@ -105,7 +105,6 @@ class UserLogs(ApiResource): @resource('/v1/organization//logs') @path_param('orgname', 'The name of the organization') -@internal_only @related_user_resource(UserLogs) class OrgLogs(ApiResource): """ Resource for fetching logs for the entire organization. """ @@ -114,6 +113,7 @@ class OrgLogs(ApiResource): @query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('performer', 'Username for which to filter logs.', type=str) + @require_scope(scopes.ORG_ADMIN) def get(self, args, orgname): """ List the logs for the specified organization. """ permission = AdministerOrganizationPermission(orgname) diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index c4c870fec..83bd9f1ea 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -5,12 +5,14 @@ from flask import request from app import billing as stripe from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin, log_action, show_if, path_param) + require_user_admin, log_action, show_if, path_param, + require_scope) from endpoints.api.team import team_view from endpoints.api.user import User, PrivateRepositories from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission, CreateRepositoryPermission) from auth.auth_context import get_authenticated_user +from auth import scopes from data import model from data.billing import get_plan from util.gravatar import compute_hash @@ -98,7 +100,6 @@ class OrganizationList(ApiResource): @resource('/v1/organization/') @path_param('orgname', 'The name of the organization') -@internal_only @related_user_resource(User) class Organization(ApiResource): """ Resource for managing organizations. """ @@ -119,6 +120,8 @@ class Organization(ApiResource): }, }, } + + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganization') def get(self, orgname): """ Get the details for the specified organization """ @@ -134,6 +137,7 @@ class Organization(ApiResource): raise Unauthorized() + @require_scope(scopes.ORG_ADMIN) @nickname('changeOrganizationDetails') @validate_json_request('UpdateOrg') def put(self, orgname): @@ -170,6 +174,8 @@ class Organization(ApiResource): @show_if(features.BILLING) class OrgPrivateRepositories(ApiResource): """ Custom verb to compute whether additional private repositories are available. """ + + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationPrivateAllowed') def get(self, orgname): """ Return whether or not this org is allowed to create new private repositories. """ @@ -202,9 +208,10 @@ class OrgPrivateRepositories(ApiResource): @resource('/v1/organization//members') @path_param('orgname', 'The name of the organization') -@internal_only class OrgnaizationMemberList(ApiResource): """ Resource for listing the members of an organization. """ + + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationMembers') def get(self, orgname): """ List the members of the specified organization. """ @@ -237,9 +244,10 @@ class OrgnaizationMemberList(ApiResource): @resource('/v1/organization//members/') @path_param('orgname', 'The name of the organization') @path_param('membername', 'The username of the organization member') -@internal_only class OrganizationMember(ApiResource): """ Resource for managing individual organization members. """ + + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationMember') def get(self, orgname, membername): """ Get information on the specific orgnaization member. """ @@ -273,6 +281,7 @@ class OrganizationMember(ApiResource): @path_param('client_id', 'The OAuth client ID') class ApplicationInformation(ApiResource): """ Resource that returns public information about a registered application. """ + @nickname('getApplicationInformation') def get(self, client_id): """ Get information on the specified application. """ @@ -309,7 +318,6 @@ def app_view(application): @resource('/v1/organization//applications') @path_param('orgname', 'The name of the organization') -@internal_only class OrganizationApplications(ApiResource): """ Resource for managing applications defined by an organizations. """ schemas = { @@ -345,7 +353,7 @@ class OrganizationApplications(ApiResource): }, } - + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationApplications') def get(self, orgname): """ List the applications for the specified organization """ @@ -361,6 +369,7 @@ class OrganizationApplications(ApiResource): raise Unauthorized() + @require_scope(scopes.ORG_ADMIN) @nickname('createOrganizationApplication') @validate_json_request('NewApp') def post(self, orgname): @@ -395,7 +404,6 @@ class OrganizationApplications(ApiResource): @resource('/v1/organization//applications/') @path_param('orgname', 'The name of the organization') @path_param('client_id', 'The OAuth client ID') -@internal_only class OrganizationApplicationResource(ApiResource): """ Resource for managing an application defined by an organizations. """ schemas = { @@ -433,6 +441,7 @@ class OrganizationApplicationResource(ApiResource): }, } + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationApplication') def get(self, orgname, client_id): """ Retrieves the application with the specified client_id under the specified organization """ @@ -451,6 +460,7 @@ class OrganizationApplicationResource(ApiResource): raise Unauthorized() + @require_scope(scopes.ORG_ADMIN) @nickname('updateOrganizationApplication') @validate_json_request('UpdateApp') def put(self, orgname, client_id): @@ -484,7 +494,7 @@ class OrganizationApplicationResource(ApiResource): return app_view(application) raise Unauthorized() - + @require_scope(scopes.ORG_ADMIN) @nickname('deleteOrganizationApplication') def delete(self, orgname, client_id): """ Deletes the application under this organization. """ diff --git a/endpoints/api/prototype.py b/endpoints/api/prototype.py index 8e43bbf42..d4d736d2c 100644 --- a/endpoints/api/prototype.py +++ b/endpoints/api/prototype.py @@ -1,9 +1,11 @@ from flask import request from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, - log_action, Unauthorized, NotFound, internal_only, path_param) + log_action, Unauthorized, NotFound, internal_only, path_param, + require_scope) from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user +from auth import scopes from data import model @@ -55,7 +57,6 @@ def log_prototype_action(action_kind, orgname, prototype, **kwargs): @resource('/v1/organization//prototypes') @path_param('orgname', 'The name of the organization') -@internal_only class PermissionPrototypeList(ApiResource): """ Resource for listing and creating permission prototypes. """ schemas = { @@ -116,6 +117,7 @@ class PermissionPrototypeList(ApiResource): }, } + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationPrototypePermissions') def get(self, orgname): """ List the existing prototypes for this organization. """ @@ -132,6 +134,7 @@ class PermissionPrototypeList(ApiResource): raise Unauthorized() + @require_scope(scopes.ORG_ADMIN) @nickname('createOrganizationPrototypePermission') @validate_json_request('NewPrototype') def post(self, orgname): @@ -182,7 +185,6 @@ class PermissionPrototypeList(ApiResource): @resource('/v1/organization//prototypes/') @path_param('orgname', 'The name of the organization') @path_param('prototypeid', 'The ID of the prototype') -@internal_only class PermissionPrototype(ApiResource): """ Resource for managingin individual permission prototypes. """ schemas = { @@ -207,6 +209,7 @@ class PermissionPrototype(ApiResource): }, } + @require_scope(scopes.ORG_ADMIN) @nickname('deleteOrganizationPrototypePermission') def delete(self, orgname, prototypeid): """ Delete an existing permission prototype. """ @@ -227,6 +230,7 @@ class PermissionPrototype(ApiResource): raise Unauthorized() + @require_scope(scopes.ORG_ADMIN) @nickname('updateOrganizationPrototypePermission') @validate_json_request('PrototypeUpdate') def put(self, orgname, prototypeid): diff --git a/endpoints/api/team.py b/endpoints/api/team.py index bfbeae650..4ecef0e6e 100644 --- a/endpoints/api/team.py +++ b/endpoints/api/team.py @@ -31,7 +31,6 @@ def member_view(member): @resource('/v1/organization//team/') @path_param('orgname', 'The name of the organization') @path_param('teamname', 'The name of the team') -@internal_only class OrganizationTeam(ApiResource): """ Resource for manging an organization's teams. """ schemas = { @@ -60,6 +59,7 @@ class OrganizationTeam(ApiResource): }, } + @require_scope(scopes.ORG_ADMIN) @nickname('updateOrganizationTeam') @validate_json_request('TeamDescription') def put(self, orgname, teamname): @@ -101,6 +101,7 @@ class OrganizationTeam(ApiResource): raise Unauthorized() + @require_scope(scopes.ORG_ADMIN) @nickname('deleteOrganizationTeam') def delete(self, orgname, teamname): """ Delete the specified team. """ @@ -116,9 +117,10 @@ class OrganizationTeam(ApiResource): @resource('/v1/organization//team//members') @path_param('orgname', 'The name of the organization') @path_param('teamname', 'The name of the team') -@internal_only class TeamMemberList(ApiResource): """ Resource for managing the list of members for a team. """ + + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationTeamMembers') def get(self, orgname, teamname): """ Retrieve the list of members for the specified team. """ @@ -147,6 +149,7 @@ class TeamMemberList(ApiResource): @path_param('membername', 'The username of the team member') class TeamMember(ApiResource): """ Resource for managing individual members of a team. """ + @require_scope(scopes.ORG_ADMIN) @nickname('updateOrganizationTeamMember') def put(self, orgname, teamname, membername): diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 857b5116d..4cc224a00 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -145,7 +145,6 @@ class BuildTriggerSubdirs(RepositoryParamResource): @resource('/v1/repository//trigger//activate') @path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('trigger_uuid', 'The UUID of the build trigger') -@internal_only class BuildTriggerActivate(RepositoryParamResource): """ Custom verb for activating a build trigger once all required information has been collected. """ From 6f1a4030b6366b859341dea7c8db51a56f949317 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 27 Aug 2014 20:57:46 -0400 Subject: [PATCH 3/4] Add response schema validation (only when in TESTING mode) and add one schema. More will be added in a followup CL --- endpoints/api/__init__.py | 25 ++++++++++++++++++++ endpoints/api/discovery.py | 10 ++++++-- endpoints/api/user.py | 47 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index f6c3ecc3f..0234e6820 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -1,6 +1,7 @@ import logging import json +from app import app from flask import Blueprint, request, make_response, jsonify from flask.ext.restful import Resource, abort, Api, reqparse from flask.ext.restful.utils.cors import crossdomain @@ -52,6 +53,11 @@ class InvalidRequest(ApiException): ApiException.__init__(self, 'invalid_request', 400, error_description, payload) +class InvalidResponse(ApiException): + def __init__(self, error_description, payload=None): + ApiException.__init__(self, 'invalid_response', 500, error_description, payload) + + class InvalidToken(ApiException): def __init__(self, error_description, payload=None): ApiException.__init__(self, 'invalid_token', 401, error_description, payload) @@ -286,6 +292,25 @@ def validate_json_request(schema_name): return wrapper +def define_json_response(schema_name): + def wrapper(func): + @add_method_metadata('response_schema', schema_name) + @wraps(func) + def wrapped(self, *args, **kwargs): + schema = self.schemas[schema_name] + try: + resp = func(self, *args, **kwargs) + + if app.config['TESTING']: + validate(resp, schema) + + return resp + except ValidationError as ex: + raise InvalidResponse(ex.message) + return wrapped + return wrapper + + def request_error(exception=None, **kwargs): data = kwargs.copy() message = 'Request error.' diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index 47181dc9f..db61a14b2 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -94,12 +94,18 @@ def swagger_route_data(include_internal=False, compact=False): new_operation = { 'method': method_name, - 'nickname': method_metadata(method, 'nickname') + 'nickname': method_metadata(method, 'nickname') or '(unnamed)' } if not compact: + response_type = 'void' + res_schema_name = method_metadata(method, 'response_schema') + if res_schema_name: + models[res_schema_name] = view_class.schemas[res_schema_name] + response_type = res_schema_name + new_operation.update({ - 'type': 'void', + 'type': response_type, 'summary': method.__doc__.strip() if method.__doc__ else '', 'parameters': parameters, }) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 8ba2c0507..3bafe40b4 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -8,7 +8,8 @@ from flask.ext.principal import identity_changed, AnonymousIdentity from app import app, billing as stripe, authentication from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, NotFound, require_user_admin, path_param, - InvalidToken, require_scope, format_date, hide_if, show_if, license_error) + InvalidToken, require_scope, format_date, hide_if, show_if, license_error, + define_json_response) from endpoints.api.subscribe import subscribe from endpoints.common import common_login from data import model @@ -50,13 +51,13 @@ def user_view(user): 'verified': user.verified, 'anonymous': False, 'username': user.username, - 'email': user.email, 'gravatar': compute_hash(user.email), } user_admin = UserAdminPermission(user.username) if user_admin.can(): user_response.update({ + 'email': user.email, 'organizations': [org_view(o) for o in organizations], 'logins': [login_view(login) for login in logins], 'can_create_repo': True, @@ -130,10 +131,51 @@ class User(ApiResource): }, }, }, + 'UserView': { + 'id': 'UserView', + 'type': 'object', + 'description': 'Describes a user', + 'required': ['verified', 'anonymous', 'gravatar'], + 'properties': { + 'verified': { + 'type': 'boolean', + 'description': 'Whether the user\'s email address has been verified' + }, + 'anonymous': { + 'type': 'boolean', + 'description': 'true if this user data represents a guest user' + }, + 'email': { + 'type': 'string', + 'description': 'The user\'s email address', + }, + 'gravatar': { + 'type': 'string', + 'description': 'Gravatar hash representing the user\'s icon' + }, + 'organizations': { + 'type': 'array', + 'description': 'Information about the organizations in which the user is a member' + }, + 'logins': { + 'type': 'array', + 'description': 'The list of external login providers against which the user has authenticated' + }, + 'can_create_repo': { + 'type': 'boolean', + 'description': 'Whether the user has permission to create repositories' + }, + 'preferred_namespace': { + 'type': 'boolean', + 'description': 'If true, the user\'s namespace is the preferred namespace to display' + } + } + }, } @require_scope(scopes.READ_USER) @nickname('getLoggedInUser') + @define_json_response('UserView') def get(self): """ Get user information for the authenticated user. """ user = get_authenticated_user() @@ -146,6 +188,7 @@ class User(ApiResource): @nickname('changeUserDetails') @internal_only @validate_json_request('UpdateUser') + @define_json_response('UserView') def put(self): """ Update a users details such as password or email. """ user = get_authenticated_user() From 58ca76239beb3a066c76aea566807ec09e88b736 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 17 Nov 2014 14:54:07 -0500 Subject: [PATCH 4/4] Add ability to one-click generate an authorization access token in the applications panel --- data/model/oauth.py | 8 +++++++- endpoints/common.py | 1 + endpoints/web.py | 3 +-- static/css/quay.css | 8 ++++++++ static/js/app.js | 6 ++++++ static/js/controllers.js | 18 +++++++++++++++++- static/partials/manage-application.html | 25 +++++++++++++++++++++++++ templates/base.html | 1 + 8 files changed, 66 insertions(+), 4 deletions(-) diff --git a/data/model/oauth.py b/data/model/oauth.py index 51bfc053e..f6bbed992 100644 --- a/data/model/oauth.py +++ b/data/model/oauth.py @@ -9,6 +9,7 @@ from data.database import (OAuthApplication, OAuthAuthorizationCode, OAuthAccess random_string_generator) from data.model.legacy import get_user from auth import scopes +from flask import render_template logger = logging.getLogger(__name__) @@ -154,6 +155,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider): def get_token_response(self, response_type, client_id, redirect_uri, **params): + # Ensure proper response_type if response_type != 'token': err = 'unsupported_response_type' @@ -161,7 +163,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider): # Check redirect URI is_valid_redirect_uri = self.validate_redirect_uri(client_id, redirect_uri) - if not is_valid_redirect_uri: + if redirect_uri != 'display' and not is_valid_redirect_uri: return self._invalid_redirect_uri_response() # Check conditions @@ -196,6 +198,10 @@ class DatabaseAuthorizationProvider(AuthorizationProvider): url = utils.build_url(redirect_uri, params) url += '#access_token=%s&token_type=%s&expires_in=%s' % (access_token, token_type, expires_in) + if redirect_uri == 'display': + return self._make_response( + render_template("message.html", message="Access Token: " + access_token)) + return self._make_response(headers={'Location': url}, status_code=302) diff --git a/endpoints/common.py b/endpoints/common.py index 6a221bd5c..5de16c034 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -198,6 +198,7 @@ def render_page_template(name, **kwargs): feature_set=json.dumps(features.get_features()), config_set=json.dumps(getFrontendVisibleConfig(app.config)), oauth_set=json.dumps(get_oauth_config()), + scope_set=json.dumps(scopes.ALL_SCOPES), mixpanel_key=app.config.get('MIXPANEL_KEY', ''), google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''), sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''), diff --git a/endpoints/web.py b/endpoints/web.py index b355f5ec9..b2fffd811 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -376,7 +376,7 @@ def request_authorization_code(): if (not current_user.is_authenticated() or not provider.validate_has_scopes(client_id, current_user.db_user().username, scope)): - if not provider.validate_redirect_uri(client_id, redirect_uri): + if redirect_uri != 'display' and not provider.validate_redirect_uri(client_id, redirect_uri): current_app = provider.get_application_for_client_id(client_id) if not current_app: abort(404) @@ -416,7 +416,6 @@ def request_authorization_code(): else: return provider.get_authorization_code(response_type, client_id, redirect_uri, scope=scope) - @web.route('/oauth/access_token', methods=['POST']) @no_cache @param_required('grant_type') diff --git a/static/css/quay.css b/static/css/quay.css index 25934010b..7e44e3a59 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4871,4 +4871,12 @@ i.slack-icon { #startTriggerDialog #runForm .field-title { width: 120px; padding-right: 10px; +} + +#gen-token table { + margin: 10px; +} + +#gen-token input[type="checkbox"] { + margin-right: 10px; } \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 7c00c5831..4351c6166 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1711,6 +1711,12 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading return notificationService; }]); + $provide.factory('OAuthService', ['$location', 'Config', function($location, Config) { + var oauthService = {}; + oauthService.SCOPES = window.__auth_scopes; + return oauthService; + }]); + $provide.factory('KeyService', ['$location', 'Config', function($location, Config) { var keyService = {} var oauth = window.__oauth; diff --git a/static/js/controllers.js b/static/js/controllers.js index 07041ae7a..62254984b 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -2676,12 +2676,28 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul } -function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, ApiService) { +function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, OAuthService, ApiService, UserService, Config) { var orgname = $routeParams.orgname; var clientId = $routeParams.clientid; + $scope.Config = Config; + $scope.OAuthService = OAuthService; $scope.updating = false; + $scope.genScopes = {}; + + UserService.updateUserIn($scope); + + $scope.getScopes = function(scopes) { + var checked = []; + for (var scopeName in scopes) { + if (scopes.hasOwnProperty(scopeName) && scopes[scopeName]) { + checked.push(scopeName); + } + } + return checked; + }; + $scope.askResetClientSecret = function() { $('#resetSecretModal').modal({}); }; diff --git a/static/partials/manage-application.html b/static/partials/manage-application.html index aaae745b8..7d0bb1bfb 100644 --- a/static/partials/manage-application.html +++ b/static/partials/manage-application.html @@ -30,6 +30,7 @@ @@ -91,6 +92,30 @@ + +
+
+ Click the button below to generate a new OAuth 2 Access Token. +
+ +
+ Note: The generated token will act on behalf of user + {{ user.username }} +
+ + + + + +
+ + + Generate Access Token + +
+
diff --git a/templates/base.html b/templates/base.html index 317a3683e..4824848fc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -45,6 +45,7 @@ window.__features = {{ feature_set|safe }}; window.__config = {{ config_set|safe }}; window.__oauth = {{ oauth_set|safe }}; + window.__auth_scopes = {{ scope_set|safe }}; window.__token = '{{ csrf_token() }}';