456 lines
14 KiB
Python
456 lines
14 KiB
Python
import logging
|
|
import json
|
|
|
|
from flask import request
|
|
from flask.ext.login import logout_user
|
|
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,
|
|
InvalidToken, require_scope, format_date, hide_if, show_if)
|
|
from endpoints.api.subscribe import subscribe
|
|
from endpoints.common import common_login
|
|
from data import model
|
|
from data.billing import get_plan
|
|
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
|
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
|
from auth.auth_context import get_authenticated_user
|
|
from auth import scopes
|
|
from util.gravatar import compute_hash
|
|
from util.email import (send_confirmation_email, send_recovery_email,
|
|
send_change_email)
|
|
|
|
import features
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def user_view(user):
|
|
def org_view(o):
|
|
admin_org = AdministerOrganizationPermission(o.username)
|
|
return {
|
|
'name': o.username,
|
|
'gravatar': compute_hash(o.email),
|
|
'is_org_admin': admin_org.can(),
|
|
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
|
|
'preferred_namespace': not (o.stripe_id is None)
|
|
}
|
|
|
|
organizations = model.get_user_organizations(user.username)
|
|
|
|
def login_view(login):
|
|
return {
|
|
'service': login.service.name,
|
|
'service_identifier': login.service_ident,
|
|
}
|
|
|
|
logins = model.list_federated_logins(user)
|
|
|
|
user_response = {
|
|
'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({
|
|
'organizations': [org_view(o) for o in organizations],
|
|
'logins': [login_view(login) for login in logins],
|
|
'can_create_repo': True,
|
|
'invoice_email': user.invoice_email,
|
|
'preferred_namespace': not (user.stripe_id is None),
|
|
})
|
|
|
|
if features.SUPER_USERS:
|
|
user_response.update({
|
|
'super_user': user and user == get_authenticated_user() and SuperUserPermission().can()
|
|
})
|
|
|
|
return user_response
|
|
|
|
|
|
def notification_view(notification):
|
|
return {
|
|
'organization': notification.target.username if notification.target.organization else None,
|
|
'kind': notification.kind.name,
|
|
'created': format_date(notification.created),
|
|
'metadata': json.loads(notification.metadata_json),
|
|
}
|
|
|
|
|
|
@resource('/v1/user/')
|
|
class User(ApiResource):
|
|
""" Operations related to users. """
|
|
schemas = {
|
|
'NewUser': {
|
|
'id': 'NewUser',
|
|
'type': 'object',
|
|
'description': 'Fields which must be specified for a new user.',
|
|
'required': [
|
|
'username',
|
|
'password',
|
|
'email',
|
|
],
|
|
'properties': {
|
|
'username': {
|
|
'type': 'string',
|
|
'description': 'The user\'s username',
|
|
},
|
|
'password': {
|
|
'type': 'string',
|
|
'description': 'The user\'s password',
|
|
},
|
|
'email': {
|
|
'type': 'string',
|
|
'description': 'The user\'s email address',
|
|
},
|
|
}
|
|
},
|
|
'UpdateUser': {
|
|
'id': 'UpdateUser',
|
|
'type': 'object',
|
|
'description': 'Fields which can be updated in a user.',
|
|
'properties': {
|
|
'password': {
|
|
'type': 'string',
|
|
'description': 'The user\'s password',
|
|
},
|
|
'invoice_email': {
|
|
'type': 'boolean',
|
|
'description': 'Whether the user desires to receive an invoice email.',
|
|
},
|
|
'email': {
|
|
'type': 'string',
|
|
'description': 'The user\'s email address',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
@require_scope(scopes.READ_USER)
|
|
@nickname('getLoggedInUser')
|
|
def get(self):
|
|
""" Get user information for the authenticated user. """
|
|
user = get_authenticated_user()
|
|
if user is None or user.organization or not UserReadPermission(user.username).can():
|
|
raise InvalidToken("Requires authentication", payload={'session_required': False})
|
|
|
|
return user_view(user)
|
|
|
|
@require_user_admin
|
|
@nickname('changeUserDetails')
|
|
@internal_only
|
|
@validate_json_request('UpdateUser')
|
|
def put(self):
|
|
""" Update a users details such as password or email. """
|
|
user = get_authenticated_user()
|
|
user_data = request.get_json()
|
|
|
|
try:
|
|
if 'password' in user_data:
|
|
logger.debug('Changing password for user: %s', user.username)
|
|
log_action('account_change_password', user.username)
|
|
model.change_password(user, user_data['password'])
|
|
|
|
if 'invoice_email' in user_data:
|
|
logger.debug('Changing invoice_email for user: %s', user.username)
|
|
model.change_invoice_email(user, user_data['invoice_email'])
|
|
|
|
if 'email' in user_data and user_data['email'] != user.email:
|
|
new_email = user_data['email']
|
|
if model.find_user_by_email(new_email):
|
|
# Email already used.
|
|
raise request_error(message='E-mail address already used')
|
|
|
|
logger.debug('Sending email to change email address for user: %s',
|
|
user.username)
|
|
code = model.create_confirm_email_code(user, new_email=new_email)
|
|
send_change_email(user.username, user_data['email'], code.code)
|
|
|
|
except model.InvalidPasswordException, ex:
|
|
raise request_error(exception=ex)
|
|
|
|
return user_view(user)
|
|
|
|
@nickname('createNewUser')
|
|
@internal_only
|
|
@validate_json_request('NewUser')
|
|
def post(self):
|
|
""" Create a new user. """
|
|
user_data = request.get_json()
|
|
|
|
existing_user = model.get_user(user_data['username'])
|
|
if existing_user:
|
|
raise request_error(message='The username already exists')
|
|
|
|
try:
|
|
new_user = model.create_user(user_data['username'], user_data['password'],
|
|
user_data['email'])
|
|
code = model.create_confirm_email_code(new_user)
|
|
send_confirmation_email(new_user.username, new_user.email, code.code)
|
|
return 'Created', 201
|
|
except model.DataModelException as ex:
|
|
raise request_error(exception=ex)
|
|
|
|
@resource('/v1/user/private')
|
|
@internal_only
|
|
@show_if(features.BILLING)
|
|
class PrivateRepositories(ApiResource):
|
|
""" Operations dealing with the available count of private repositories. """
|
|
@require_user_admin
|
|
@nickname('getUserPrivateAllowed')
|
|
def get(self):
|
|
""" Get the number of private repos this user has, and whether they are allowed to create more.
|
|
"""
|
|
user = get_authenticated_user()
|
|
private_repos = model.get_private_repo_count(user.username)
|
|
repos_allowed = 0
|
|
|
|
if user.stripe_id:
|
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
|
if cus.subscription:
|
|
plan = get_plan(cus.subscription.plan.id)
|
|
if plan:
|
|
repos_allowed = plan['privateRepos']
|
|
|
|
return {
|
|
'privateCount': private_repos,
|
|
'privateAllowed': (private_repos < repos_allowed)
|
|
}
|
|
|
|
|
|
def conduct_signin(username_or_email, password):
|
|
needs_email_verification = False
|
|
invalid_credentials = False
|
|
|
|
verified = authentication.verify_user(username_or_email, password)
|
|
if verified:
|
|
if common_login(verified):
|
|
return {'success': True}
|
|
else:
|
|
needs_email_verification = True
|
|
|
|
else:
|
|
invalid_credentials = True
|
|
|
|
return {
|
|
'needsEmailVerification': needs_email_verification,
|
|
'invalidCredentials': invalid_credentials,
|
|
}, 403
|
|
|
|
|
|
@resource('/v1/user/convert')
|
|
@internal_only
|
|
class ConvertToOrganization(ApiResource):
|
|
""" Operations for converting a user to an organization. """
|
|
schemas = {
|
|
'ConvertUser': {
|
|
'id': 'ConvertUser',
|
|
'type': 'object',
|
|
'description': 'Information required to convert a user to an organization.',
|
|
'required': [
|
|
'adminUser',
|
|
'adminPassword'
|
|
],
|
|
'properties': {
|
|
'adminUser': {
|
|
'type': 'string',
|
|
'description': 'The user who will become an org admin\'s username',
|
|
},
|
|
'adminPassword': {
|
|
'type': 'string',
|
|
'description': 'The user who will become an org admin\'s password',
|
|
},
|
|
'plan': {
|
|
'type': 'string',
|
|
'description': 'The plan to which the organization should be subscribed',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
@require_user_admin
|
|
@nickname('convertUserToOrganization')
|
|
@validate_json_request('ConvertUser')
|
|
def post(self):
|
|
""" Convert the user to an organization. """
|
|
user = get_authenticated_user()
|
|
convert_data = request.get_json()
|
|
|
|
# Ensure that the new admin user is the not user being converted.
|
|
admin_username = convert_data['adminUser']
|
|
if admin_username == user.username:
|
|
raise request_error(reason='invaliduser',
|
|
message='The admin user is not valid')
|
|
|
|
# Ensure that the sign in credentials work.
|
|
admin_password = convert_data['adminPassword']
|
|
if not authentication.verify_user(admin_username, admin_password):
|
|
raise request_error(reason='invaliduser',
|
|
message='The admin user credentials are not valid')
|
|
|
|
# Subscribe the organization to the new plan.
|
|
if features.BILLING:
|
|
plan = convert_data.get('plan', 'free')
|
|
subscribe(user, plan, None, True) # Require business plans
|
|
|
|
# Convert the user to an organization.
|
|
model.convert_user_to_organization(user, model.get_user(admin_username))
|
|
log_action('account_convert', user.username)
|
|
|
|
# And finally login with the admin credentials.
|
|
return conduct_signin(admin_username, admin_password)
|
|
|
|
|
|
@resource('/v1/signin')
|
|
@internal_only
|
|
class Signin(ApiResource):
|
|
""" Operations for signing in the user. """
|
|
schemas = {
|
|
'SigninUser': {
|
|
'id': 'SigninUser',
|
|
'type': 'object',
|
|
'description': 'Information required to sign in a user.',
|
|
'required': [
|
|
'username',
|
|
'password',
|
|
],
|
|
'properties': {
|
|
'username': {
|
|
'type': 'string',
|
|
'description': 'The user\'s username',
|
|
},
|
|
'password': {
|
|
'type': 'string',
|
|
'description': 'The user\'s password',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
@nickname('signinUser')
|
|
@validate_json_request('SigninUser')
|
|
def post(self):
|
|
""" Sign in the user with the specified credentials. """
|
|
signin_data = request.get_json()
|
|
if not signin_data:
|
|
raise NotFound()
|
|
|
|
username = signin_data['username']
|
|
password = signin_data['password']
|
|
|
|
return conduct_signin(username, password)
|
|
|
|
|
|
@resource('/v1/signout')
|
|
@internal_only
|
|
class Signout(ApiResource):
|
|
""" Resource for signing out users. """
|
|
@nickname('logout')
|
|
def post(self):
|
|
""" Request that the current user be signed out. """
|
|
logout_user()
|
|
identity_changed.send(app, identity=AnonymousIdentity())
|
|
return {'success': True}
|
|
|
|
|
|
@resource("/v1/recovery")
|
|
@internal_only
|
|
class Recovery(ApiResource):
|
|
""" Resource for requesting a password recovery email. """
|
|
schemas = {
|
|
'RequestRecovery': {
|
|
'id': 'RequestRecovery',
|
|
'type': 'object',
|
|
'description': 'Information required to sign in a user.',
|
|
'required': [
|
|
'email',
|
|
],
|
|
'properties': {
|
|
'email': {
|
|
'type': 'string',
|
|
'description': 'The user\'s email address',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
@nickname('requestRecoveryEmail')
|
|
@validate_json_request('RequestRecovery')
|
|
def post(self):
|
|
""" Request a password recovery email."""
|
|
email = request.get_json()['email']
|
|
code = model.create_reset_password_email_code(email)
|
|
send_recovery_email(email, code.code)
|
|
return 'Created', 201
|
|
|
|
|
|
@resource('/v1/user/notifications')
|
|
@internal_only
|
|
class UserNotificationList(ApiResource):
|
|
@require_user_admin
|
|
@nickname('listUserNotifications')
|
|
def get(self):
|
|
notifications = model.list_notifications(get_authenticated_user())
|
|
return {
|
|
'notifications': [notification_view(notification) for notification in notifications]
|
|
}
|
|
|
|
|
|
def authorization_view(access_token):
|
|
oauth_app = access_token.application
|
|
return {
|
|
'application': {
|
|
'name': oauth_app.name,
|
|
'description': oauth_app.description,
|
|
'url': oauth_app.application_uri,
|
|
'gravatar': compute_hash(oauth_app.gravatar_email or oauth_app.organization.email),
|
|
'organization': {
|
|
'name': oauth_app.organization.username,
|
|
'gravatar': compute_hash(oauth_app.organization.email)
|
|
}
|
|
},
|
|
'scopes': scopes.get_scope_information(access_token.scope),
|
|
'uuid': access_token.uuid
|
|
}
|
|
|
|
@resource('/v1/user/authorizations')
|
|
@internal_only
|
|
class UserAuthorizationList(ApiResource):
|
|
@require_user_admin
|
|
@nickname('listUserAuthorizations')
|
|
def get(self):
|
|
access_tokens = model.oauth.list_access_tokens_for_user(get_authenticated_user())
|
|
|
|
return {
|
|
'authorizations': [authorization_view(token) for token in access_tokens]
|
|
}
|
|
|
|
|
|
@resource('/v1/user/authorizations/<access_token_uuid>')
|
|
@internal_only
|
|
class UserAuthorization(ApiResource):
|
|
@require_user_admin
|
|
@nickname('getUserAuthorization')
|
|
def get(self, access_token_uuid):
|
|
access_token = model.oauth.lookup_access_token_for_user(get_authenticated_user(),
|
|
access_token_uuid)
|
|
if not access_token:
|
|
raise NotFound()
|
|
|
|
return authorization_view(access_token)
|
|
|
|
@require_user_admin
|
|
@nickname('deleteUserAuthorization')
|
|
def delete(self, access_token_uuid):
|
|
access_token = model.oauth.lookup_access_token_for_user(get_authenticated_user(),
|
|
access_token_uuid)
|
|
if not access_token:
|
|
raise NotFound()
|
|
|
|
access_token.delete_instance(recursive=True, delete_nullable=True)
|
|
return 'Deleted', 204
|