Auth controller
Endpoints added. OpenId support added. Memory storage added for tokens and authorization codes. Tbd in next commits: * Add a middleware for token validation Change-Id: I1805bc645428bc9301dc3447537fd9792afe781d
This commit is contained in:
parent
20aadc8701
commit
0bb5d06aa9
@ -33,6 +33,9 @@ lock_path = $state_path/lock
|
||||
# Port the bind the API server to
|
||||
# bind_port = 8080
|
||||
|
||||
# OpenId Authentication endpoint
|
||||
# openid_url = https://login.launchpad.net/+openid
|
||||
|
||||
[database]
|
||||
# This line MUST be changed to actually run storyboard
|
||||
# Example:
|
||||
|
@ -3,10 +3,12 @@ pbr>=0.5.21,<1.0
|
||||
alembic>=0.4.1
|
||||
Babel>=0.9.6
|
||||
iso8601>=0.1.8
|
||||
oauthlib>=0.6
|
||||
oslo.config>=1.2.0
|
||||
pecan>=0.2.0
|
||||
python-openid
|
||||
PyYAML>=3.1.0
|
||||
requests>=1.1
|
||||
six>=1.4.1
|
||||
SQLAlchemy>=0.8,<=0.8.99
|
||||
WSME>=0.5b6
|
||||
|
0
storyboard/api/auth/__init__.py
Normal file
0
storyboard/api/auth/__init__.py
Normal file
192
storyboard/api/auth/auth_controller.py
Normal file
192
storyboard/api/auth/auth_controller.py
Normal file
@ -0,0 +1,192 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from oauthlib.oauth2 import RequestValidator
|
||||
from oauthlib.oauth2 import WebApplicationServer
|
||||
from oslo.config import cfg
|
||||
|
||||
#Todo(nkonovalov): make storage configurable
|
||||
from storyboard.api.auth.memory_storage import MemoryTokenStorage
|
||||
from storyboard.db import api as db_api
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SkeletonValidator(RequestValidator):
|
||||
"""This is oauth skeleton for handling all kind of validations and storage
|
||||
manipulations.
|
||||
|
||||
As it is and OAuth2, not OpenId-connect, some methods are not required to
|
||||
be implemented.
|
||||
|
||||
Scope parameter validation is skipped as it is not a part of OpenId-connect
|
||||
protocol.
|
||||
|
||||
"""
|
||||
|
||||
def validate_client_id(self, client_id, request, *args, **kwargs):
|
||||
"""Check that a valid client is connecting
|
||||
|
||||
"""
|
||||
|
||||
# Let's think about valid clients later
|
||||
return True
|
||||
|
||||
def validate_redirect_uri(self, client_id, redirect_uri, request, *args,
|
||||
**kwargs):
|
||||
"""Check that the client is allowed to redirect using the given
|
||||
redirect_uri.
|
||||
|
||||
"""
|
||||
|
||||
#todo(nkonovalov): check an uri based on CONF.domain
|
||||
return True
|
||||
|
||||
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
|
||||
return request.sb_redirect_uri
|
||||
|
||||
def validate_scopes(self, client_id, scopes, client, request, *args,
|
||||
**kwargs):
|
||||
"""Scopes are not supported in OpenId-connect
|
||||
The "user" value is hardcoded here to fill the difference between
|
||||
the protocols.
|
||||
|
||||
"""
|
||||
|
||||
return scopes == "user"
|
||||
|
||||
def get_default_scopes(self, client_id, request, *args, **kwargs):
|
||||
"""Scopes a client will authorize for if none are supplied in the
|
||||
authorization request.
|
||||
|
||||
"""
|
||||
|
||||
return ["user"]
|
||||
|
||||
def validate_response_type(self, client_id, response_type, client, request,
|
||||
*args, **kwargs):
|
||||
"""Clients should only be allowed to use one type of response type, the
|
||||
one associated with their one allowed grant type.
|
||||
In this case it must be "code".
|
||||
|
||||
"""
|
||||
|
||||
return response_type == "code"
|
||||
|
||||
# Post-authorization
|
||||
|
||||
def save_authorization_code(self, client_id, code, request, *args,
|
||||
**kwargs):
|
||||
"""Save the code to the storage and remove the state as it is persisted
|
||||
in the "code" argument
|
||||
"""
|
||||
|
||||
openid = request._params["openid.claimed_id"]
|
||||
email = request._params["openid.sreg.email"]
|
||||
fullname = request._params["openid.sreg.fullname"]
|
||||
|
||||
user = db_api.user_get_by_openid(openid)
|
||||
|
||||
if not user:
|
||||
user = db_api.user_create({"openid": openid,
|
||||
"fullname": fullname,
|
||||
"email": email})
|
||||
else:
|
||||
user = db_api.user_update(user.id, {"fullname": fullname,
|
||||
"email": email})
|
||||
|
||||
TOKEN_STORAGE.save_authorization_code(code, user_id=user.id)
|
||||
|
||||
# Token request
|
||||
|
||||
def authenticate_client(self, request, *args, **kwargs):
|
||||
"""Skip the authentication here. It is handled through an OpenId client
|
||||
The parameters are set to match th OAuth protocol.
|
||||
|
||||
"""
|
||||
|
||||
setattr(request, "client", type("Object", (object,), {})())
|
||||
setattr(request.client, "client_id", "1")
|
||||
return True
|
||||
|
||||
def validate_code(self, client_id, code, client, request, *args, **kwargs):
|
||||
"""Validate the code belongs to the client."""
|
||||
|
||||
return TOKEN_STORAGE.check_authorization_code(code)
|
||||
|
||||
def confirm_redirect_uri(self, client_id, code, redirect_uri, client,
|
||||
*args, **kwargs):
|
||||
"""Check that the client is allowed to redirect using the given
|
||||
redirect_uri.
|
||||
|
||||
"""
|
||||
|
||||
#todo(nkonovalov): check an uri based on CONF.domain
|
||||
return True
|
||||
|
||||
def validate_grant_type(self, client_id, grant_type, client, request,
|
||||
*args, **kwargs):
|
||||
"""Clients should only be allowed to use one type of grant.
|
||||
In this case, it must be "authorization_code" or "refresh_token"
|
||||
|
||||
"""
|
||||
|
||||
return (grant_type == "authorization_code"
|
||||
or grant_type == "refresh_token")
|
||||
|
||||
def save_bearer_token(self, token, request, *args, **kwargs):
|
||||
"""Save all token information to the storage."""
|
||||
|
||||
code = request._params["code"]
|
||||
code_info = TOKEN_STORAGE.get_authorization_code_info(code)
|
||||
user_id = code_info.user_id
|
||||
|
||||
TOKEN_STORAGE.save_token(access_token=token["access_token"],
|
||||
expires_in=token["expires_in"],
|
||||
refresh_token=token["refresh_token"],
|
||||
user_id=user_id)
|
||||
|
||||
def invalidate_authorization_code(self, client_id, code, request, *args,
|
||||
**kwargs):
|
||||
"""Authorization codes are use once, invalidate it when a token has
|
||||
been acquired.
|
||||
|
||||
"""
|
||||
|
||||
TOKEN_STORAGE.invalidate_authorization_code(code)
|
||||
|
||||
# Protected resource request
|
||||
|
||||
def validate_bearer_token(self, token, scopes, request):
|
||||
"""The check will be performed in a separate middleware."""
|
||||
|
||||
pass
|
||||
|
||||
# Token refresh request
|
||||
|
||||
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
|
||||
"""Scopes a client will authorize for if none are supplied in the
|
||||
authorization request.
|
||||
|
||||
"""
|
||||
return ["user"]
|
||||
|
||||
|
||||
validator = SkeletonValidator()
|
||||
SERVER = WebApplicationServer(validator)
|
||||
|
||||
TOKEN_STORAGE = MemoryTokenStorage()
|
91
storyboard/api/auth/memory_storage.py
Normal file
91
storyboard/api/auth/memory_storage.py
Normal file
@ -0,0 +1,91 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
|
||||
from storyboard.api.auth import storage
|
||||
|
||||
|
||||
class Token(object):
|
||||
def __init__(self, access_token, refresh_token, expires_in, user_id):
|
||||
self.access_token = access_token
|
||||
self.refresh_token = refresh_token
|
||||
self.expires_in = expires_in
|
||||
self.expires_at = datetime.datetime.now() + \
|
||||
datetime.timedelta(seconds=expires_in)
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
class AuthorizationCode(object):
|
||||
def __init__(self, code, user_id):
|
||||
self.code = code
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
class MemoryTokenStorage(storage.StorageBase):
|
||||
|
||||
def __init__(self):
|
||||
self.token_set = set([])
|
||||
self.auth_code_set = set([])
|
||||
self.state_set = set([])
|
||||
|
||||
def save_token(self, access_token, expires_in, refresh_token, user_id):
|
||||
token_info = Token(access_token=access_token,
|
||||
expires_in=expires_in,
|
||||
refresh_token=refresh_token,
|
||||
user_id=user_id)
|
||||
|
||||
self.token_set.add(token_info)
|
||||
|
||||
def save_authorization_code(self, authorization_code, user_id):
|
||||
self.auth_code_set.add(AuthorizationCode(authorization_code, user_id))
|
||||
|
||||
def check_authorization_code(self, code):
|
||||
code_entry = None
|
||||
for entry in self.auth_code_set:
|
||||
if entry.code["code"] == code:
|
||||
code_entry = entry
|
||||
break
|
||||
|
||||
if not code_entry:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_authorization_code_info(self, code):
|
||||
for entry in self.auth_code_set:
|
||||
if entry.code["code"] == code:
|
||||
return entry
|
||||
|
||||
return None
|
||||
|
||||
def invalidate_authorization_code(self, code):
|
||||
code_entry = None
|
||||
for entry in self.auth_code_set:
|
||||
if entry.code["code"] == code:
|
||||
code_entry = entry
|
||||
break
|
||||
|
||||
self.auth_code_set.remove(code_entry)
|
||||
|
||||
def save_state(self, state):
|
||||
self.state_set.add(state)
|
||||
|
||||
def check_remove_state(self, state):
|
||||
if state in self.state_set:
|
||||
self.state_set.remove(state)
|
||||
return True
|
||||
|
||||
return False
|
117
storyboard/api/auth/openid_client.py
Normal file
117
storyboard/api/auth/openid_client.py
Normal file
@ -0,0 +1,117 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from oslo.config import cfg
|
||||
import requests
|
||||
|
||||
from storyboard.api.auth import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
OPENID_OPTS = [
|
||||
cfg.StrOpt('openid_url',
|
||||
default='https://login.launchpad.net/+openid',
|
||||
help='OpenId Authentication endpoint')
|
||||
]
|
||||
|
||||
CONF.register_opts(OPENID_OPTS)
|
||||
|
||||
|
||||
class OpenIdClient(object):
|
||||
|
||||
def send_openid_redirect(self, request, response):
|
||||
redirect_location = CONF.openid_url
|
||||
response.status_code = 303
|
||||
|
||||
return_params = {
|
||||
"scope": str(request.params.get("scope")),
|
||||
"state": str(request.params.get("state")),
|
||||
"client_id": str(request.params.get("client_id")),
|
||||
"response_type": str(request.params.get("response_type")),
|
||||
"sb_redirect_uri": str(request.params.get("redirect_uri"))
|
||||
}
|
||||
|
||||
#TODO(krotscheck): URI base should be fully inferred from the request.
|
||||
# assuming that the API is hosted at /api isn't good.
|
||||
return_to_url = request.host_url + "/api/v1/openid/authorize_return?" \
|
||||
+ utils.join_params(return_params, encode=True)
|
||||
|
||||
openid_params = {
|
||||
"openid.ns": "http://specs.openid.net/auth/2.0",
|
||||
"openid.mode": "checkid_setup",
|
||||
|
||||
"openid.claimed_id": "http://specs.openid.net/auth/2.0/"
|
||||
"identifier_select",
|
||||
"openid.identity": "http://specs.openid.net/auth/2.0/"
|
||||
"identifier_select",
|
||||
|
||||
"openid.realm": request.host_url,
|
||||
"openid.return_to": return_to_url,
|
||||
|
||||
"openid.ns.sreg": "http://openid.net/sreg/1.0",
|
||||
"openid.sreg.required": "fullname,email",
|
||||
|
||||
"openid.ns.ext2": "http://openid.net/srv/ax/1.0",
|
||||
"openid.ext2.mode": "fetch_request",
|
||||
"openid.ext2.type.FirstName": "http://schema.openid.net/"
|
||||
"namePerson/first",
|
||||
"openid.ext2.type.LastName": "http://schema.openid.net/"
|
||||
"namePerson/last",
|
||||
"openid.ext2.type.Email": "http://schema.openid.net/contact/email",
|
||||
"openid.ext2.required": "FirstName,LastName,Email"
|
||||
}
|
||||
joined_params = utils.join_params(openid_params)
|
||||
|
||||
redirect_location = redirect_location + '?' + joined_params
|
||||
response.headers["Location"] = redirect_location
|
||||
|
||||
return response
|
||||
|
||||
def verify_openid(self, request, response):
|
||||
verify_params = dict(request.params.copy())
|
||||
verify_params["openid.mode"] = "check_authentication"
|
||||
|
||||
verify_response = requests.post(CONF.openid_url, data=verify_params)
|
||||
verify_data_tokens = verify_response.content.split()
|
||||
verify_dict = dict((token.split(":")[0], token.split(":")[1])
|
||||
for token in verify_data_tokens)
|
||||
|
||||
if (verify_response.status_code / 100 != 2
|
||||
or verify_dict['is_valid'] != 'true'):
|
||||
response.status_code = 401 # Unauthorized
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_association(self, op_location):
|
||||
# Let's skip it for MVP at least
|
||||
query_dict = {
|
||||
"openid.ns": "http://specs.openid.net/auth/2.0",
|
||||
"openid.mode": "associate",
|
||||
"openid.assoc_type": "HMAC-SHA256",
|
||||
"openid.session_type": "no-encryption"
|
||||
}
|
||||
assoc_data = requests.post(op_location, data=query_dict).content
|
||||
|
||||
data_tokens = assoc_data.split()
|
||||
data_dict = dict((token.split(":")[0], token.split(":")[1])
|
||||
for token in data_tokens)
|
||||
|
||||
return data_dict["assoc_handle"]
|
||||
|
||||
client = OpenIdClient()
|
51
storyboard/api/auth/storage.py
Normal file
51
storyboard/api/auth/storage.py
Normal file
@ -0,0 +1,51 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class StorageBase(object):
|
||||
|
||||
@abc.abstractmethod
|
||||
def save_state(self, state):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def check_remove_state(self, state):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def save_authorization_code(self, authorization_code, user_id):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def check_authorization_code(self, code):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_authorization_code_info(self, code):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def invalidate_authorization_code(self, code):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def save_token(self, access_token, expires_in, refresh_token, user_id):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove_token(self, token):
|
||||
pass
|
24
storyboard/api/auth/utils.py
Normal file
24
storyboard/api/auth/utils.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import six
|
||||
import urllib
|
||||
|
||||
|
||||
def join_params(params, encode=True):
|
||||
return '&'.join(
|
||||
["%s=%s" % (urllib.quote(key, safe='') if encode else key,
|
||||
urllib.quote(val, safe='') if encode else val)
|
||||
for key, val in six.iteritems(params)])
|
119
storyboard/api/v1/auth.py
Normal file
119
storyboard/api/v1/auth.py
Normal file
@ -0,0 +1,119 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import six
|
||||
|
||||
from oslo.config import cfg
|
||||
import pecan
|
||||
from pecan import request
|
||||
from pecan import response
|
||||
from pecan import rest
|
||||
|
||||
from storyboard.api.auth.auth_controller import SERVER
|
||||
from storyboard.api.auth.auth_controller import TOKEN_STORAGE
|
||||
from storyboard.api.auth.openid_client import client as openid_client
|
||||
from storyboard.api.auth import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
AUTH_OPTS = [
|
||||
]
|
||||
CONF.register_opts(AUTH_OPTS)
|
||||
|
||||
|
||||
class AuthController(rest.RestController):
|
||||
_custom_actions = {
|
||||
"authorize": ["GET"],
|
||||
"authorize_return": ["GET"],
|
||||
"token": ["POST"],
|
||||
}
|
||||
|
||||
@pecan.expose()
|
||||
def authorize(self):
|
||||
"""Authorization code request."""
|
||||
|
||||
return openid_client.send_openid_redirect(request, response)
|
||||
|
||||
@pecan.expose()
|
||||
def authorize_return(self):
|
||||
"""Authorization code redirect endpoint.
|
||||
At this point the server verifies an OpenId and retrieves user's
|
||||
e-mail and full name from request
|
||||
|
||||
The client may already use both the e-mail and the fullname in the
|
||||
templates, even though there was no token request so far.
|
||||
|
||||
"""
|
||||
|
||||
if not openid_client.verify_openid(request, response):
|
||||
# The verify call will set unauthorized code
|
||||
return response
|
||||
|
||||
headers, body, code = SERVER.create_authorization_response(
|
||||
uri=request.url,
|
||||
http_method=request.method,
|
||||
body=request.body,
|
||||
scopes=request.params.get("scope"),
|
||||
headers=request.headers)
|
||||
|
||||
response.headers = dict((str(k), str(v))
|
||||
for k, v in headers.iteritems())
|
||||
response.status_code = code
|
||||
response.body = body or ''
|
||||
|
||||
return response
|
||||
|
||||
@pecan.expose()
|
||||
def client_redirect(self):
|
||||
"""This method is required to perform a hash-bang url redirect."""
|
||||
|
||||
response.status_code = 303
|
||||
|
||||
params = dict((str(k), str(v))
|
||||
for k, v in six.iteritems(request.params))
|
||||
joined_params = utils.join_params(params)
|
||||
response.headers["Location"] = "".join([request.host_url, "/",
|
||||
CONF.auth_token_url, "?",
|
||||
joined_params])
|
||||
|
||||
return response
|
||||
|
||||
@pecan.expose()
|
||||
def token(self):
|
||||
"""Access token endpoint."""
|
||||
|
||||
auth_code = request.params.get("code")
|
||||
code_info = TOKEN_STORAGE.get_authorization_code_info(auth_code)
|
||||
|
||||
headers, body, code = SERVER.create_token_response(
|
||||
uri=request.url,
|
||||
http_method=request.method,
|
||||
body=request.body,
|
||||
headers=request.headers)
|
||||
|
||||
response.headers = dict((str(k), str(v))
|
||||
for k, v in headers.iteritems())
|
||||
response.status_code = code
|
||||
|
||||
json_body = json.loads(body)
|
||||
json_body.update({
|
||||
'id_token': code_info.user_id
|
||||
})
|
||||
|
||||
response.body = json.dumps(json_body)
|
||||
return response
|
@ -29,15 +29,15 @@ class UsersController(rest.RestController):
|
||||
users = wsme_models.User.get_all()
|
||||
return users
|
||||
|
||||
@wsme_pecan.wsexpose(wsme_models.User, unicode)
|
||||
def get_one(self, username):
|
||||
@wsme_pecan.wsexpose(wsme_models.User, int)
|
||||
def get_one(self, user_id):
|
||||
"""Retrieve details about one user.
|
||||
|
||||
:param username: unique name to identify the user.
|
||||
:param user_id: The unique id of this user
|
||||
"""
|
||||
user = wsme_models.User.get(username=username)
|
||||
user = wsme_models.User.get(id=user_id)
|
||||
if not user:
|
||||
raise ClientSideError("User %s not found" % username,
|
||||
raise ClientSideError("User %s not found" % user_id,
|
||||
status_code=404)
|
||||
return user
|
||||
|
||||
@ -52,14 +52,14 @@ class UsersController(rest.RestController):
|
||||
raise ClientSideError("Could not create User")
|
||||
return created_user
|
||||
|
||||
@wsme_pecan.wsexpose(wsme_models.User, unicode, wsme_models.User)
|
||||
def put(self, username, user):
|
||||
@wsme_pecan.wsexpose(wsme_models.User, int, wsme_models.User)
|
||||
def put(self, user_id, user):
|
||||
"""Modify this user.
|
||||
|
||||
:param username: unique name to identify the user.
|
||||
:param user_id: unique id to identify the user.
|
||||
:param user: a user within the request body.
|
||||
"""
|
||||
updated_user = wsme_models.User.update("username", username, user)
|
||||
updated_user = wsme_models.User.update("id", user_id, user)
|
||||
if not updated_user:
|
||||
raise ClientSideError("Could not update user %s" % username)
|
||||
raise ClientSideError("Could not update user %s" % user_id)
|
||||
return updated_user
|
||||
|
@ -13,6 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from storyboard.api.v1.auth import AuthController
|
||||
from storyboard.api.v1.project_groups import ProjectGroupsController
|
||||
from storyboard.api.v1.projects import ProjectsController
|
||||
from storyboard.api.v1.stories import StoriesController
|
||||
@ -29,3 +30,5 @@ class V1Controller(object):
|
||||
users = UsersController()
|
||||
stories = StoriesController()
|
||||
tasks = TasksController()
|
||||
|
||||
openid = AuthController()
|
||||
|
@ -82,7 +82,46 @@ def _entity_update(kls, entity_id, values):
|
||||
return entity
|
||||
|
||||
|
||||
# BEGIN Projects
|
||||
## BEGIN Users
|
||||
|
||||
def user_get(user_id):
|
||||
return _entity_get(models.User, user_id)
|
||||
|
||||
|
||||
def user_get_by_openid(openid):
|
||||
query = model_query(models.User, get_session())
|
||||
return query.filter_by(openid=openid).first()
|
||||
|
||||
|
||||
def user_create(values):
|
||||
user = models.User()
|
||||
user.update(values.copy())
|
||||
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
try:
|
||||
user.save(session=session)
|
||||
except db_exc.DBDuplicateEntry as e:
|
||||
raise exc.DuplicateEntry("Duplicate entry for User: %s"
|
||||
% e.columns)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def user_update(user_id, values):
|
||||
session = get_session()
|
||||
|
||||
with session.begin():
|
||||
user = _entity_get(models.User, user_id)
|
||||
if user is None:
|
||||
raise exc.NotFound("User %s not found" % user_id)
|
||||
|
||||
user.update(values.copy())
|
||||
|
||||
return user
|
||||
|
||||
|
||||
## BEGIN Projects
|
||||
|
||||
def project_get(project_id):
|
||||
return _entity_get(models.Project, project_id)
|
||||
|
@ -532,12 +532,7 @@ def _setup_logging_from_conf():
|
||||
else:
|
||||
handler.setFormatter(ContextFormatter(datefmt=datefmt))
|
||||
|
||||
if CONF.debug:
|
||||
log_root.setLevel(logging.DEBUG)
|
||||
elif CONF.verbose:
|
||||
log_root.setLevel(logging.INFO)
|
||||
else:
|
||||
log_root.setLevel(logging.WARNING)
|
||||
log_root.setLevel(logging.DEBUG)
|
||||
|
||||
for pair in CONF.default_log_levels:
|
||||
mod, _sep, level_name = pair.partition('=')
|
||||
|
Loading…
Reference in New Issue
Block a user