Compare commits

...

15 Commits

  1. 4
      .gitignore
  2. 25
      .gitlab-ci.yml
  3. 4
      TODO
  4. 21
      administration_client/.gitignore
  5. 26
      administration_client/README.md
  6. 5
      administration_client/babel.config.js
  7. 13
      administration_client/create_config_files.sh
  8. 81
      administration_client/package.json
  9. BIN
      administration_client/public/favicon.ico
  10. 17
      administration_client/public/index.html
  11. 29
      administration_client/src/App.vue
  12. BIN
      administration_client/src/assets/logo.png
  13. 59
      administration_client/src/components/HelloWorld.vue
  14. 12
      administration_client/src/main.ts
  15. 23
      administration_client/src/router.ts
  16. 13
      administration_client/src/shims-tsx.d.ts
  17. 4
      administration_client/src/shims-vue.d.ts
  18. 16
      administration_client/src/store.ts
  19. 5
      administration_client/src/views/About.vue
  20. 18
      administration_client/src/views/Home.vue
  21. 12
      administration_client/tests/unit/HelloWorld.spec.ts
  22. 40
      administration_client/tsconfig.json
  23. 19
      administration_client/tslint.json
  24. 8317
      administration_client/yarn.lock
  25. 15
      server/atheneum/api/authentication_api.py
  26. 10
      server/atheneum/middleware/authentication_middleware.py
  27. 16
      server/atheneum/service/transformation_service.py
  28. 45
      server/documentation/api/authentication.rst
  29. 4
      server/documentation/api/index.rst
  30. 24
      server/documentation/api/user.rst
  31. 2
      server/documentation/apidoc-gen.sh
  32. 9
      server/documentation/conf.py
  33. 1
      server/documentation/index.rst
  34. 46
      server/documentation/server/atheneum.api.rst
  35. 22
      server/documentation/server/atheneum.middleware.rst
  36. 22
      server/documentation/server/atheneum.model.rst
  37. 49
      server/documentation/server/atheneum.rst
  38. 70
      server/documentation/server/atheneum.service.rst
  39. 46
      server/documentation/server/atheneum.utility.rst
  40. 7
      server/documentation/server/modules.rst
  41. 15
      server/tests/api/test_authentication_api.py
  42. 22
      server/tests/api/test_user_api.py
  43. 46
      server/tests/conftest.py
  44. 92
      server/tests/service/test_authentication_service.py
  45. 1
      server/tests/service/test_transformation_service.py

4
.gitignore

@ -9,3 +9,7 @@
/server/.pytest_cache/
/server/documentation/_build/
/server/instance/
# Atheneum Administration Client Specific Ignores
/administration_client/node_modules/
/administration_client/dist/

25
.gitlab-ci.yml

@ -2,7 +2,7 @@ stages:
- test
- deploy
tests:
server-tests:
image: python:3.6-slim-stretch
stage: test
script:
@ -13,16 +13,27 @@ tests:
tags:
- docker
administration-client-tests:
image: node:8.11-stretch
stage: test
script:
- cd administration_client
- ./create_config_files.sh
- yarn install
- yarn run test:unit
tags:
- docker
pages:
image: python:3.6-slim-stretch
stage: deploy
script:
- python3 -m pip install pipenv
- cd server
- pipenv install --dev --system
- cd documentation
- sphinx-build -M html "." "_build"
- mv _build/html/ ../../public/
- python3 -m pip install pipenv
- cd server
- pipenv install --dev --system
- cd documentation
- sphinx-build -M html "." "_build"
- mv _build/html/ ../../public/
artifacts:
paths:
- public

4
TODO

@ -0,0 +1,4 @@
Items are ranked from top to bottom for priority
* Administration Web Interface
* Docker Compose

21
administration_client/.gitignore

@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

26
administration_client/README.md

