Merge branch 'master' of https://bitbucket.org/yackob03/quay
This commit is contained in:
commit
ce7620673b
13 changed files with 197 additions and 68 deletions
10
app.py
10
app.py
|
@ -3,15 +3,19 @@ import logging
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask.ext.principal import Principal
|
from flask.ext.principal import Principal
|
||||||
from flask.ext.login import LoginManager
|
from flask.ext.login import LoginManager
|
||||||
|
from flask.ext.mail import Mail
|
||||||
|
from config import ProductionConfig
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(ProductionConfig())
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
Principal(app, use_sessions=True)
|
Principal(app, use_sessions=True)
|
||||||
|
|
||||||
app.secret_key = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
|
|
||||||
|
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
login_manager.login_view = 'signin'
|
login_manager.login_view = 'signin'
|
||||||
|
|
||||||
|
mail = Mail()
|
||||||
|
mail.init_app(app)
|
||||||
|
|
17
config.py
Normal file
17
config.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
class FlaskConfig(object):
|
||||||
|
SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
|
||||||
|
|
||||||
|
|
||||||
|
class MailConfig(object):
|
||||||
|
MAIL_SERVER = 'email-smtp.us-east-1.amazonaws.com'
|
||||||
|
MAIL_USE_TLS = True
|
||||||
|
MAIL_PORT = 587
|
||||||
|
MAIL_USERNAME = 'AKIAIXV5SDGCPVMU3N4Q'
|
||||||
|
MAIL_PASSWORD = 'AhmX/vWE91uQ2RtcEKTkfNrzZehEjPNXOXeOXgQNfLao'
|
||||||
|
DEFAULT_MAIL_SENDER = 'support@fluxmonkey.io'
|
||||||
|
MAIL_FAIL_SILENTLY = False
|
||||||
|
TESTING = False
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionConfig(FlaskConfig, MailConfig):
|
||||||
|
pass
|
|
@ -15,16 +15,14 @@ class BaseModel(Model):
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
username = CharField(unique=True)
|
username = CharField(unique=True, index=True)
|
||||||
password_hash = CharField()
|
password_hash = CharField()
|
||||||
email = CharField(unique=True)
|
email = CharField(unique=True, index=True)
|
||||||
|
verified = BooleanField(default=False)
|
||||||
# TODO move this to False and require email verification
|
|
||||||
verified = BooleanField(default=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Visibility(BaseModel):
|
class Visibility(BaseModel):
|
||||||
name = CharField()
|
name = CharField(index=True)
|
||||||
|
|
||||||
|
|
||||||
class Repository(BaseModel):
|
class Repository(BaseModel):
|
||||||
|
@ -42,14 +40,20 @@ class Repository(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Role(BaseModel):
|
class Role(BaseModel):
|
||||||
name = CharField()
|
name = CharField(index=True)
|
||||||
|
|
||||||
|
|
||||||
class RepositoryPermission(BaseModel):
|
class RepositoryPermission(BaseModel):
|
||||||
user = ForeignKeyField(User)
|
user = ForeignKeyField(User, index=True)
|
||||||
repository = ForeignKeyField(Repository)
|
repository = ForeignKeyField(Repository, index=True)
|
||||||
role = ForeignKeyField(Role)
|
role = ForeignKeyField(Role)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
indexes = (
|
||||||
|
(('user', 'repository'), True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def random_string_generator(length=16):
|
def random_string_generator(length=16):
|
||||||
def random_string():
|
def random_string():
|
||||||
|
@ -60,12 +64,20 @@ def random_string_generator(length=16):
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(BaseModel):
|
class AccessToken(BaseModel):
|
||||||
code = CharField(default=random_string_generator(), unique=True)
|
code = CharField(default=random_string_generator(), unique=True, index=True)
|
||||||
user = ForeignKeyField(User)
|
user = ForeignKeyField(User)
|
||||||
repository = ForeignKeyField(Repository)
|
repository = ForeignKeyField(Repository)
|
||||||
created = DateTimeField(default=datetime.now)
|
created = DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailConfirmation(BaseModel):
|
||||||
|
code = CharField(default=random_string_generator(), unique=True, index=True)
|
||||||
|
user = ForeignKeyField(User)
|
||||||
|
pw_reset = BooleanField(default=False)
|
||||||
|
email_confirm = BooleanField(default=False)
|
||||||
|
created = DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
class Image(BaseModel):
|
class Image(BaseModel):
|
||||||
# This class is intentionally denormalized. Even though images are supposed
|
# This class is intentionally denormalized. Even though images are supposed
|
||||||
# to be globally unique we can't treat them as such for permissions and
|
# to be globally unique we can't treat them as such for permissions and
|
||||||
|
@ -90,10 +102,17 @@ class RepositoryTag(BaseModel):
|
||||||
image = ForeignKeyField(Image)
|
image = ForeignKeyField(Image)
|
||||||
repository = ForeignKeyField(Repository)
|
repository = ForeignKeyField(Repository)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
indexes = (
|
||||||
|
(('repository', 'name'), True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def initialize_db():
|
def initialize_db():
|
||||||
create_model_tables([User, Repository, Image, AccessToken, Role,
|
create_model_tables([User, Repository, Image, AccessToken, Role,
|
||||||
RepositoryPermission, Visibility, RepositoryTag])
|
RepositoryPermission, Visibility, RepositoryTag,
|
||||||
|
EmailConfirmation])
|
||||||
Role.create(name='admin')
|
Role.create(name='admin')
|
||||||
Role.create(name='write')
|
Role.create(name='write')
|
||||||
Role.create(name='read')
|
Role.create(name='read')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import logging
|
import logging
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
|
import operator
|
||||||
|
|
||||||
from database import *
|
from database import *
|
||||||
from util.validation import (validate_email, validate_username,
|
from util.validation import (validate_email, validate_username,
|
||||||
|
@ -28,9 +29,27 @@ def create_user(username, password, email):
|
||||||
try:
|
try:
|
||||||
new_user = User.create(username=username, password_hash=pw_hash,
|
new_user = User.create(username=username, password_hash=pw_hash,
|
||||||
email=email)
|
email=email)
|
||||||
|
return new_user
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise DataModelException(ex.message)
|
raise DataModelException(ex.message)
|
||||||
return new_user
|
|
||||||
|
|
||||||
|
def create_confirm_email_code(user):
|
||||||
|
code = EmailConfirmation.create(user=user, email_confirm=True)
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_user_email(code):
|
||||||
|
code = EmailConfirmation.get(EmailConfirmation.code == code,
|
||||||
|
EmailConfirmation.email_confirm == True)
|
||||||
|
|
||||||
|
user = code.user
|
||||||
|
user.verified = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
code.delete_instance()
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
def get_user(username):
|
def get_user(username):
|
||||||
|
@ -41,7 +60,9 @@ def get_user(username):
|
||||||
|
|
||||||
|
|
||||||
def get_matching_users(username_prefix):
|
def get_matching_users(username_prefix):
|
||||||
return list(User.select().where(User.username ** (username_prefix + '%')).limit(10))
|
query = User.select().where(User.username ** (username_prefix + '%'))
|
||||||
|
return list(query.limit(10))
|
||||||
|
|
||||||
|
|
||||||
def verify_user(username, password):
|
def verify_user(username, password):
|
||||||
try:
|
try:
|
||||||
|
@ -75,8 +96,28 @@ def get_token(code):
|
||||||
return AccessToken.get(AccessToken.code == code)
|
return AccessToken.get(AccessToken.code == code)
|
||||||
|
|
||||||
|
|
||||||
def get_matching_repositories(repo_term):
|
def get_visible_repositories(username=None):
|
||||||
return list(Repository.select().where(Repository.name ** ('%' + repo_term + '%') | Repository.namespace ** ('%' + repo_term + '%') | Repository.description ** ('%' + repo_term + '%')).limit(10))
|
query = Repository.select().distinct().join(Visibility)
|
||||||
|
or_clauses = [(Visibility.name == 'public')]
|
||||||
|
|
||||||
|
if username:
|
||||||
|
with_perms = query.switch(Repository).join(RepositoryPermission,
|
||||||
|
JOIN_LEFT_OUTER)
|
||||||
|
query = with_perms.join(User)
|
||||||
|
or_clauses.append(User.username == username)
|
||||||
|
|
||||||
|
return query.where(reduce(operator.or_, or_clauses))
|
||||||
|
|
||||||
|
|
||||||
|
def get_matching_repositories(repo_term, username=None):
|
||||||
|
visible = get_visible_repositories(username)
|
||||||
|
search_clauses = (Repository.name ** ('%' + repo_term + '%') |
|
||||||
|
Repository.namespace ** ('%' + repo_term + '%') |
|
||||||
|
Repository.description ** ('%' + repo_term + '%'))
|
||||||
|
|
||||||
|
|
||||||
|
final = visible.where(search_clauses).limit(10)
|
||||||
|
return list(final)
|
||||||
|
|
||||||
|
|
||||||
def change_password(user, new_password):
|
def change_password(user, new_password):
|
||||||
|
@ -108,20 +149,20 @@ def get_all_repo_users(namespace_name, repository_name):
|
||||||
Repository.name == repository_name)
|
Repository.name == repository_name)
|
||||||
|
|
||||||
|
|
||||||
def get_repository(namespace, name):
|
def get_repository(namespace_name, repository_name):
|
||||||
try:
|
try:
|
||||||
return Repository.get(Repository.name == name,
|
return Repository.get(Repository.name == repository_name,
|
||||||
Repository.namespace == namespace)
|
Repository.namespace == namespace_name)
|
||||||
except Repository.DoesNotExist:
|
except Repository.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_user_repositories(user):
|
def repository_is_public(namespace_name, repository_name):
|
||||||
select = RepositoryPermission.select(RepositoryPermission, Repository, Role)
|
joined = Repository.select().join(Visibility)
|
||||||
with_user = select.join(User).where(User.username == user.username)
|
query = joined.where(Repository.namespace == namespace_name,
|
||||||
with_role = with_user.switch(RepositoryPermission).join(Role)
|
Repository.name == repository_name,
|
||||||
with_repo = with_role.switch(RepositoryPermission).join(Repository)
|
Visibility.name == 'public')
|
||||||
return with_repo
|
return len(list(query)) > 0
|
||||||
|
|
||||||
|
|
||||||
def create_repository(namespace, name, owner):
|
def create_repository(namespace, name, owner):
|
||||||
|
@ -151,8 +192,8 @@ def set_image_metadata(image_id, namespace_name, repository_name,
|
||||||
created_date_str, comment):
|
created_date_str, comment):
|
||||||
joined = Image.select().join(Repository)
|
joined = Image.select().join(Repository)
|
||||||
image_list = list(joined.where(Repository.name == repository_name,
|
image_list = list(joined.where(Repository.name == repository_name,
|
||||||
Repository.namespace == namespace_name,
|
Repository.namespace == namespace_name,
|
||||||
Image.image_id == image_id))
|
Image.image_id == image_id))
|
||||||
|
|
||||||
if not image_list:
|
if not image_list:
|
||||||
raise DataModelException('No image with specified id and repository')
|
raise DataModelException('No image with specified id and repository')
|
||||||
|
@ -169,6 +210,7 @@ def get_repository_images(namespace_name, repository_name):
|
||||||
return joined.where(Repository.name == repository_name,
|
return joined.where(Repository.name == repository_name,
|
||||||
Repository.namespace == namespace_name)
|
Repository.namespace == namespace_name)
|
||||||
|
|
||||||
|
|
||||||
def get_tag_images(namespace_name, repository_name, tag_name):
|
def get_tag_images(namespace_name, repository_name, tag_name):
|
||||||
joined = Image.select().join(RepositoryTag).join(Repository)
|
joined = Image.select().join(RepositoryTag).join(Repository)
|
||||||
fetched = list(joined.where(Repository.name == repository_name,
|
fetched = list(joined.where(Repository.name == repository_name,
|
||||||
|
@ -177,6 +219,7 @@ def get_tag_images(namespace_name, repository_name, tag_name):
|
||||||
|
|
||||||
return fetched
|
return fetched
|
||||||
|
|
||||||
|
|
||||||
def list_repository_tags(namespace_name, repository_name):
|
def list_repository_tags(namespace_name, repository_name):
|
||||||
select = RepositoryTag.select(RepositoryTag, Image)
|
select = RepositoryTag.select(RepositoryTag, Image)
|
||||||
with_repo = select.join(Repository)
|
with_repo = select.join(Repository)
|
||||||
|
@ -272,6 +315,7 @@ def set_user_repo_permission(username, namespace_name, repository_name,
|
||||||
role=new_role)
|
role=new_role)
|
||||||
return new_perm
|
return new_perm
|
||||||
|
|
||||||
|
|
||||||
def delete_user_permission(username, namespace_name, repository_name):
|
def delete_user_permission(username, namespace_name, repository_name):
|
||||||
if username == namespace_name:
|
if username == namespace_name:
|
||||||
raise DataModelException('Namespace owner must always be admin.')
|
raise DataModelException('Namespace owner must always be admin.')
|
||||||
|
|
|
@ -64,10 +64,10 @@ def match_repos_api(prefix):
|
||||||
'description': repo.description,
|
'description': repo.description,
|
||||||
}
|
}
|
||||||
|
|
||||||
repos = [repo_view(repo) for repo in model.get_matching_repositories(prefix) if
|
username = current_user.db_user.username
|
||||||
ReadRepositoryPermission(repo.namespace, repo.name).can()]
|
matching = model.get_matching_repositories(prefix, username)
|
||||||
response = {
|
response = {
|
||||||
'repositories': repos
|
'repositories': [repo_view(repo) for repo in matching]
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
@ -76,16 +76,16 @@ def match_repos_api(prefix):
|
||||||
@app.route('/api/repository/', methods=['GET'])
|
@app.route('/api/repository/', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def list_repos_api():
|
def list_repos_api():
|
||||||
def repo_view(repo_perm):
|
def repo_view(repo_obj):
|
||||||
return {
|
return {
|
||||||
'namespace': repo_perm.repository.namespace,
|
'namespace': repo_obj.namespace,
|
||||||
'name': repo_perm.repository.name,
|
'name': repo_obj.name,
|
||||||
'role': repo_perm.role.name,
|
'description': repo_obj.description,
|
||||||
'description': repo_perm.repository.description,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
username = current_user.db_user.username
|
||||||
repos = [repo_view(repo)
|
repos = [repo_view(repo)
|
||||||
for repo in model.get_user_repositories(current_user.db_user)]
|
for repo in model.get_visible_repositories(username)]
|
||||||
response = {
|
response = {
|
||||||
'repositories': repos
|
'repositories': repos
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,7 @@ def get_repo_api(namespace, repository):
|
||||||
}
|
}
|
||||||
|
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
if permission.can() or model.repository_is_public(namespace, repository):
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
if repo:
|
if repo:
|
||||||
tags = model.list_repository_tags(namespace, repository)
|
tags = model.list_repository_tags(namespace, repository)
|
||||||
|
@ -162,12 +162,13 @@ def role_view(repo_perm_obj):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/<path:repository>/tag/<tag>/images', methods=['GET'])
|
@app.route('/api/repository/<path:repository>/tag/<tag>/images',
|
||||||
|
methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def list_tag_images(namespace, repository, tag):
|
def list_tag_images(namespace, repository, tag):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
if permission.can() or model.repository_is_public(namespace, repository):
|
||||||
images = model.get_tag_images(namespace, repository, tag)
|
images = model.get_tag_images(namespace, repository, tag)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
@ -234,6 +235,7 @@ def change_permissions(namespace, repository, username):
|
||||||
|
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/<path:repository>/permissions/<username>',
|
@app.route('/api/repository/<path:repository>/permissions/<username>',
|
||||||
methods=['DELETE'])
|
methods=['DELETE'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|
|
@ -11,6 +11,7 @@ from app import app
|
||||||
from auth.auth import (process_auth, get_authenticated_user,
|
from auth.auth import (process_auth, get_authenticated_user,
|
||||||
get_validated_token)
|
get_validated_token)
|
||||||
from util.names import parse_namespace_repository, parse_repository_name
|
from util.names import parse_namespace_repository, parse_repository_name
|
||||||
|
from util.email import send_confirmation_email
|
||||||
from auth.permissions import (ModifyRepositoryPermission,
|
from auth.permissions import (ModifyRepositoryPermission,
|
||||||
ReadRepositoryPermission, UserPermission)
|
ReadRepositoryPermission, UserPermission)
|
||||||
|
|
||||||
|
@ -46,8 +47,10 @@ def generate_headers(f):
|
||||||
@app.route('/v1/users/', methods=['POST'])
|
@app.route('/v1/users/', methods=['POST'])
|
||||||
def create_user():
|
def create_user():
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
model.create_user(user_data['username'], user_data['password'],
|
new_user = model.create_user(user_data['username'], user_data['password'],
|
||||||
user_data['email'])
|
user_data['email'])
|
||||||
|
code = model.create_confirm_email_code(new_user)
|
||||||
|
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||||
return make_response('Created', 201)
|
return make_response('Created', 201)
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,7 +157,7 @@ def get_repository_images(namespace, repository):
|
||||||
|
|
||||||
# TODO invalidate token?
|
# TODO invalidate token?
|
||||||
|
|
||||||
if permission.can():
|
if permission.can() or model.repository_is_public(namespace, repository):
|
||||||
all_images = []
|
all_images = []
|
||||||
for image in model.get_repository_images(namespace, repository):
|
for image in model.get_repository_images(namespace, repository):
|
||||||
new_image_view = {
|
new_image_view = {
|
||||||
|
|
|
@ -80,14 +80,14 @@ def set_cache_headers(f):
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
def get_image_layer(namespace, repository, image_id, headers):
|
def get_image_layer(namespace, repository, image_id, headers):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
if not permission.can():
|
if permission.can() or model.repository_is_public(namespace, repository):
|
||||||
abort(403)
|
try:
|
||||||
|
return Response(store.stream_read(store.image_layer_path(
|
||||||
|
namespace, repository, image_id)), headers=headers)
|
||||||
|
except IOError:
|
||||||
|
abort(404) #'Image not found', 404)
|
||||||
|
|
||||||
try:
|
abort(403)
|
||||||
return Response(store.stream_read(store.image_layer_path(
|
|
||||||
namespace, repository, image_id)), headers=headers)
|
|
||||||
except IOError:
|
|
||||||
abort(404) #'Image not found', 404)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/v1/images/<image_id>/layer', methods=['PUT'])
|
@app.route('/v1/images/<image_id>/layer', methods=['PUT'])
|
||||||
|
@ -182,7 +182,8 @@ def put_image_checksum(namespace, repository, image_id):
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
def get_image_json(namespace, repository, image_id, headers):
|
def get_image_json(namespace, repository, image_id, headers):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
if not permission.can():
|
if (not permission.can() and not
|
||||||
|
model.repository_is_public(namespace, repository)):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -211,7 +212,8 @@ def get_image_json(namespace, repository, image_id, headers):
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
def get_image_ancestry(namespace, repository, image_id, headers):
|
def get_image_ancestry(namespace, repository, image_id, headers):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
if not permission.can():
|
if (not permission.can() and not
|
||||||
|
model.repository_is_public(namespace, repository)):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
||||||
def get_tags(namespace, repository):
|
def get_tags(namespace, repository):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
if permission.can():
|
if permission.can() or model.repository_is_public(namespace, repository):
|
||||||
tags = model.list_repository_tags(namespace, repository)
|
tags = model.list_repository_tags(namespace, repository)
|
||||||
tag_map = {tag.name: tag.image.image_id for tag in tags}
|
tag_map = {tag.name: tag.image.image_id for tag in tags}
|
||||||
return jsonify(tag_map)
|
return jsonify(tag_map)
|
||||||
|
@ -40,7 +40,7 @@ def get_tags(namespace, repository):
|
||||||
def get_tag(namespace, repository, tag):
|
def get_tag(namespace, repository, tag):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
if permission.can():
|
if permission.can() or model.repository_is_public(namespace, repository):
|
||||||
tag_image = model.get_tag_image(namespace, repository, tag)
|
tag_image = model.get_tag_image(namespace, repository, tag)
|
||||||
response = make_response(tag_image.image_id, 200)
|
response = make_response(tag_image.image_id, 200)
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,12 @@ def index():
|
||||||
return send_file('templates/index.html')
|
return send_file('templates/index.html')
|
||||||
|
|
||||||
|
|
||||||
|
def common_login(db_user):
|
||||||
|
logger.debug('Successfully signed in as: %s' % db_user.username)
|
||||||
|
login_user(_LoginWrappedDBUser(db_user))
|
||||||
|
identity_changed.send(app, identity=Identity(db_user.username, 'username'))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/signin', methods=['POST'])
|
@app.route('/signin', methods=['POST'])
|
||||||
def signin():
|
def signin():
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
|
@ -44,18 +50,28 @@ def signin():
|
||||||
#TODO Allow email login
|
#TODO Allow email login
|
||||||
verified = model.verify_user(username, password)
|
verified = model.verify_user(username, password)
|
||||||
if verified:
|
if verified:
|
||||||
logger.debug('Successfully signed in as: %s' % username)
|
common_login(verified)
|
||||||
|
|
||||||
login_user(_LoginWrappedDBUser(verified))
|
|
||||||
|
|
||||||
identity_changed.send(app, identity=Identity(verified.username,
|
|
||||||
'username'))
|
|
||||||
|
|
||||||
return redirect(request.args.get('next') or url_for('index'))
|
return redirect(request.args.get('next') or url_for('index'))
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/confirm', methods=['GET'])
|
||||||
|
def confirm_email():
|
||||||
|
code = request.values['code']
|
||||||
|
user = model.confirm_user_email(code)
|
||||||
|
|
||||||
|
common_login(user)
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/reset', methods=['GET'])
|
||||||
|
def password_reset():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@app.route('/signin', methods=['GET'])
|
@app.route('/signin', methods=['GET'])
|
||||||
def render_signin_page():
|
def render_signin_page():
|
||||||
return send_file('templates/signin.html')
|
return send_file('templates/signin.html')
|
||||||
|
|
|
@ -3,3 +3,4 @@ flask
|
||||||
py-bcrypt
|
py-bcrypt
|
||||||
Flask-Principal
|
Flask-Principal
|
||||||
Flask-Login
|
Flask-Login
|
||||||
|
Flask-Mail
|
BIN
test.db
BIN
test.db
Binary file not shown.
20
util/email.py
Normal file
20
util/email.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from flask.ext.mail import Message
|
||||||
|
|
||||||
|
from app import mail, app
|
||||||
|
|
||||||
|
|
||||||
|
CONFIRM_MESSAGE = """
|
||||||
|
This email address was recently used to register the username '%s'
|
||||||
|
at <a href="http://quay.io">Quay.io</a>.<br>
|
||||||
|
<br>
|
||||||
|
To confirm this email address, please click the following link:<br>
|
||||||
|
<a href="http://quay.io/confirm?code=%s">http://quay.io/confirm?code=%s</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def send_confirmation_email(username, email, token):
|
||||||
|
msg = Message('Welcome to Quay! Please confirm your email.',
|
||||||
|
sender='support@fluxmonkey.io', # Why do I need this?
|
||||||
|
recipients=[email])
|
||||||
|
msg.html = CONFIRM_MESSAGE % (username, token, token)
|
||||||
|
mail.send(msg)
|
|
@ -14,6 +14,7 @@ def validate_username(username):
|
||||||
len(username) > 1 and
|
len(username) > 1 and
|
||||||
len(username) < 256)
|
len(username) < 256)
|
||||||
|
|
||||||
|
|
||||||
def validate_password(password):
|
def validate_password(password):
|
||||||
# No whitespace and minimum length of 8
|
# No whitespace and minimum length of 8
|
||||||
if re.search(r'\s', password):
|
if re.search(r'\s', password):
|
||||||
|
|
Reference in a new issue