Browse Source

Add authentication through openstackid.org

In Refstack's database store only fullname, email and openid.
After sign in refstack backend create session and write it id in cookie.
When UI is opened in browser, Angular try to get info from
/v1/profile. If data about user received then user is authenticated.

Change-Id: Ib2cabc0c6b4de4b2ca1f02cc9e062a6e3550daa0
changes/35/176335/24
Vladislav Kuzmin 6 years ago
committed by sslypushenko
parent
commit
7b48c99fcb
47 changed files with 1253 additions and 166 deletions
  1. +0
    -1
      docker/Dockerfile
  2. +20
    -16
      docker/nginx/refstack-site.conf
  3. +1
    -1
      docker/scripts/api-up
  4. +2
    -1
      docker/scripts/start.sh
  5. +5
    -1
      docker/templates/refstack.conf.tmpl
  6. +79
    -18
      etc/refstack.conf.sample
  7. +23
    -0
      refstack-ui/app/app.js
  8. +29
    -0
      refstack-ui/app/components/auth/authController.js
  9. +4
    -0
      refstack-ui/app/components/profile/profile.html
  10. +21
    -0
      refstack-ui/app/components/profile/profileController.js
  11. +2
    -0
      refstack-ui/app/index.html
  12. +5
    -0
      refstack-ui/app/shared/header/header.html
  13. +45
    -24
      refstack-ui/tests/unit/ControllerSpec.js
  14. +8
    -2
      refstack-ui/tests/unit/FilterSpec.js
  15. +15
    -0
      refstack/__init__.py
  16. +15
    -0
      refstack/api/__init__.py
  17. +34
    -10
      refstack/api/app.py
  18. +18
    -3
      refstack/api/constants.py
  19. +15
    -0
      refstack/api/controllers/__init__.py
  20. +170
    -0
      refstack/api/controllers/auth.py
  21. +5
    -0
      refstack/api/controllers/root.py
  22. +39
    -0
      refstack/api/controllers/user.py
  23. +29
    -18
      refstack/api/controllers/v1.py
  24. +96
    -9
      refstack/api/utils.py
  25. +15
    -0
      refstack/common/__init__.py
  26. +15
    -10
      refstack/common/validators.py
  27. +1
    -3
      refstack/db/__init__.py
  28. +22
    -6
      refstack/db/api.py
  29. +15
    -0
      refstack/db/migrations/__init__.py
  30. +15
    -0
      refstack/db/migrations/alembic/__init__.py
  31. +4
    -3
      refstack/db/migrations/alembic/env.py
  32. +1
    -3
      refstack/db/migrations/alembic/migration.py
  33. +38
    -0
      refstack/db/migrations/alembic/versions/2f178b0bf762_create_user_table.py
  34. +3
    -1
      refstack/db/migrations/alembic/versions/42278d6179b9_init.py
  35. +15
    -0
      refstack/db/sqlalchemy/__init__.py
  36. +40
    -7
      refstack/db/sqlalchemy/api.py
  37. +13
    -3
      refstack/db/sqlalchemy/models.py
  38. +4
    -0
      refstack/db/utils.py
  39. +26
    -19
      refstack/opts.py
  40. +15
    -0
      refstack/tests/__init__.py
  41. +1
    -1
      refstack/tests/api/__init__.py
  42. +15
    -0
      refstack/tests/unit/__init__.py
  43. +162
    -4
      refstack/tests/unit/test_api.py
  44. +74
    -0
      refstack/tests/unit/test_api_utils.py
  45. +21
    -2
      refstack/tests/unit/test_app.py
  46. +57
    -0
      refstack/tests/unit/test_db.py
  47. +1
    -0
      requirements.txt

+ 0
- 1
docker/Dockerfile View File

