Add some login machinery.
This commit is contained in:
parent
366907b08d
commit
e107d79612
10 changed files with 172 additions and 11 deletions
57
api.py
Normal file
57
api.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import request, make_response, jsonify, abort
|
||||||
|
from flask.ext.login import login_required, current_user
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
import model
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
from util import parse_repository_name
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/')
|
||||||
|
def welcome():
|
||||||
|
return make_response('welcome', 200)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def create_repo_api():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def list_repos_api():
|
||||||
|
def repo_view(repo_perm):
|
||||||
|
return {
|
||||||
|
'namespace': repo_perm.repository.namespace,
|
||||||
|
'name': repo_perm.repository.name,
|
||||||
|
'role': repo_perm.role.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
repos = [repo_view(repo)
|
||||||
|
for repo in model.get_user_repositories(current_user)]
|
||||||
|
response = {
|
||||||
|
'repositories': repos
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/<path:repository>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
@parse_repository_name
|
||||||
|
def update_repo_api(namespace, repository):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/<path:repository>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
@parse_repository_name
|
||||||
|
def get_repo_api(namespace, repository):
|
||||||
|
pass
|
47
app.py
47
app.py
|
@ -1,10 +1,55 @@
|
||||||
from flask import Flask, make_response, jsonify
|
import logging
|
||||||
|
|
||||||
|
from flask import (Flask, make_response, request, abort, render_template,
|
||||||
|
redirect, url_for)
|
||||||
from flask.ext.principal import Principal
|
from flask.ext.principal import Principal
|
||||||
|
from flask.ext.login import login_user, LoginManager
|
||||||
|
|
||||||
|
import model
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
Principal(app, use_sessions=False)
|
Principal(app, use_sessions=False)
|
||||||
|
|
||||||
|
app.secret_key = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
|
||||||
|
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = 'signin'
|
||||||
|
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(username):
|
||||||
|
return model.get_user(username)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET'])
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/signin', methods=['POST'])
|
||||||
|
def signin():
|
||||||
|
username = request.form['username']
|
||||||
|
password = request.form['password']
|
||||||
|
|
||||||
|
#TODO Allow email login
|
||||||
|
verified = model.verify_user(username, password)
|
||||||
|
if verified:
|
||||||
|
logger.debug('Successfully signed in as: %s' % username)
|
||||||
|
|
||||||
|
login_user(verified)
|
||||||
|
return redirect(request.args.get('next') or url_for('index'))
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/signin', methods=['GET'])
|
||||||
|
def render_signin_page():
|
||||||
|
return render_template('signin.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/_ping')
|
@app.route('/_ping')
|
||||||
@app.route('/v1/_ping')
|
@app.route('/v1/_ping')
|
||||||
def ping():
|
def ping():
|
||||||
|
|
12
database.py
12
database.py
|
@ -20,6 +20,18 @@ class User(BaseModel):
|
||||||
email = CharField(unique=True)
|
email = CharField(unique=True)
|
||||||
verified = BooleanField(default=False)
|
verified = BooleanField(default=False)
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return self.verified
|
||||||
|
|
||||||
|
def is_authenticated(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_anonymous(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return unicode(self.username)
|
||||||
|
|
||||||
|
|
||||||
class Visibility(BaseModel):
|
class Visibility(BaseModel):
|
||||||
name = CharField()
|
name = CharField()
|
||||||
|
|
11
index.py
11
index.py
|
@ -8,26 +8,19 @@ from functools import wraps
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from auth import process_auth, get_authenticated_user, get_validated_token
|
from auth import process_auth, get_authenticated_user, get_validated_token
|
||||||
from util import parse_namespace_repository
|
from util import parse_namespace_repository, parse_repository_name
|
||||||
from permissions import (ModifyRepositoryPermission, ReadRepositoryPermission,
|
from permissions import (ModifyRepositoryPermission, ReadRepositoryPermission,
|
||||||
UserPermission)
|
UserPermission)
|
||||||
|
|
||||||
import model
|
import model
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
REGISTRY_SERVER = 'localhost:5003'
|
REGISTRY_SERVER = 'localhost:5003'
|
||||||
|
|
||||||
|
|
||||||
def parse_repository_name(f):
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(repository, *args, **kwargs):
|
|
||||||
(namespace, repository) = parse_namespace_repository(repository)
|
|
||||||
return f(namespace, repository, *args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def generate_headers(access):
|
def generate_headers(access):
|
||||||
def add_headers(f):
|
def add_headers(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
|
|
15
model.py
15
model.py
|
@ -15,6 +15,13 @@ def create_user(username, password, email):
|
||||||
return new_user
|
return new_user
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(username):
|
||||||
|
try:
|
||||||
|
return User.get(User.username == username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def verify_user(username, password):
|
def verify_user(username, password):
|
||||||
try:
|
try:
|
||||||
fetched = User.get(User.username == username)
|
fetched = User.get(User.username == username)
|
||||||
|
@ -61,6 +68,14 @@ def get_repository(namespace, name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_repositories(user):
|
||||||
|
select = RepositoryPermission.select(RepositoryPermission, Repository, Role)
|
||||||
|
with_user = select.join(User).where(User.username == user.username)
|
||||||
|
with_role = with_user.switch(RepositoryPermission).join(Role)
|
||||||
|
with_repo = with_role.switch(RepositoryPermission).join(Repository)
|
||||||
|
return with_repo
|
||||||
|
|
||||||
|
|
||||||
def create_repository(namespace, name, owner):
|
def create_repository(namespace, name, owner):
|
||||||
private = Visibility.get(name='private')
|
private = Visibility.get(name='private')
|
||||||
repo = Repository.create(namespace=namespace, name=name,
|
repo = Repository.create(namespace=namespace, name=name,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
peewee
|
peewee
|
||||||
flask
|
flask
|
||||||
py-bcrypt
|
py-bcrypt
|
||||||
Flask-Principal
|
Flask-Principal
|
||||||
|
Flask-Login
|
15
templates/index.html
Normal file
15
templates/index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Quay - Private Docker Repository</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<!-- Latest compiled and minified CSS -->
|
||||||
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
|
||||||
|
|
||||||
|
<!-- Optional theme -->
|
||||||
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
templates/signin.html
Normal file
12
templates/signin.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Quay Sign In</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form method="post">
|
||||||
|
Username: <input type="text" name="username"><br>
|
||||||
|
Password: <input type="password" name="password"><br>
|
||||||
|
<input type="submit" value="Sign In">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
util.py
10
util.py
|
@ -1,5 +1,7 @@
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
def parse_namespace_repository(repository):
|
def parse_namespace_repository(repository):
|
||||||
parts = repository.rstrip('/').split('/', 1)
|
parts = repository.rstrip('/').split('/', 1)
|
||||||
|
@ -10,3 +12,11 @@ def parse_namespace_repository(repository):
|
||||||
(namespace, repository) = parts
|
(namespace, repository) = parts
|
||||||
repository = urllib.quote_plus(repository)
|
repository = urllib.quote_plus(repository)
|
||||||
return (namespace, repository)
|
return (namespace, repository)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_repository_name(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(repository, *args, **kwargs):
|
||||||
|
(namespace, repository) = parse_namespace_repository(repository)
|
||||||
|
return f(namespace, repository, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
1
wsgi.py
1
wsgi.py
|
@ -3,6 +3,7 @@ import logging
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
import index
|
import index
|
||||||
|
import api
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
FORMAT = '%(asctime)-15s - %(levelname)s - %(pathname)s - ' + \
|
FORMAT = '%(asctime)-15s - %(levelname)s - %(pathname)s - ' + \
|
||||||
|
|
Reference in a new issue