@ -0,0 +1,26 @@
# administration_client
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn run serve
```
### Compiles and minifies for production
```
yarn run build
```
### Lints and fixes files
```
yarn run lint
```
### Run your unit tests
```
yarn run test:unit
```

5
administration_client/babel.config.js

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

13
administration_client/create_config_files.sh

@ -0,0 +1,13 @@
#!/usr/bin/env bash
if [ -z "$NPM_CONFIG_REGISTRY" ]
then
echo "No custom registry defined..."
else
echo "Creating custom registry configurations..."
echo "registry \"$NPM_CONFIG_REGISTRY\""
echo "registry \"$NPM_CONFIG_REGISTRY\"" >> .npmrc
echo "registry \"$NPM_CONFIG_REGISTRY\"" >> .yarnrc
echo "Replacing static yarn registry references in yarn.lock... BUGFIX... revisit with yarn 2+... https://github.com/yarnpkg/yarn/issues/3330"
find . -name yarn.lock -exec sed -i "s#https://registry.yarnpkg.com#`awk '$1 == "registry" { gsub(/"/,""); print $NF}' .yarnrc`#g" {} \;
fi

81
administration_client/package.json

@ -0,0 +1,81 @@
{
"name": "atheneum_administration_client",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"vue": "^2.5.17",
"vue-class-component": "^6.0.0",
"vue-property-decorator": "^7.0.0",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
},
"devDependencies": {
"@types/jest": "^23.1.4",
"@vue/cli-plugin-babel": "^3.0.1",
"@vue/cli-plugin-typescript": "^3.0.1",
"@vue/cli-plugin-unit-jest": "^3.0.1",
"@vue/cli-service": "^3.0.1",
"@vue/test-utils": "^1.0.0-beta.20",
"babel-core": "7.0.0-bridge.0",
"lint-staged": "^7.2.2",
"node-sass": "^4.9.0",
"sass-loader": "^7.0.1",
"ts-jest": "^23.0.0",
"typescript": "^3.0.0",
"vue-template-compiler": "^2.5.17"
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"jest": {
"moduleFileExtensions": [
"js",
"jsx",
"json",
"vue",
"ts",
"tsx"
],
"transform": {
"^.+\\.vue$": "vue-jest",
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
"^.+\\.tsx?$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"snapshotSerializers": [
"jest-serializer-vue"
],
"testMatch": [
"**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
],
"testURL": "http://localhost/"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.ts": [
"vue-cli-service lint",
"git add"
],
"*.vue": [
"vue-cli-service lint",
"git add"
]
}
}

BIN
administration_client/public/favicon.ico

17
administration_client/public/index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>administration_client</title>
</head>
<body>
<noscript>
<strong>We're sorry but Atheneum Administration Client doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

29
administration_client/src/App.vue

@ -0,0 +1,29 @@
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
<style lang="scss">
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>

BIN
administration_client/src/assets/logo.png

After

Width: 200  |  Height: 200  |  Size: 6.7 KiB

59
administration_client/src/components/HelloWorld.vue

@ -0,0 +1,59 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" target="_blank" rel="noopener">unit-jest</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
@Prop() private msg!: string;
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

12
administration_client/src/main.ts

@ -0,0 +1,12 @@
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');

23
administration_client/src/router.ts

@ -0,0 +1,23 @@
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
},
],
});

13
administration_client/src/shims-tsx.d.ts

@ -0,0 +1,13 @@
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

4
administration_client/src/shims-vue.d.ts

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

16
administration_client/src/store.ts

@ -0,0 +1,16 @@
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
});

5
administration_client/src/views/About.vue

@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

18
administration_client/src/views/Home.vue

@ -0,0 +1,18 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
@Component({
components: {
HelloWorld,
},
})
export default class Home extends Vue {}
</script>

12
administration_client/tests/unit/HelloWorld.spec.ts

@ -0,0 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message';
const wrapper = shallowMount(HelloWorld, {
propsData: { msg },
});
expect(wrapper.text()).toMatch(msg);
});
});

40
administration_client/tsconfig.json

@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"node",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

19
administration_client/tslint.json

@ -0,0 +1,19 @@
{
"defaultSeverity": "warning",
"extends": [
"tslint:recommended"
],
"linterOptions": {
"exclude": [
"node_modules/**"
]
},
"rules": {
"quotemark": [true, "single"],
"indent": [true, "spaces", 2],
"interface-name": false,
"ordered-imports": false,
"object-literal-sort-keys": false,
"no-consecutive-blank-lines": false
}
}

8317
administration_client/yarn.lock
File diff suppressed because it is too large
View File

15
server/atheneum/api/authentication_api.py

@ -1,13 +1,15 @@
"""Authentication API blueprint and endpoint definitions."""
from flask import Blueprint, g
from flask import Blueprint, g, request
from atheneum.api.decorators import return_json
from atheneum.api.model import APIMessage, APIResponse
from atheneum.middleware import authentication_middleware
from atheneum.model import UserToken
from atheneum.service import (
user_token_service,
authentication_service,
user_service
transformation_service,
user_service,
user_token_service,
)
AUTH_BLUEPRINT = Blueprint(
@ -23,7 +25,12 @@ def login() -> APIResponse:
:return: A login token for continued authentication
"""
user_token = user_token_service.create(g.user)
new_token_options: UserToken = transformation_service.deserialize_model(
UserToken, request.json, ['note', 'expirationTime'])
user_token = user_token_service.create(
g.user,
note=new_token_options.note,
expiration_time=new_token_options.expiration_time)
return APIResponse(user_token, 200)

10
server/atheneum/middleware/authentication_middleware.py

@ -79,7 +79,7 @@ def authorization_failed(required_role: str) -> Response:
def parse_token_header(
header_value: str) -> Optional[Authorization]:
"""
Parse the Authorization: Token header for the username and token.
Parse the Authorization: Bearer header for the username and token.
:param header_value:
:return:
@ -92,13 +92,13 @@ def parse_token_header(
auth_type = auth_type.lower()
except ValueError:
return None
if auth_type == b'token':
if auth_type == b'bearer':
try:
username, token = base64.b64decode(auth_info).split(b':', 1)
except binascii.Error:
return None
return Authorization('token', {'username': bytes_to_wsgi(username),
'password': bytes_to_wsgi(token)})
return Authorization('bearer', {'username': bytes_to_wsgi(username),
'password': bytes_to_wsgi(token)})
return None
@ -146,7 +146,7 @@ def require_token_auth(func: Callable) -> Callable:
request.headers.get('Authorization', None))
if token and authenticate_with_token(token.username, token.password):
return func(*args, **kwargs)
return authentication_failed('Token')
return authentication_failed('Bearer')
return decorate

16
server/atheneum/service/transformation_service.py

@ -36,9 +36,19 @@ class BaseTransformer:
return ret
def deserialize(self,
json_model: dict,
json_model: Optional[dict],
options: Optional[List[str]]) -> Any:
"""Convert dict to Model."""
"""
Convert dict to Model.
If the dict is None or empty, return an empty model.
:param json_model: The dict representing the serialized model
:param options: the fields to deserialize
:return: an instance of the model
"""
if json_model is None or not json_model:
return self.model
field_factories = self._deserializers()
if not options:
options = list(field_factories.keys())
@ -101,7 +111,7 @@ def serialize_model(model_obj: db.Model,
def deserialize_model(
model_type: Type[db.Model],
json_model_object: dict,
json_model_object: Optional[dict],
options: Optional[List[str]] = None) -> db.Model:
"""Lookup a Model and hand it off to the deserializer."""
try:

45
server/documentation/api/authentication.rst

@ -29,11 +29,46 @@ Authentication API
"version": 0
}
**Example request with note and expirationTime**
.. sourcecode:: http
POST /auth/login HTTP/1.1
Host: example.tld
Accept: application/json
Authorization: Basic <Base64 Encoded Basic Auth>
Content-Type: application/json
{
"note": "Client API Access",
"expirationTime": "2019-07-29T23:59:59-05:00"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"note": "Client API Access",
"expirationTime": "2019-07-29T23:59:59-05:00",
"creationTime": "2018-07-29T11:59:29-05:00",
"enabled": true,
"token": "b94cf5c7-cddc-4610-9d4c-6b8e04088ae8",
"version": 0
}
:<header Accept: Response content type depends on :mailheader:`Accept` header
:<header Authorization: Encoded basic authorization
:<header Content-Type: Application/json
:<json string note: Note to attach to the created token
:<json iso8601 expirationTime: Expiration time for the userToken
:>header Content-Type: Depends on :mailheader:`Accept` header of request
:>json datetime creationTime: Creation time for the userToken
:>json datetime expirationTime: Expiration time for the userToken
:>json iso8601 creationTime: Creation time for the userToken
:>json iso8601 expirationTime: Expiration time for the userToken
:>json boolean enabled: Whether the userToken is enabled
:>json string token: UserToken to use for further authentication
:>json int version: Version for the object
@ -51,7 +86,7 @@ Authentication API
POST /auth/bump HTTP/1.1
Host: example.tld
Accept: application/json
Authorization: Token <Base64(user:userToken)>
Authorization: Bearer <Base64(user:userToken)>
**Example response**:
@ -68,7 +103,7 @@ Authentication API
:<header Accept: Response content type depends on :mailheader:`Accept` header
:<header Authorization: Encoded token authorization
:>header Content-Type: Depends on :mailheader:`Accept` header of request
:>json datetime lastLoginTime: Updated lastLoginTime for the user
:>json iso8601 lastLoginTime: Updated lastLoginTime for the user
:statuscode 200: User last_login_time successfully bumped
:statuscode 401: Authorization failed
@ -83,7 +118,7 @@ Authentication API
POST /auth/logout HTTP/1.1
Host: example.tld
Accept: application/json
Authorization: Token <Base64(user:userToken)>
Authorization: Bearer <Base64(user:userToken)>
**Example response**:

4
server/documentation/api/index.rst

@ -1,5 +1,5 @@
Atheneum API documentation
==========================
Atheneum HTTP API
=================
.. toctree::
:maxdepth: 2

24
server/documentation/api/user.rst

@ -12,7 +12,7 @@ User API
GET /user HTTP/1.1
Host: example.tld
Accept: application/json
Authorization: Token <Base64(user:userToken)>
Authorization: Bearer <Base64(user:userToken)>
**Example response**:
@ -65,7 +65,7 @@ User API
GET /user/atheneum_administrator HTTP/1.1
Host: example.tld
Accept: application/json
Authorization: Token <Base64(user:userToken)>
Authorization: Bearer <Base64(user:userToken)>
**Example response**:
@ -87,8 +87,8 @@ User API
:<header Accept: Response content type depends on :mailheader:`Accept` header
:<header Authorization: The encoded basic authorization
:>header Content-Type: Depends on :mailheader:`Accept` header of request
:>json datetime creationTime: Creation time for the user
:>json datetime lastLoginTime: When the user last logged in, or was last bumped
:>json iso8601 creationTime: Creation time for the user
:>json iso8601 lastLoginTime: When the user last logged in, or was last bumped
:>json string name: The user name
:>json string role: The role assigned to the user
:>json int version: Version information
@ -107,7 +107,7 @@ User API
PATCH /user/atheneum_administrator HTTP/1.1
Host: example.tld
Accept: application/json
Authorization: Token <Base64(user:userToken)>
Authorization: Bearer <Base64(user:userToken)>
Content-Type: application/json
{
@ -135,14 +135,14 @@ User API
:<header Accept: Response content type depends on :mailheader:`Accept` header
:<header Authorization: Encoded token authorization
:<header Content-Type: application/json
:<json datetime createDateTime: Update createDateTime (Administrator Only)
:<json datetime lastLoginTime: Update lastLoginTime
:<json iso8601 createDateTime: Update createDateTime (Administrator Only)
:<json iso8601 lastLoginTime: Update lastLoginTime
:<json string name: Update user name (Administrator Only)
:<json string role: Update user role (Must be less than or equal to the role authenticating the action)
:<json int version: Must match the latest version of the user
:>header Content-Type: Depends on :mailheader:`Accept` header of request
:>json datetime creationTime: Creation time for the user
:>json datetime lastLoginTime: When the user last logged in, or was last bumped
:>json iso8601 creationTime: Creation time for the user
:>json iso8601 lastLoginTime: When the user last logged in, or was last bumped
:>json string name: The user name
:>json string role: The role assigned to the user
:>json int version: Version information
@ -162,7 +162,7 @@ User API
POST /user HTTP/1.1
Host: example.tld
Accept: application/json
Authorization: Token <Base64(user:userToken)>
Authorization: Bearer <Base64(user:userToken)>
Content-Type: application/json
{
@ -193,7 +193,7 @@ User API
:<json string password: Password to use
:<json string role: Role to assign to the user (Must be less than or equal to the role of the authenticating user)
:>header Content-Type: Depends on :mailheader:`Accept` header of request
:>json datetime creationTime: Datetime the user was created
:>json iso8601 creationTime: Datetime the user was created
:>json string name: Name of the created user
:>json string role: Role of the created user
:>json int version: Version number of the created user
@ -212,7 +212,7 @@ User API
DELETE /user/test_user HTTP/1.1
Host: example.tld
Accept: application/json
Authorization: Token <Base64(user:userToken)>
Authorization: Bearer <Base64(user:userToken)>
**Example response**:

2
server/documentation/apidoc-gen.sh

@ -0,0 +1,2 @@
#!/usr/bin/env bash
noglob sphinx-apidoc -o server ../atheneum/ tests/*

9
server/documentation/conf.py

@ -12,9 +12,9 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import os
import sys
sys.path.insert(0, os.path.abspath('../'))
# -- Project information -----------------------------------------------------
@ -40,7 +40,8 @@ release = '2018.8.1'
# ones.
extensions = [
'sphinxcontrib.httpdomain',
'sphinxjsondomain'
'sphinxjsondomain',
'sphinx.ext.autodoc'
]
# Add any paths that contain templates here, relative to this directory.

1
server/documentation/index.rst

@ -12,6 +12,7 @@ Welcome to Atheneum's documentation!
introduction
api/index
server/modules
Indices and tables
==================

46
server/documentation/server/atheneum.api.rst

@ -0,0 +1,46 @@
atheneum.api package
====================
Submodules
----------
atheneum.api.authentication\_api module
---------------------------------------
.. automodule:: atheneum.api.authentication_api
:members:
:undoc-members:
:show-inheritance:
atheneum.api.decorators module
------------------------------
.. automodule:: atheneum.api.decorators
:members:
:undoc-members:
:show-inheritance:
atheneum.api.model module
-------------------------
.. automodule:: atheneum.api.model
:members:
:undoc-members:
:show-inheritance:
atheneum.api.user\_api module
-----------------------------
.. automodule:: atheneum.api.user_api
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: atheneum.api
:members:
:undoc-members:
:show-inheritance:

22
server/documentation/server/atheneum.middleware.rst

@ -0,0 +1,22 @@
atheneum.middleware package
===========================
Submodules
----------
atheneum.middleware.authentication\_middleware module
-----------------------------------------------------
.. automodule:: atheneum.middleware.authentication_middleware
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: atheneum.middleware
:members:
:undoc-members:
:show-inheritance:

22
server/documentation/server/atheneum.model.rst

@ -0,0 +1,22 @@
atheneum.model package
======================
Submodules
----------
atheneum.model.user\_model module
---------------------------------
.. automodule:: atheneum.model.user_model
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: atheneum.model
:members:
:undoc-members:
:show-inheritance:

49
server/documentation/server/atheneum.rst

@ -0,0 +1,49 @@
atheneum package
================
Subpackages
-----------
.. toctree::
atheneum.api
atheneum.middleware
atheneum.model
atheneum.service
atheneum.utility
Submodules
----------
atheneum.db module
------------------
.. automodule:: atheneum.db
:members:
:undoc-members:
:show-inheritance:
atheneum.default\_settings module
---------------------------------
.. automodule:: atheneum.default_settings
:members:
:undoc-members:
:show-inheritance:
atheneum.errors module
----------------------
.. automodule:: atheneum.errors
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: atheneum
:members:
:undoc-members:
:show-inheritance:

70
server/documentation/server/atheneum.service.rst

@ -0,0 +1,70 @@
atheneum.service package
========================
Submodules
----------
atheneum.service.authentication\_service module
-----------------------------------------------
.. automodule:: atheneum.service.authentication_service
:members:
:undoc-members:
:show-inheritance:
atheneum.service.patch\_service module
--------------------------------------
.. automodule:: atheneum.service.patch_service
:members:
:undoc-members:
:show-inheritance:
atheneum.service.role\_service module
-------------------------------------
.. automodule:: atheneum.service.role_service
:members:
:undoc-members:
:show-inheritance:
atheneum.service.transformation\_service module
-----------------------------------------------
.. automodule:: atheneum.service.transformation_service
:members:
:undoc-members:
:show-inheritance:
atheneum.service.user\_service module
-------------------------------------
.. automodule:: atheneum.service.user_service
:members:
:undoc-members:
:show-inheritance:
atheneum.service.user\_token\_service module
--------------------------------------------
.. automodule:: atheneum.service.user_token_service
:members:
:undoc-members:
:show-inheritance:
atheneum.service.validation\_service module
-------------------------------------------
.. automodule:: atheneum.service.validation_service
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: atheneum.service
:members:
:undoc-members:
:show-inheritance:

46
server/documentation/server/atheneum.utility.rst

@ -0,0 +1,46 @@
atheneum.utility package
========================
Submodules
----------
atheneum.utility.authentication\_utility module
-----------------------------------------------
.. automodule:: atheneum.utility.authentication_utility
:members:
:undoc-members:
:show-inheritance:
atheneum.utility.json\_utility module
-------------------------------------
.. automodule:: atheneum.utility.json_utility
:members:
:undoc-members:
:show-inheritance:
atheneum.utility.pagination\_utility module
-------------------------------------------
.. automodule:: atheneum.utility.pagination_utility
:members:
:undoc-members:
:show-inheritance:
atheneum.utility.session\_utility module
----------------------------------------
.. automodule:: atheneum.utility.session_utility
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: atheneum.utility
:members:
:undoc-members:
:show-inheritance:

7
server/documentation/server/modules.rst

@ -0,0 +1,7 @@
Atheneum Server API
===================
.. toctree::
:maxdepth: 4
atheneum

15
server/tests/api/test_authentication_api.py

@ -1,3 +1,7 @@
from datetime import datetime
import rfc3339
from tests.conftest import AuthActions
@ -7,6 +11,17 @@ def test_login_happy_path(auth: AuthActions):
assert result.json['token'] is not None and len(result.json['token']) > 0
def test_login_happy_path_with_options(auth: AuthActions):
token_note = 'Test Note'
token_expiration_time = datetime.now()
result = auth.login(token_note, token_expiration_time)
assert result.status_code == 200
assert result.json['token'] is not None and len(result.json['token']) > 0
assert result.json['note'] == token_note
assert result.json['expirationTime'] == rfc3339.format(
token_expiration_time)
def test_bump_happy_path(auth: AuthActions):
auth.login()
result = auth.bump()

22
server/tests/api/test_user_api.py

@ -13,7 +13,7 @@ def test_get_users_happy_path(auth: AuthActions, client: FlaskClient):
result = client.get(
'/user',
headers={
auth_header[0]: auth_header[1]
auth_header.header: auth_header.data
})
assert 200 == result.status_code
assert result.json is not None
@ -30,7 +30,7 @@ def test_get_users_nonexistent_page(auth: AuthActions, client: FlaskClient):
result = client.get(
'/user?page=2',
headers={
auth_header[0]: auth_header[1]
auth_header.header: auth_header.data
})
assert 404 == result.status_code
assert result.json is not None
@ -42,7 +42,7 @@ def test_get_user_happy_path(auth: AuthActions, client: FlaskClient):
result = client.get(
'/user/{}'.format(client.application.config['test_username']),
headers={
auth_header[0]: auth_header[1]
auth_header.header: auth_header.data
})
assert 200 == result.status_code
assert result.json is not None
@ -57,7 +57,7 @@ def test_patch_user_happy_path(auth: AuthActions, client: FlaskClient):
user = client.get(
'/user/{}'.format(client.application.config['test_username']),
headers={
auth_header[0]: auth_header[1]
auth_header.header: auth_header.data
})
patched_user = client.patch(
@ -67,7 +67,7 @@ def test_patch_user_happy_path(auth: AuthActions, client: FlaskClient):
'lastLoginTime': last_login_time
}),
headers={
auth_header[0]: auth_header[1],
auth_header.header: auth_header.data,
'Content-Type': 'application/json'
})
@ -85,7 +85,7 @@ def test_register_user_happy_path(auth: AuthActions, client: FlaskClient):
'name': 'test_registered_user'
}),
headers={
auth_header[0]: auth_header[1],
auth_header.header: auth_header.data,
'Content-Type': 'application/json'
})
assert 200 == result.status_code
@ -104,7 +104,7 @@ def test_register_user_invalid_password(
'password': ''
}),
headers={
auth_header[0]: auth_header[1],
auth_header.header: auth_header.data,
'Content-Type': 'application/json'
})
assert 400 == result.status_code
@ -121,7 +121,7 @@ def test_register_user_twice_failure(auth: AuthActions, client: FlaskClient):
'name': 'test_registered_user'
}),
headers={
auth_header[0]: auth_header[1],
auth_header.header: auth_header.data,
'Content-Type': 'application/json'
})
result2 = client.post(
@ -130,7 +130,7 @@ def test_register_user_twice_failure(auth: AuthActions, client: FlaskClient):
'name': 'test_registered_user'
}),
headers={
auth_header[0]: auth_header[1],
auth_header.header: auth_header.data,
'Content-Type': 'application/json'
})
assert 200 == result1.status_code
@ -150,13 +150,13 @@ def test_delete_user_happy_path(auth: AuthActions, client: FlaskClient):
'name': 'test_registered_user'
}),
headers={
auth_header[0]: auth_header[1],
auth_header.header: auth_header.data,
'Content-Type': 'application/json'
})
result2 = client.delete(
'/user/'+result1.json['name'],
headers={
auth_header[0]: auth_header[1]
auth_header.header: auth_header.data
})
assert 200 == result1.status_code
assert result1.json is not None

46
server/tests/conftest.py

@ -3,10 +3,13 @@ import os
import random
import string
import tempfile
from typing import Tuple, Any
from collections import namedtuple
from datetime import datetime
from typing import Tuple, Any, Optional
import pytest
from flask import Flask
import rfc3339
from flask import Flask, json
from flask.testing import FlaskClient, FlaskCliRunner
from werkzeug.test import Client
@ -66,6 +69,9 @@ def runner(app: Flask) -> FlaskCliRunner:
return app.test_cli_runner()
AuthorizationHeader = namedtuple('AuthorizationHeader', ['header', 'data'])
class AuthActions(object):
def __init__(self,
client: Client,
@ -81,13 +87,31 @@ class AuthActions(object):
self.password = password
return self
def login(self) -> Any:
def login(
self,
note: Optional[str] = None,
expiration_time: Optional[datetime] = None) -> Any:
auth_header = self.get_authorization_header_basic()
auth_json = None
request_headers = {
auth_header.header: auth_header.data
}
if note is not None or expiration_time is not None:
token_options = {}
if note is not None:
token_options['note'] = note
if expiration_time is not None:
token_options['expirationTime'] = rfc3339.format(
expiration_time)
auth_json = json.dumps(token_options)
request_headers['Content-Type'] = 'application/json'
result = self._client.post(
'/auth/login',
headers={
auth_header[0]: auth_header[1]
}
headers=request_headers,
data=auth_json
)
self.token = result.json['token']
return result
@ -110,17 +134,19 @@ class AuthActions(object):
}
)
def get_authorization_header_basic(self) -> Tuple[str, str]:
def get_authorization_header_basic(self) -> AuthorizationHeader:
credentials = base64.b64encode(
'{}:{}'.format(self.username, self.password).encode('utf8')) \
.decode('utf8').strip()
return 'Authorization', 'Basic {}'.format(credentials)
return AuthorizationHeader(
'Authorization', 'Basic {}'.format(credentials))
def get_authorization_header_token(self) -> Tuple[str, str]:
def get_authorization_header_token(self) -> AuthorizationHeader:
credentials = base64.b64encode(
'{}:{}'.format(self.username, self.token).encode('utf8')) \
.decode('utf8').strip()
return 'Authorization', 'Token {}'.format(credentials)
return AuthorizationHeader(
'Authorization', 'Bearer {}'.format(credentials))
@pytest.fixture

92
server/tests/service/test_authentication_service.py

@ -0,0 +1,92 @@
from datetime import datetime, timedelta
import pytest
from mock import patch, MagicMock
from nacl.exceptions import InvalidkeyError
from atheneum import errors
from atheneum.model import User, UserToken
from atheneum.service.authentication_service import (
validate_password_strength,
is_valid_token,
is_valid_password
)
service_module = 'atheneum.service.authentication_service'
def test_validate_password_strength_happy_path():
valid_password = "HorseStapleBattery9"
assert valid_password == validate_password_strength(valid_password)
def test_validate_password_strength_length_failure():
invalid_password = "TooShor"
with pytest.raises(errors.ValidationError) as e_info:
error = e_info
validate_password_strength(invalid_password)
assert error is not None
def test_validate_password_strength_uppercase_failure():
invalid_password = "NOUPPERCASE9"
with pytest.raises(errors.ValidationError) as e_info:
error = e_info
validate_password_strength(invalid_password)
assert error is not None
def test_validate_password_strength_lowercase_failure():
invalid_password = "NOLOWERCASE9"
with pytest.raises(errors.ValidationError) as e_info:
error = e_info
validate_password_strength(invalid_password)
assert error is not None
def test_validate_password_strength_number_failure():
invalid_password = "NoNumber"
with pytest.raises(errors.ValidationError) as e_info:
error = e_info
validate_password_strength(invalid_password)
assert error is not None
@patch(service_module + '.pwhash.verify')
def test_is_valid_password_invalid_key_error(
mock_pwhash_verify: MagicMock):
user = User()
user.password_hash = ''
mock_pwhash_verify.side_effect = InvalidkeyError()
assert not is_valid_password(user, '')
def test_is_valid_token_happy_path():
user_token = UserToken()
user_token.enabled = True
assert is_valid_token(user_token)
def test_is_valid_token_no_token():
assert not is_valid_token(None)
def test_is_valid_token_disabled():
user_token = UserToken()
user_token.enabled = False
assert not is_valid_token(user_token)
def test_is_valid_token_expired():
user_token = UserToken()
user_token.enabled = True
user_token.expiration_time = datetime.now() - timedelta(weeks=1)
assert not is_valid_token(user_token)

1
server/tests/service/test_transformation_service.py

@ -39,7 +39,6 @@ def test_registering_two_transformers_of_the_same_type():
def _deserializers(self) -> Dict[str, Callable[[db.Model, Any], None]]:
pass
error = None
with pytest.raises(KeyError) as e_info:
error = e_info
register_transformer(BadTransformer)

Loading…
Cancel
Save