@ -21,7 +21,6 @@ RUN \
RUN \
pip install virtualenv tox ipython ipdb httpie && \
npm install -g bower && \
ln -s /usr/bin/nodejs /usr/bin/node
ADD /docker/scripts/* /usr/bin/


+ 20
- 16
docker/nginx/refstack-site.conf View File

@ -1,19 +1,23 @@
server {
server_name 127.0.0.1;
listen 443 ssl;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
server_name 127.0.0.1;
listen 443 ssl;
ssl on;
ssl_certificate /etc/nginx/certificates/refstack_dev.crt;
ssl_certificate_key /etc/nginx/certificates/refstack_dev.key;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AES:RSA+3DES:!ADH:!AECDH:!MD5:!DSS:!RC4;
ssl_prefer_server_ciphers on;
ssl_session_timeout 5m;
location / {
access_log off;
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
ssl on;
ssl_certificate /etc/nginx/certificates/refstack_dev.crt;
ssl_certificate_key /etc/nginx/certificates/refstack_dev.key;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AES:RSA+3DES:!ADH:!AECDH:!MD5:!DSS:!RC4;
ssl_prefer_server_ciphers on;
ssl_session_timeout 5m;
location / {
access_log off;
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

+ 1
- 1
docker/scripts/api-up View File

@ -2,4 +2,4 @@
[[ ${DEBUG_MODE} ]] && set -x
api-sync
cd /home/dev/refstack
.venv/bin/pecan serve refstack/api/config.py > /dev/null
.venv/bin/pecan serve refstack/api/config.py

+ 2
- 1
docker/scripts/start.sh View File

@ -41,7 +41,8 @@ build_refstack_env () {
#Install some dev tools
.venv/bin/pip install ipython ipdb httpie
cd /home/dev/refstack
bower install --config.interactive=false
npm install
# bower install --config.interactive=false
build_tmpl /refstack/docker/templates/config.json.tmpl /home/dev/refstack/refstack-ui/app/config.json
build_tmpl /refstack/docker/templates/refstack.conf.tmpl /home/dev/refstack.conf


+ 5
- 1
docker/templates/refstack.conf.tmpl View File

@ -1,12 +1,16 @@
[DEFAULT]
debug = true
verbose = true
ui_url = https://${REFSTACK_HOST:-127.0.0.1}
[api]
static_root = /home/dev/refstack/refstack-ui/app
template_path = /home/dev/refstack/refstack-ui/app
app_dev_mode = true
test_results_url = https://${REFSTACK_HOST:-127.0.0.1}/#/results/%s
api_url = https://${REFSTACK_HOST:-127.0.0.1}
[database]
connection = ${REFSTACK_MYSQL_URL}
[osid]
openstack_openid_endpoint = https://172.17.42.1:8443/accounts/openid2

+ 79
- 18
etc/refstack.conf.sample View File

@ -40,14 +40,16 @@
#log_dir = <None>
# Use syslog for logging. Existing syslog format is DEPRECATED during
# I, and will change in J to honor RFC5424. (boolean value)
# I, and changed in J to honor RFC5424. (boolean value)
#use_syslog = false
# (Optional) Enables or disables syslog rfc5424 format for logging. If
# enabled, prefixes the MSG part of the syslog message with APP-NAME
# (RFC5424). The format without the APP-NAME is deprecated in I, and
# will be removed in J. (boolean value)
#use_syslog_rfc_format = false
# (RFC5424). The format without the APP-NAME is deprecated in K, and
# will be removed in M, along with this option. (boolean value)
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
#use_syslog_rfc_format = true
# Syslog facility to receive log lines. (string value)
#syslog_log_facility = LOG_USER
@ -67,7 +69,7 @@
# Prefix each line of exception output with this format. (string
# value)
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s
# List of logger=LEVEL pairs. (list value)
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN
@ -86,10 +88,17 @@
# (string value)
#instance_uuid_format = "[instance: %(uuid)s] "
# Enables or disables fatal status of deprecations. (boolean value)
#fatal_deprecations = false
#
# From refstack
#
# Url of user interface for Refstack. Need for redirects after sign in
# and sign out. (string value)
#ui_url = http://refstack.net
# The backend to use for database. (string value)
#db_backend = sqlalchemy
@ -100,6 +109,9 @@
# From refstack
#
# Url of public Refstack API. (string value)
#api_url = http://api.refstack.net
# The directory where your static files can be found. Pecan comes with
# middleware that can be used to serve static files (like CSS and
# Javascript files) during development. %(project_root)s is special
@ -116,9 +128,9 @@
# relative the project root. (string value)
#template_path = %(project_root)s/templates
# List of sites allowed cross-origin resource access. If this is empty,
# only same-origin requests are allowed.
#allowed_cors_origins = http://refstack.net, http://localhost:8080
# List of sites allowed cross-site resource access. If this is empty,
# only same-origin requests are allowed. (list value)
#allowed_cors_origins =
# Switch Refstack app into debug mode. Helpful for development. In
# debug mode static file will be served by pecan application. Also,
@ -127,7 +139,17 @@
#app_dev_mode = false
# Template for test result url. (string value)
#test_results_url = http://refstack.net/output.html?test_id=%s
#test_results_url = /#/results/%s
# The GitHub API URL of the repository and location of the DefCore
# capability files. This URL is used to get a listing of all
# capability files. (string value)
#github_api_capabilities_url = https://api.github.com/repos/openstack/defcore/contents
# This is the base URL that is used for retrieving specific capability
# files. Capability file names will be appended to this URL to get the
# contents of that file. (string value)
#github_raw_base_url = https://raw.githubusercontent.com/openstack/defcore/master/
# Number of results for one page (integer value)
#results_per_page = 20
@ -135,15 +157,6 @@
# The format for start_date and end_date parameters (string value)
#input_date_format = %Y-%m-%d %H:%M:%S
# The GitHub API URL of the repository and location of the DefCore
# capability files. This URL is used to get a listing of all capability
# files.
#github_api_capabilities_url = https://api.github.com/repos/openstack/defcore/contents
# The base URL that is used for retrieving specific capability files.
# Capability file names will be appended to this URL to get the contents
# of that file.
#github_raw_base_url = https://raw.githubusercontent.com/openstack/defcore/master/
[database]
@ -249,3 +262,51 @@
# error is raised. Set to -1 to specify an infinite retry count.
# (integer value)
#db_max_retries = 20
[osid]
#
# From refstack
#
# OpenStackID Auth Server URI. (string value)
#openstack_openid_endpoint = https://openstackid.org/accounts/openid2
# Interaction mode. Specifies whether Openstack Id IdP may interact
# with the user to determine the outcome of the request. (string
# value)
#openid_mode = checkid_setup
# Protocol version. Value identifying the OpenID protocol version
# being used. This value should be "http://specs.openid.net/auth/2.0".
# (string value)
#openid_ns = http://specs.openid.net/auth/2.0
# Return endpoint in Refstack's API. Value indicating the endpoint
# where the user should be returned to after signing in. Openstack Id
# Idp only supports HTTPS address types. (string value)
#openid_return_to = /v1/auth/signin_return
# Claimed identifier. This value must be set to
# "http://specs.openid.net/auth/2.0/identifier_select". or to user
# claimed identity (user local identifier or user owned identity [ex:
# custom html hosted on a owned domain set to html discover]). (string
# value)
#openid_claimed_id = http://specs.openid.net/auth/2.0/identifier_select
# Alternate identifier. This value must be set to
# http://specs.openid.net/auth/2.0/identifier_select. (string value)
#openid_identity = http://specs.openid.net/auth/2.0/identifier_select
# Indicates request for user attribute information. This value must be
# set to "http://openid.net/extensions/sreg/1.1". (string value)
#openid_ns_sreg = http://openid.net/extensions/sreg/1.1
# Comma-separated list of field names which, if absent from the
# response, will prevent the Consumer from completing the registration
# without End User interation. The field names are those that are
# specified in the Response Format, with the "openid.sreg." prefix
# removed. Valid values include: "country", "email", "firstname",
# "language", "lastname" (string value)
#openid_sreg_required = email,fullname

+ 23
- 0
refstack-ui/app/app.js View File

@ -35,6 +35,29 @@ refstackApp.config([
url: '/results/:testID',
templateUrl: '/components/results-report/resultsReport.html',
controller: 'resultsReportController'
}).
state('profile', {
url: '/profile',
templateUrl: '/components/profile/profile.html',
controller: 'profileController'
});
}
]);
/**
* Try to authenticate user
*/
refstackApp.run(['$http', '$rootScope', 'refstackApiUrl',
function($http, $rootScope, refstackApiUrl) {
'use strict';
var profile_url = refstackApiUrl + '/profile';
$http.get(profile_url, {withCredentials: true}).
success(function(data) {
$rootScope.currentUser = data;
}).
error(function() {
$rootScope.currentUser = null;
});
}
]);


+ 29
- 0
refstack-ui/app/components/auth/authController.js View File

@ -0,0 +1,29 @@
var refstackApp = angular.module('refstackApp');
/**
* Refstack Auth Controller
* This controller handles account authentication for users.
*/
refstackApp.controller('authController',
['$scope', '$window', '$rootScope', 'refstackApiUrl',
function($scope, $window, $rootScope, refstackApiUrl){
'use strict';
var sign_in_url = refstackApiUrl + '/auth/signin';
$scope.doSignIn = function () {
$window.location.href = sign_in_url;
};
var sign_out_url = refstackApiUrl + '/auth/signout';
$scope.doSignOut = function () {
$rootScope.currentUser = null;
$window.location.href = sign_out_url;
};
$scope.isAuthenticated = function () {
if ($scope.currentUser) {
return !!$scope.currentUser.openid;
}
return false;
};
}]);

+ 4
- 0
refstack-ui/app/components/profile/profile.html View File

@ -0,0 +1,4 @@
<h1>Hello, {{user.fullname}}!</h1>
{{bar}}
<p>openid: {{user.openid}}</p>
<p>email: {{user.email}}</p>

+ 21
- 0
refstack-ui/app/components/profile/profileController.js View File

@ -0,0 +1,21 @@
/**
* Refstack User Profile Controller
* This controller handles user's profile page, where a user can view
* account-specific information.
*/
var refstackApp = angular.module('refstackApp');
refstackApp.controller('profileController',
['$scope', '$http', 'refstackApiUrl', '$state',
function($scope, $http, refstackApiUrl, $state) {
'use strict';
var profile_url = refstackApiUrl + '/profile';
$http.get(profile_url, {withCredentials: true}).
success(function(data) {
$scope.user = data;
}).
error(function() {
$state.go('home');
});
}]);

+ 2
- 0
refstack-ui/app/index.html View File

@ -40,6 +40,8 @@
<script src="components/capabilities/capabilitiesController.js"></script>
<script src="components/results/resultsController.js"></script>
<script src="components/results-report/resultsReportController.js"></script>
<script src="components/profile/profileController.js"></script>
<script src="components/auth/authController.js"></script>
<!-- Filters -->
<script src="shared/filters.js"></script>


+ 5
- 0
refstack-ui/app/shared/header/header.html View File

@ -20,6 +20,11 @@ Refstack
<li ng-class="{ active: isActive('/capabilities')}"><a ui-sref="capabilities">DefCore Capabilities</a></li>
<li ng-class="{ active: isActive('/results')}"><a ui-sref="results">Community Results</a></li>
</ul>
<ul ng-controller="authController" class="nav navbar-nav navbar-right">
<li ng-class="{ active: isActive('/profile')}" ng-if="isAuthenticated()"><a ui-sref="profile">{{currentUser.fullname}}</a></li>
<li ng-if="isAuthenticated()"><a href="" ng-click="doSignOut()">Sign Out</a></li>
<li ng-if="!isAuthenticated()"><a href="" ng-click="doSignIn()">Sign In / Sign Up</a></li>
</ul>
</div>
</div>
</nav>


+ 45
- 24
refstack-ui/tests/unit/ControllerSpec.js View File

@ -2,9 +2,16 @@
describe('Refstack controllers', function () {
'use strict';
var fakeApiUrl = 'http://foo.bar/v1';
beforeEach(function () {
module(function ($provide) {
$provide.constant('refstackApiUrl', fakeApiUrl);
});
module('refstackApp');
});
describe('headerController', function () {
var scope, $location;
beforeEach(module('refstackApp'));
beforeEach(inject(function ($rootScope, $controller, _$location_) {
scope = $rootScope.$new();
@ -29,15 +36,39 @@ describe('Refstack controllers', function () {
});
});
describe('authController', function () {
var scope, $httpBackend, $window;
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new();
$window = {location: { href: jasmine.createSpy()} };
$controller('authController', {$scope: scope, $window: $window});
}));
it('should show signin url for signed user', function () {
$httpBackend.expectGET(fakeApiUrl +
'/profile').respond({'openid': 'foo@bar.com',
'email': 'foo@bar.com',
'fullname': 'foo' });
$httpBackend.flush();
scope.doSignIn();
expect($window.location.href).toBe(fakeApiUrl + '/auth/signin');
expect(scope.isAuthenticated()).toBe(true);
});
it('should show signout url for not signed user', function () {
$httpBackend.expectGET(fakeApiUrl +
'/profile').respond(401);
$httpBackend.flush();
scope.doSignOut();
expect($window.location.href).toBe(fakeApiUrl + '/auth/signout');
expect(scope.isAuthenticated()).toBe(false);
});
});
describe('capabilitiesController', function () {
var scope, $httpBackend;
var fakeApiUrl = 'http://foo.bar/v1';
beforeEach(function () {
module('refstackApp');
module(function ($provide) {
$provide.constant('refstackApiUrl', fakeApiUrl);
});
});
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
@ -56,6 +87,8 @@ describe('Refstack controllers', function () {
});
it('should fetch the selected capabilities version', function () {
$httpBackend.expectGET(fakeApiUrl +
'/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl +
'/capabilities').respond(['2015.03.json', '2015.04.json']);
// Should call request with latest version.
@ -115,7 +148,6 @@ describe('Refstack controllers', function () {
describe('resultsController', function () {
var scope, $httpBackend;
var fakeApiUrl = 'http://foo.bar/v1';
var fakeResponse = {
'pagination': {'current_page': 1, 'total_pages': 2},
'results': [{
@ -125,13 +157,6 @@ describe('Refstack controllers', function () {
}]
};
beforeEach(function () {
module('refstackApp');
module(function ($provide) {
$provide.constant('refstackApiUrl', fakeApiUrl);
});
});
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new();
@ -141,6 +166,7 @@ describe('Refstack controllers', function () {
it('should fetch the first page of results with proper URL args',
function () {
// Initial results should be page 1 of all results.
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl +
'/results?page=1').respond(fakeResponse);
$httpBackend.flush();
@ -162,6 +188,7 @@ describe('Refstack controllers', function () {
});
it('should set an error when results cannot be retrieved', function () {
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl + '/results?page=1').respond(404,
{'detail': 'Not Found'});
$httpBackend.flush();
@ -174,6 +201,7 @@ describe('Refstack controllers', function () {
it('should have an function to clear filters and update the view',
function () {
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl +
'/results?page=1').respond(fakeResponse);
scope.startDate = 'some date';
@ -190,7 +218,6 @@ describe('Refstack controllers', function () {
describe('resultsReportController', function () {
var scope, $httpBackend, stateparams;
var fakeApiUrl = 'http://foo.bar/v1';
var fakeResultResponse = {'results': ['test_id_1']};
var fakeCapabilityResponse = {
'platform': {'required': ['compute']},
@ -211,13 +238,6 @@ describe('Refstack controllers', function () {
}
};
beforeEach(function () {
module('refstackApp');
module(function ($provide) {
$provide.constant('refstackApiUrl', fakeApiUrl);
});
});
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
stateparams = {testID: 1234};
@ -229,6 +249,7 @@ describe('Refstack controllers', function () {
it('should make all necessary API requests to get results ' +
'and capabilities',
function () {
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl +
'/results/1234').respond(fakeResultResponse);
$httpBackend.expectGET(fakeApiUrl +


+ 8
- 2
refstack-ui/tests/unit/FilterSpec.js View File

@ -2,9 +2,16 @@
describe('Refstack filters', function () {
'use strict';
var fakeApiUrl = 'http://foo.bar/v1';
beforeEach(function () {
module(function ($provide) {
$provide.constant('refstackApiUrl', fakeApiUrl);
});
module('refstackApp');
});
describe('Filter: arrayConverter', function () {
var $filter;
beforeEach(module('refstackApp'));
beforeEach(inject(function (_$filter_) {
$filter = _$filter_('arrayConverter');
}));
@ -19,7 +26,6 @@ describe('Refstack filters', function () {
describe('Filter: capitalize', function() {
var $filter;
beforeEach(module('refstackApp'));
beforeEach(inject(function(_$filter_) {
$filter = _$filter_('capitalize');
}));


+ 15
- 0
refstack/__init__.py View File

@ -0,0 +1,15 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""Refstack package."""

+ 15
- 0
refstack/api/__init__.py View File

@ -0,0 +1,15 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""Refstack API package."""

+ 34
- 10
refstack/api/app.py View File

@ -19,20 +19,33 @@ import json
import logging
import os
from beaker.middleware import SessionMiddleware
from oslo_config import cfg
from oslo_log import log
from oslo_log import loggers
import pecan
import webob
from refstack.api import utils as api_utils
from refstack.common import validators
LOG = log.getLogger(__name__)
PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
os.pardir)
UI_OPTS = [
cfg.StrOpt('ui_url',
default='http://refstack.net',
help='Url of user interface for Refstack. Need for redirects '
'after sign in and sign out.'
),
]
API_OPTS = [
cfg.StrOpt('api_url',
default='http://refstack.net',
help='Url of public Refstack API.'
),
cfg.StrOpt('static_root',
default='%(project_root)s/static',
help='The directory where your static files can '
@ -65,7 +78,7 @@ API_OPTS = [
'contain some details with debug information.'
),
cfg.StrOpt('test_results_url',
default='http://refstack.net/#/results/%s',
default='/#/results/%s',
help='Template for test result url.'
),
cfg.StrOpt('github_api_capabilities_url',
@ -89,6 +102,8 @@ CONF = cfg.CONF
opt_group = cfg.OptGroup(name='api',
title='Options for the Refstack API')
CONF.register_opts(UI_OPTS)
CONF.register_group(opt_group)
CONF.register_opts(API_OPTS, opt_group)
@ -96,9 +111,8 @@ log.register_options(CONF)
class JSONErrorHook(pecan.hooks.PecanHook):
"""
A pecan hook that translates webob HTTP errors into a JSON format.
"""
"""A pecan hook that translates webob HTTP errors into a JSON format."""
def __init__(self):
"""Hook init."""
@ -106,7 +120,9 @@ class JSONErrorHook(pecan.hooks.PecanHook):
def on_error(self, state, exc):
"""Request error handler."""
if isinstance(exc, webob.exc.HTTPError):
if isinstance(exc, webob.exc.HTTPRedirection):
return
elif isinstance(exc, webob.exc.HTTPError):
status_code = exc.status_int
body = {'title': exc.title}
elif isinstance(exc, validators.ValidationError):
@ -128,9 +144,8 @@ class JSONErrorHook(pecan.hooks.PecanHook):
class CORSHook(pecan.hooks.PecanHook):
"""
A pecan hook that handles Cross-Origin Resource Sharing.
"""
"""A pecan hook that handles Cross-Origin Resource Sharing."""
def __init__(self):
"""Init the hook by getting the allowed origins."""
@ -149,6 +164,7 @@ class CORSHook(pecan.hooks.PecanHook):
'GET, OPTIONS, PUT, POST'
state.response.headers['Access-Control-Allow-Headers'] = \
'origin, authorization, accept, content-type'
state.response.headers['Access-Control-Allow-Credentials'] = 'true'
def setup_app(config):
@ -187,8 +203,16 @@ def setup_app(config):
)]
)
beaker_conf = {
'session.key': 'refstack',
'session.type': 'memory',
'session.timeout': 604800,
'session.validate_key': api_utils.get_token(),
}
app = SessionMiddleware(app, beaker_conf)
if CONF.api.app_dev_mode:
LOG.debug('\n\n Refstack is served at %s \n\n',
CONF.api.test_results_url.split('/#/')[0])
LOG.debug('\n\n <<< Refstack UI is available at %s >>>\n\n',
CONF.ui_url)
return app

+ 18
- 3
refstack/api/constants.py View File

@ -12,12 +12,27 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Constants for Refstack API
"""
"""Constants for Refstack API."""
# Names of input parameters for request
START_DATE = 'start_date'
END_DATE = 'end_date'
CPID = 'cpid'
PAGE = 'page'
# OpenID parameters
OPENID_MODE = 'openid.mode'
OPENID_NS = 'openid.ns'
OPENID_RETURN_TO = 'openid.return_to'
OPENID_CLAIMED_ID = 'openid.claimed_id'
OPENID_IDENTITY = 'openid.identity'
OPENID_REALM = 'openid.realm'
OPENID_NS_SREG = 'openid.ns.sreg'
OPENID_NS_SREG_REQUIRED = 'openid.sreg.required'
OPENID_NS_SREG_EMAIL = 'openid.sreg.email'
OPENID_NS_SREG_FULLNAME = 'openid.sreg.fullname'
OPENID_ERROR = 'openid.error'
# User session parameters
CSRF_TOKEN = 'csrf_token'
USER_OPENID = 'user_openid'

+ 15
- 0
refstack/api/controllers/__init__.py View File

@ -0,0 +1,15 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""API controllers package."""

+ 170
- 0
refstack/api/controllers/auth.py View File

@ -0,0 +1,170 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""Authentication controller."""
from oslo_config import cfg
from oslo_log import log
import pecan
from pecan import rest
from six.moves.urllib import parse
from refstack.api import constants as const
from refstack.api import utils as api_utils
from refstack import db
LOG = log.getLogger(__name__)
OPENID_OPTS = [
cfg.StrOpt('openstack_openid_endpoint',
default='https://openstackid.org/accounts/openid2',
help='OpenStackID Auth Server URI.'
),
cfg.StrOpt('openid_mode',
default='checkid_setup',
help='Interaction mode. Specifies whether Openstack Id '
'IdP may interact with the user to determine the '
'outcome of the request.'
),
cfg.StrOpt('openid_ns',
default='http://specs.openid.net/auth/2.0',
help='Protocol version. Value identifying the OpenID '
'protocol version being used. This value should '
'be "http://specs.openid.net/auth/2.0".'
),
cfg.StrOpt('openid_return_to',
default='/v1/auth/signin_return',
help='Return endpoint in Refstack\'s API. Value indicating '
'the endpoint where the user should be returned to after '
'signing in. Openstack Id Idp only supports HTTPS '
'address types.'
),
cfg.StrOpt('openid_claimed_id',
default='http://specs.openid.net/auth/2.0/identifier_select',
help='Claimed identifier. This value must be set to '
'"http://specs.openid.net/auth/2.0/identifier_select". '
'or to user claimed identity (user local identifier '
'or user owned identity [ex: custom html hosted on a '
'owned domain set to html discover]).'
),
cfg.StrOpt('openid_identity',
default='http://specs.openid.net/auth/2.0/identifier_select',
help='Alternate identifier. This value must be set to '
'http://specs.openid.net/auth/2.0/identifier_select.'
),
cfg.StrOpt('openid_ns_sreg',
default='http://openid.net/extensions/sreg/1.1',
help='Indicates request for user attribute information. '
'This value must be set to '
'"http://openid.net/extensions/sreg/1.1".'
),
cfg.StrOpt('openid_sreg_required',
default='email,fullname',
help='Comma-separated list of field names which, '
'if absent from the response, will prevent the '
'Consumer from completing the registration without '
'End User interation. The field names are those that '
'are specified in the Response Format, with the '
'"openid.sreg." prefix removed. Valid values include: '
'"country", "email", "firstname", "language", "lastname"'
)
]
CONF = cfg.CONF
opt_group = cfg.OptGroup(name='osid',
title='Options for the Refstack OpenID 2.0 through '
'Openstack Authentication Server')
CONF.register_group(opt_group)
CONF.register_opts(OPENID_OPTS, opt_group)
class AuthController(rest.RestController):
"""Controller provides user authentication in OpenID 2.0 IdP."""
_custom_actions = {
"signin": ["GET"],
"signin_return": ["GET"],
"signout": ["GET"]
}
@pecan.expose()
def signin(self):
"""Handle signin request."""
session = api_utils.get_user_session()
if api_utils.is_authenticated():
pecan.redirect(CONF.ui_url)
else:
api_utils.delete_params_from_user_session([const.USER_OPENID])
csrf_token = api_utils.get_token()
session[const.CSRF_TOKEN] = csrf_token
session.save()
return_endpoint = parse.urljoin(CONF.api.api_url,
CONF.osid.openid_return_to)
return_to = api_utils.set_query_params(return_endpoint,
{const.CSRF_TOKEN: csrf_token})
params = {
const.OPENID_MODE: CONF.osid.openid_mode,
const.OPENID_NS: CONF.osid.openid_ns,
const.OPENID_RETURN_TO: return_to,
const.OPENID_CLAIMED_ID: CONF.osid.openid_claimed_id,
const.OPENID_IDENTITY: CONF.osid.openid_identity,
const.OPENID_REALM: CONF.api.api_url,
const.OPENID_NS_SREG: CONF.osid.openid_ns_sreg,
const.OPENID_NS_SREG_REQUIRED: CONF.osid.openid_sreg_required,
}
url = CONF.osid.openstack_openid_endpoint
url = api_utils.set_query_params(url, params)
pecan.redirect(location=url)
@pecan.expose()
def signin_return(self):
"""Handle returned request from OpenID 2.0 IdP."""
session = api_utils.get_user_session()
if pecan.request.GET.get(const.OPENID_ERROR):
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
pecan.abort(401, pecan.request.GET.get(const.OPENID_ERROR))
if pecan.request.GET.get(const.OPENID_MODE) == 'cancel':
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
pecan.abort(401, 'Authentication canceled.')
session_token = session.get(const.CSRF_TOKEN)
request_token = pecan.request.GET.get(const.CSRF_TOKEN)
if request_token != session_token:
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
pecan.abort(401, 'Authentication is failed. Try again.')
api_utils.verify_openid_request(pecan.request)
user_info = {
'openid': pecan.request.GET.get(const.OPENID_CLAIMED_ID),
'email': pecan.request.GET.get(const.OPENID_NS_SREG_EMAIL),
'fullname': pecan.request.GET.get(const.OPENID_NS_SREG_FULLNAME)
}
user = db.user_update_or_create(user_info)
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
session[const.USER_OPENID] = user.openid
session.save()
pecan.redirect(CONF.ui_url)
@pecan.expose()
def signout(self):
"""Handle signout request."""
if api_utils.is_authenticated():
api_utils.delete_params_from_user_session([const.USER_OPENID])
pecan.redirect(CONF.ui_url)

+ 5
- 0
refstack/api/controllers/root.py View File

@ -32,4 +32,9 @@ class RootController(object):
if CONF.api.app_dev_mode:
@expose(generic=True, template='index.html')
def index(self):
"""Return index.html in development mode.
It allows to run both API and UI with pecan serve.
Template path should point into UI app folder
"""
return dict()

+ 39
- 0
refstack/api/controllers/user.py View File

@ -0,0 +1,39 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""User profile controller."""
import pecan
from pecan import rest
from pecan.secure import secure
from refstack.api import constants as const
from refstack.api import utils as api_utils
from refstack import db
class ProfileController(rest.RestController):
"""Controller provides user information in OpenID 2.0 IdP."""
@secure(api_utils.is_authenticated)
@pecan.expose('json')
def get(self):
"""Handle get request on user info."""
session = api_utils.get_user_session()
user = db.user_get(session.get(const.USER_OPENID))
return {
"openid": user.openid,
"email": user.email,
"fullname": user.fullname
}

+ 29
- 18
refstack/api/controllers/v1.py View File

@ -24,10 +24,13 @@ from pecan import rest
import re
import requests
import requests_cache
from six.moves.urllib import parse
from refstack import db
from refstack.api import constants as const
from refstack.api import utils as api_utils
from refstack.api.controllers import auth
from refstack.api.controllers import user
from refstack.common import validators
LOG = log.getLogger(__name__)
@ -55,7 +58,8 @@ requests_cache.install_cache(cache_name='github_cache',
class BaseRestControllerWithValidation(rest.RestController):
"""
"""Rest controller with validation.
Controller provides validation for POSTed data
exposed endpoints:
POST base_url/
@ -66,22 +70,24 @@ class BaseRestControllerWithValidation(rest.RestController):
__validator__ = None
def __init__(self): # pragma: no cover
"""Init."""
if self.__validator__:
self.validator = self.__validator__()
else:
raise ValueError("__validator__ is not defined")
def get_item(self, item_id): # pragma: no cover
"""Handler for getting item"""
"""Handler for getting item."""
raise NotImplementedError
def store_item(self, item_in_json): # pragma: no cover
"""Handler for storing item. Should return new item id"""
"""Handler for storing item. Should return new item id."""
raise NotImplementedError
@pecan.expose('json')
def get_one(self, arg):
"""Return test results in JSON format.
:param arg: item ID in uuid4 format or action
"""
if self.validator.assert_id(arg):
@ -110,7 +116,7 @@ class ResultsController(BaseRestControllerWithValidation):
__validator__ = validators.TestResultValidator
def get_item(self, item_id):
"""Handler for getting item"""
"""Handler for getting item."""
test_info = db.get_test(item_id)
if not test_info:
pecan.abort(404)
@ -122,7 +128,7 @@ class ResultsController(BaseRestControllerWithValidation):
"results": test_name_list}
def store_item(self, item_in_json):
"""Handler for storing item. Should return new item id"""
"""Handler for storing item. Should return new item id."""
item = item_in_json.copy()
if pecan.request.headers.get('X-Public-Key'):
if 'metadata' not in item:
@ -132,21 +138,21 @@ class ResultsController(BaseRestControllerWithValidation):
test_id = db.store_results(item)
LOG.debug(item)
return {'test_id': test_id,
'url': CONF.api.test_results_url % test_id}
'url': parse.urljoin(CONF.ui_url,
CONF.api.test_results_url) % test_id}
@pecan.expose('json')
def get(self):
"""Get information of all uploaded test results.
Get information of all uploaded test results in descending
chronological order. Make it possible to specify some
input parameters for filtering.
For example:
/v1/results?page=<page number>&cpid=1234.
By default, page is set to page number 1,
if the page parameter is not specified.
"""
Get information of all uploaded test results in descending
chronological order.
Make it possible to specify some input parameters
for filtering.
For example:
/v1/results?page=<page number>&cpid=1234.
By default, page is set to page number 1,
if the page parameter is not specified.
"""
expected_input_params = [
const.START_DATE,
const.END_DATE,
@ -192,8 +198,11 @@ class ResultsController(BaseRestControllerWithValidation):
class CapabilitiesController(rest.RestController):
"""/v1/capabilities handler. This acts as a proxy for retrieving
capability files from the openstack/defcore Github repository."""
"""/v1/capabilities handler.
This acts as a proxy for retrieving capability files
from the openstack/defcore Github repository.
"""
@pecan.expose('json')
def get(self):
@ -248,3 +257,5 @@ class V1Controller(object):
results = ResultsController()
capabilities = CapabilitiesController()
auth = auth.AuthController()
profile = user.ProfileController()

+ 96
- 9
refstack/api/utils.py View File

@ -15,12 +15,18 @@
"""Refstack API's utils."""
import copy
import random
import requests
import string
from oslo_config import cfg
from oslo_log import log
from oslo_utils import timeutils
import pecan
import six
from six.moves.urllib import parse
from refstack import db
from refstack.api import constants as const
LOG = log.getLogger(__name__)
@ -28,14 +34,17 @@ CONF = cfg.CONF
class ParseInputsError(Exception):
"""Raise if input params are invalid."""
pass
def _get_input_params_from_request(expected_params):
"""Get input parameters from request.
:param expecred_params: (array) Expected input
params specified in constants.
:param expecred_params: (array) Expected input
params specified in constants.
"""
filters = {}
for param in expected_params:
@ -53,9 +62,8 @@ def _get_input_params_from_request(expected_params):
def parse_input_params(expected_input_params):
"""Parse input parameters from request.
:param expecred_params: (array) Expected input
params specified in constants.
:param expecred_params: (array) Expected input
params specified in constants.
"""
raw_filters = _get_input_params_from_request(expected_input_params)
filters = copy.deepcopy(raw_filters)
@ -83,8 +91,9 @@ def parse_input_params(expected_input_params):
def _calculate_pages_number(per_page, records_count):
"""Return pages number.
:param per_page: (int) results number fot one page.
:param records_count: (int) total records count.
:param per_page: (int) results number fot one page.
:param records_count: (int) total records count.
"""
quotient, remainder = divmod(records_count, per_page)
if remainder > 0:
@ -93,8 +102,9 @@ def _calculate_pages_number(per_page, records_count):
def get_page_number(records_count):
"""Get page number from request
:param records_count: (int) total records count.
"""Get page number from request.
:param records_count: (int) total records count.
"""
page_number = pecan.request.GET.get(const.PAGE)
per_page = CONF.api.results_per_page
@ -121,3 +131,80 @@ def get_page_number(records_count):
'is greater than the total number of pages.')
return (page_number, total_pages)
def set_query_params(url, params):
"""Set params in given query."""
url_parts = parse.urlparse(url)
url = parse.urlunparse((
url_parts.scheme,
url_parts.netloc,
url_parts.path,
url_parts.params,
parse.urlencode(params),
url_parts.fragment))
return url
def get_token(length=30):
"""Get random token."""
return ''.join(random.choice(string.ascii_lowercase)
for i in range(length))
def delete_params_from_user_session(params):
"""Delete params from user session."""
session = get_user_session()
for param in params:
if session.get(param):
del session[param]
session.save()
def get_user_session():
"""Return user session."""
return pecan.request.environ['beaker.session']
def is_authenticated():
"""Return True if user is authenticated."""
session = get_user_session()
if session.get(const.USER_OPENID):
try:
if db.user_get(session.get(const.USER_OPENID)):
return True
except db.UserNotFound:
pass
return False
def verify_openid_request(request):
"""Verify OpenID returned request in OpenID."""
verify_params = dict(request.params.copy())
verify_params["openid.mode"] = "check_authentication"
verify_response = requests.post(
CONF.osid.openstack_openid_endpoint, data=verify_params,
verify=not CONF.api.app_dev_mode
)
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'):
pecan.abort(401, 'Authentication is failed. Try again.')
# Is the data we've received within our required parameters?
required_parameters = {
const.OPENID_NS_SREG_EMAIL: 'Please permit access to '
'your email address.',
const.OPENID_NS_SREG_FULLNAME: 'Please permit access to '
'your name.',
}
for name, error in six.iteritems(required_parameters):
if name not in verify_params or not verify_params[name]:
pecan.abort(401, 'Authentication is failed. %s' % error)
return True

+ 15
- 0
refstack/common/__init__.py View File

@ -0,0 +1,15 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""Refstack common package."""

+ 15
- 10
refstack/common/validators.py View File

@ -13,8 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
""" Validators module
"""
"""Validators module."""
import binascii
import uuid
@ -30,7 +29,10 @@ ext_format_checker = jsonschema.FormatChecker()
class ValidationError(Exception):
"""Raise if request doesn't pass trough validation process."""
def __init__(self, title, exc=None):
"""Init."""
super(ValidationError, self).__init__(title)
self.exc = exc
self.title = title
@ -40,14 +42,16 @@ class ValidationError(Exception):
if self.exc else self.title
def __repr__(self):
"""Repr method."""
return self.details
def __str__(self):
"""Str method."""
return self.__repr__()
def is_uuid(inst):
""" Check that inst is a uuid_hex string. """
"""Check that inst is a uuid_hex string."""
try:
uuid.UUID(hex=inst)
except (TypeError, ValueError):
@ -59,20 +63,20 @@ def is_uuid(inst):
format='uuid_hex',
raises=(TypeError, ValueError))
def checker_uuid(inst):
"""Checker 'uuid_hex' format for jsonschema validator"""
"""Checker 'uuid_hex' format for jsonschema validator."""
return is_uuid(inst)
class Validator(object):
"""Base class for validators"""
"""Base class for validators."""
def __init__(self):
"""Init."""
self.schema = {} # pragma: no cover
def validate(self, request):
"""
:param json_data: data for validation
"""
"""Validate request."""
try:
body = json.loads(request.body)
except (ValueError, TypeError) as e:
@ -89,7 +93,7 @@ class TestResultValidator(Validator):
"""Validator for incoming test results."""
def __init__(self):
"""Init."""
self.schema = {
'type': 'object',
'properties': {
@ -122,6 +126,7 @@ class TestResultValidator(Validator):
)
def validate(self, request):
"""Validate uploaded test results."""
super(TestResultValidator, self).validate(request)
if request.headers.get('X-Signature') or \
request.headers.get('X-Public-Key'):
@ -142,5 +147,5 @@ class TestResultValidator(Validator):
@staticmethod
def assert_id(_id):
""" Check that _id is a valid uuid_hex string. """
"""Check that _id is a valid uuid_hex string."""
return is_uuid(_id)

+ 1
- 3
refstack/db/__init__.py View File

@ -12,8 +12,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
DB abstraction for Refstack
"""
"""DB abstraction for Refstack."""
from refstack.db.api import * # noqa

+ 22
- 6
refstack/db/api.py View File

@ -18,7 +18,6 @@
Functions in this module are imported into the refstack.db namespace.
Call these functions from refstack.db namespace, not the refstack.db.api
namespace.
"""
from oslo_config import cfg
from oslo_db import api as db_api
@ -37,8 +36,9 @@ _BACKEND_MAPPING = {'sqlalchemy': 'refstack.db.sqlalchemy.api'}
IMPL = db_api.DBAPI.from_config(cfg.CONF, backend_mapping=_BACKEND_MAPPING,
lazy=True)
UserNotFound = IMPL.UserNotFound
###################
def store_results(results):
"""Storing results into database.
@ -66,9 +66,9 @@ def get_test_results(test_id):
def get_test_records(page_number, per_page, filters):
"""Get page with applied filters for uploaded test records.
:param page_number: The number of page.
:param per_page: The number of results for one page.
:param filters: (Dict) Filters that will be applied for records.
:param page_number: The number of page.
:param per_page: The number of results for one page.
:param filters: (Dict) Filters that will be applied for records.
"""
return IMPL.get_test_records(page_number, per_page, filters)
@ -76,6 +76,22 @@ def get_test_records(page_number, per_page, filters):
def get_test_records_count(filters):
"""Get total pages number with applied filters for uploaded test records.
:param filters: (Dict) Filters that will be applied for records.
:param filters: (Dict) Filters that will be applied for records.
"""
return IMPL.get_test_records_count(filters)
def user_get(user_openid):
"""Get user info.
:param user_openid: User openid
"""
return IMPL.user_get(user_openid)
def user_update_or_create(user_info):
"""Create user DB record if it exists, otherwise record will be updated.
:param user_info: User record
"""
return IMPL.user_update_or_create(user_info)

+ 15
- 0
refstack/db/migrations/__init__.py View File

@ -0,0 +1,15 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""Migrations."""

+ 15
- 0
refstack/db/migrations/alembic/__init__.py View File

@ -0,0 +1,15 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# 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.
"""Alembic backend for migrations."""

+ 4
- 3
refstack/db/migrations/alembic/env.py View File

@ -14,6 +14,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Alembic environment script."""
from __future__ import with_statement
from alembic import context
@ -23,12 +25,11 @@ from refstack.db.sqlalchemy import models as db_models
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context."""
and associate a connection with the context.
"""
engine = db_api.get_engine()
connection = engine.connect()
target_metadata = db_models.RefStackBase.metadata


+ 1
- 3
refstack/db/migrations/alembic/migration.py View File

@ -12,9 +12,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Implementation of Alembic commands.
"""
"""Implementation of Alembic commands."""
import os
import alembic


+ 38
- 0
refstack/db/migrations/alembic/versions/2f178b0bf762_create_user_table.py View File

</
@ -0,0 +1,38 @@
"""Create user table.
Revision ID: 2f178b0bf762
Revises: 42278d6179b9
Create Date: 2015-05-12 12:15:43.810938
"""
# revision identifiers, used by Alembic.
revision = '2f178b0bf762'
down_revision = '42278d6179b9'