Auth for REST API (api/v1/auth/*) (#622)

* Added trivial login/logout/register
* Auth methods for REST API.

- api/v1/auth/login
- api/v1/auth/logout

For basic auth only:
- api/v1/auth/register
- api/v1/auth/password/*

* Add info about auth for REST API into CHANGELOG.md
* Add pylintrc for codacy, updated pylint and its dependecies.
* Add token authorization, renamed cvat.js to cvat-core in all places.
* Implemented register method in cvat-core
* Added first_name and last_name to RegisterSerializer.
main
Nikita Manovich 7 years ago committed by GitHub
parent 97ed2aaa28
commit e7bab87039
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,3 +1,3 @@
exclude_paths:
- '**/3rdparty/**'
- '**/engine/js/cvat.js'
- '**/engine/js/cvat-core.min.js'

@ -24,7 +24,7 @@
"eslint:recommended",
"plugin:security/recommended",
"plugin:no-unsanitized/DOM",
"airbnb",
"airbnb-base",
],
"rules": {
"no-await-in-loop": [0],

1
.gitignore vendored

@ -15,7 +15,6 @@ node_modules
# Ignore temporary files
docker-compose.override.yml
/.vscode
__pycache__
*.pyc
._*

@ -0,0 +1,420 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality. This option is deprecated
# and it will be removed in Pylint 2.0.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
disable=all
enable= E0001,E0100,E0101,E0102,E0103,E0104,E0105,E0106,E0107,E0110,
E0113,E0114,E0115,E0116,E0117,E0108,E0202,E0203,E0211,E0236,
E0238,E0239,E0240,E0241,E0301,E0302,E0601,E0603,E0604,E0701,
E0702,E0703,E0704,E0710,E0711,E0712,E1003,E1102,E1111,E0112,
E1120,E1121,E1123,E1124,E1125,E1126,E1127,E1132,E1200,E1201,
E1205,E1206,E1300,E1301,E1302,E1303,E1304,E1305,E1306,
C0123,C0200,C0303,C1001,
W0101,W0102,W0104,W0105,W0106,W0107,W0108,W0109,W0110,W0120,
W0122,W0124,W0150,W0199,W0221,W0222,W0233,W0404,W0410,W0601,
W0602,W0604,W0611,W0612,W0622,W0623,W0702,W0705,W0711,W1300,
W1301,W1302,W1303,,W1305,W1306,W1307
R0102,R0201,R0202,R0203
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
#disable=old-octal-literal,basestring-builtin,no-absolute-import,old-division,coerce-method,long-suffix,reload-builtin,unichr-builtin,indexing-exception,raising-string,dict-iter-method,metaclass-assignment,filter-builtin-not-iterating,import-star-module-level,next-method-called,cmp-method,raw_input-builtin,old-raise-syntax,cmp-builtin,apply-builtin,getslice-method,input-builtin,backtick,coerce-builtin,range-builtin-not-iterating,xrange-builtin,using-cmp-argument,buffer-builtin,hex-method,execfile-builtin,unpacking-in-except,standarderror-builtin,round-builtin,nonzero-method,unicode-builtin,reduce-builtin,file-builtin,dict-view-method,old-ne-operator,print-statement,suppressed-message,oct-method,useless-suppression,delslice-method,long-builtin,setslice-method,zip-builtin-not-iterating,map-builtin-not-iterating,intern-builtin,parameter-unpacking
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]". This option is deprecated
# and it will be removed in Pylint 2.0.
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[ELIF]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=100
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

@ -5,7 +5,15 @@
"version": "0.2.0",
"configurations": [
{
"name": "server",
"type": "chrome",
"request": "launch",
"preLaunchTask": "ui.js: server",
"name": "ui.js: debug",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/cvat-ui"
},
{
"name": "server: django",
"type": "python",
"request": "launch",
"stopOnEntry": false,
@ -19,10 +27,11 @@
"127.0.0.1:7000"
],
"django": true,
"cwd": "${workspaceFolder}"
"cwd": "${workspaceFolder}",
"console": "internalConsole"
},
{
"name": "client",
"name": "server: chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:7000/",
@ -35,7 +44,7 @@
}
},
{
"name": "RQ - default",
"name": "server: RQ - default",
"type": "python",
"request": "launch",
"stopOnEntry": false,
@ -50,10 +59,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "RQ - low",
"name": "server: RQ - low",
"type": "python",
"request": "launch",
"justMyCode": false,
@ -68,10 +78,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "git",
"name": "server: git",
"type": "python",
"request": "launch",
"justMyCode": false,
@ -83,10 +94,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "migrate",
"name": "server: migrate",
"type": "python",
"request": "launch",
"justMyCode": false,
@ -98,10 +110,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "tests",
"name": "server: tests",
"type": "python",
"request": "launch",
"justMyCode": false,
@ -116,10 +129,11 @@
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
"env": {},
"console": "internalConsole"
},
{
"name": "cvat.js debug",
"name": "core.js: debug",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/cvat-core",
@ -143,18 +157,22 @@
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
}
},
{
"type": "chrome",
}
],
"compounds": [
{
"name": "debugging",
"name": "server: debug",
"configurations": [
"client",
"server",
"RQ - default",
"RQ - low",
"git",
"server: chrome",
"server: django",
"server: RQ - default",
"server: RQ - low",
"server: git",
]
},
}
]
}

@ -1,5 +1,5 @@
{
"python.pythonPath": ".env/bin/python",
"python.pythonPath": "python",
"eslint.enable": true,
"eslint.validate": [
"javascript",
@ -18,5 +18,6 @@
"directory": ".",
"changeProcessCWD": true
}
]
],
"python.linting.pylintEnabled": true
}

14
.vscode/tasks.json vendored

@ -0,0 +1,14 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "ui.js: server",
"type": "npm",
"script": "start",
"path": "cvat-ui/",
"problemMatcher": []
}
]
}

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added in PDF extractor
- Added in a command line model manager tester
- Ability to dump/load annotations in several formats from UI (CVAT, Pascal VOC, YOLO, MS COCO, png mask, TFRecord)
- Auth for REST API (api/v1/auth/): login, logout, register, ...
### Changed
- Outside and keyframe buttons in the side panel for all interpolation shapes (they were only for boxes before)

@ -53,7 +53,7 @@ $ code .
- Reload Visual Studio Code
- Select `CVAT Debugging` configuration and start debugging (F5)
- Select `server: debug` configuration and start debugging (F5)
You have done! Now it is possible to insert breakpoints and debug server and client of the tool.

@ -26,7 +26,7 @@
"eslint:recommended",
"plugin:security/recommended",
"plugin:no-unsanitized/DOM",
"airbnb",
"airbnb-base",
],
"rules": {
"no-await-in-loop": [0],

@ -2,3 +2,5 @@ docs
node_modules
reports
package-lock.json
yarn.lock
dist

@ -1,5 +1,5 @@
{
"name": "cvat.js",
"name": "cvat-core.js",
"version": "0.1.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
@ -15,11 +15,17 @@
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"airbnb": "0.0.2",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6",
"core-js": "^3.0.1",
"coveralls": "^3.0.5",
"eslint": "^6.1.0",
"eslint": "6.1.0",
"eslint-config-airbnb-base": "14.0.0",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-no-unsafe-innerhtml": "^1.0.16",
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-security": "^1.4.0",
"jest": "^24.8.0",
"jest-junit": "^6.4.0",
"jsdoc": "^3.6.2",
@ -33,6 +39,7 @@
"form-data": "^2.5.0",
"jest-config": "^24.8.0",
"js-cookie": "^2.2.0",
"platform": "^1.3.5"
"platform": "^1.3.5",
"store": "^2.0.12"
}
}

@ -50,6 +50,12 @@
return result.map(el => new AnnotationFormat(el));
};
cvat.server.register.implementation = async (username, firstName, lastName,
email, password1, password2) => {
await serverProxy.server.register(username, firstName, lastName, email,
password1, password2);
};
cvat.server.login.implementation = async (username, password) => {
await serverProxy.server.login(username, password);
};

@ -115,6 +115,26 @@ function build() {
.apiWrapper(cvat.server.formats);
return result;
},
/**
* Method allows to register on a server
* @method register
* @async
* @memberof module:API.cvat.server
* @param {string} username An username for the new account
* @param {string} firstName A first name for the new account
* @param {string} lastName A last name for the new account
* @param {string} email A email address for the new account
* @param {string} password1 A password for the new account
* @param {string} password2 The confirmation password for the new account
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async register(username, firstName, lastName, email, password1, password2) {
const result = await PluginRegistry
.apiWrapper(cvat.server.register, username, firstName,
lastName, email, password1, password2);
return result;
},
/**
* Method allows to login on a server
* @method login
@ -122,7 +142,6 @@ function build() {
* @memberof module:API.cvat.server
* @param {string} username An username of an account
* @param {string} password A password of an account
* @throws {module:API.cvat.exceptions.ScriptingError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/

@ -12,24 +12,20 @@
const FormData = require('form-data');
const {
ServerError,
ScriptingError,
} = require('./exceptions');
const store = require('store');
const config = require('./config');
class ServerProxy {
constructor() {
const Cookie = require('js-cookie');
const Axios = require('axios');
Axios.defaults.withCredentials = true;
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
Axios.defaults.xsrfCookieName = 'csrftoken';
function setCSRFHeader(header) {
Axios.defaults.headers.delete['X-CSRFToken'] = header;
Axios.defaults.headers.patch['X-CSRFToken'] = header;
Axios.defaults.headers.post['X-CSRFToken'] = header;
Axios.defaults.headers.put['X-CSRFToken'] = header;
// Allows to move authentification headers to backend
Axios.defaults.withCredentials = true;
let token = store.get('token');
if (token) {
Axios.defaults.headers.common.Authorization = `Token ${token}`;
}
async function about() {
@ -109,108 +105,69 @@
return response.data;
}
async function login(username, password) {
function setCookie(response) {
if (response.headers['set-cookie']) {
// Browser itself setup cookie and header is none
// In NodeJS we need do it manually
let cookies = '';
for (let cookie of response.headers['set-cookie']) {
[cookie] = cookie.split(';'); // truncate extra information
const name = cookie.split('=')[0];
const value = cookie.split('=')[1];
if (name === 'csrftoken') {
setCSRFHeader(value);
}
Cookie.set(name, value);
cookies += `${cookie};`;
}
Axios.defaults.headers.common.Cookie = cookies;
} else {
// Browser code. We need set additinal header for authentification
let csrftoken = response.data.csrf;
if (csrftoken) {
setCSRFHeader(csrftoken);
Cookie.set('csrftoken', csrftoken);
} else {
csrftoken = Cookie.get('csrftoken');
if (csrftoken) {
setCSRFHeader(csrftoken);
} else {
throw new ScriptingError(
'An environment has been detected as a browser'
+ ', but CSRF token has not been found in cookies',
);
}
}
}
}
const host = config.backendAPI.slice(0, -7);
let csrf = null;
async function register(username, firstName, lastName, email, password1, password2) {
let response = null;
try {
csrf = await Axios.get(`${host}/auth/csrf`, {
const data = JSON.stringify({
username,
first_name: firstName,
last_name: lastName,
email,
password1,
password2,
});
response = await Axios.post(`${config.backendAPI}/auth/register`, data, {
proxy: config.proxy,
});
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new ServerError(
'Could not get CSRF token from a server',
`Could not register '${username}' user on the server`,
code,
);
}
setCookie(csrf);
return response.data;
}
const authentificationData = ([
async function login(username, password) {
const authenticationData = ([
`${encodeURIComponent('username')}=${encodeURIComponent(username)}`,
`${encodeURIComponent('password')}=${encodeURIComponent(password)}`,
]).join('&').replace(/%20/g, '+');
let authentificationResponse = null;
let authenticationResponse = null;
try {
authentificationResponse = await Axios.post(
`${host}/auth/login`,
authentificationData,
{
'Content-Type': 'application/x-www-form-urlencoded',
authenticationResponse = await Axios.post(
`${config.backendAPI}/auth/login`,
authenticationData, {
proxy: config.proxy,
// do not redirect to a dashboard,
// otherwise we don't get a session id in a response
maxRedirects: 0,
},
);
} catch (errorData) {
if (errorData.response.status === 302) {
// Redirection code expected
authentificationResponse = errorData.response;
} else {
const code = errorData.response
? errorData.response.status : errorData.code;
throw new ServerError(
'Could not login on a server',
code,
);
}
}
// TODO: Perhaps we should redesign the authorization method on the server.
if (authentificationResponse.data.includes('didn\'t match')) {
const code = errorData.response
? errorData.response.status : errorData.code;
throw new ServerError(
'The pair login/password is invalid',
403,
'Could not login on a server',
code,
);
}
setCookie(authentificationResponse);
if (authenticationResponse.headers['set-cookie']) {
// Browser itself setup cookie and header is none
// In NodeJS we need do it manually
const cookies = authenticationResponse.headers['set-cookie'].join(';');
Axios.defaults.headers.common.Cookie = cookies;
}
token = authenticationResponse.data.key;
store.set('token', token);
Axios.defaults.headers.common.Authorization = `Token ${token}`;
}
async function logout() {
const host = config.backendAPI.slice(0, -7);
try {
await Axios.get(`${host}/auth/logout`, {
await Axios.post(`${config.backendAPI}/auth/logout`, {
proxy: config.proxy,
});
} catch (errorData) {
@ -220,13 +177,16 @@
code,
);
}
store.remove('token');
Axios.defaults.headers.common.Authorization = '';
}
async function authorized() {
try {
await module.exports.users.getSelf();
} catch (serverError) {
if (serverError.code === 403) {
if (serverError.code === 401) {
return false;
}
@ -315,7 +275,7 @@
// If server has another status, it is unexpected
// Therefore it is server error and we can pass code 500
reject(new ServerError(
`Unknown task state has been recieved: ${response.data.state}`,
`Unknown task state has been received: ${response.data.state}`,
500,
));
}
@ -324,7 +284,7 @@
? errorData.response.status : errorData.code;
reject(new ServerError(
'Data uploading error occured',
'Data uploading error occurred',
code,
));
}
@ -623,14 +583,6 @@
});
}
// Set csrftoken header from browser cookies if it exists
// NodeJS env returns 'undefined'
// So in NodeJS we need login after each run
const csrftoken = Cookie.get('csrftoken');
if (csrftoken) {
setCSRFHeader(csrftoken);
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
@ -641,6 +593,7 @@
login,
logout,
authorized,
register,
}),
writable: false,
},

@ -37,7 +37,7 @@ const webConfig = {
entry: './src/api.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-core.js',
filename: 'cvat-core.min.js',
library: 'cvat',
libraryTarget: 'window',
},

@ -8,3 +8,5 @@ REACT_APP_API_FULL_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT
REACT_APP_LOGIN=admin
REACT_APP_PASSWORD=admin
SKIP_PREFLIGHT_CHECK=true

File diff suppressed because one or more lines are too long

@ -64,7 +64,7 @@ class LoginForm extends PureComponent<any, any> {
);
}
private onSubmit = (event: React.FormEvent<HTMLInputElement>) => {
private onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
this.props.form.validateFields((error: any, values: any) => {

@ -170,7 +170,7 @@ class RegisterForm extends PureComponent<any, any> {
callback();
};
private onSubmit = (event: React.FormEvent<HTMLInputElement>) => {
private onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
this.props.form.validateFields((error: any, values: any) => {

@ -0,0 +1,26 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.urls import path
from django.conf import settings
from rest_auth.views import (
LoginView, LogoutView, PasswordChangeView,
PasswordResetView, PasswordResetConfirmView)
from rest_auth.registration.views import RegisterView
urlpatterns = [
path('login', LoginView.as_view(), name='rest_login'),
path('logout', LogoutView.as_view(), name='rest_logout'),
]
if settings.DJANGO_AUTH_TYPE == 'BASIC':
urlpatterns += [
path('register', RegisterView.as_view(), name='rest_register'),
path('password/reset', PasswordResetView.as_view(),
name='rest_password_reset'),
path('password/reset/confirm', PasswordResetConfirmView.as_view(),
name='rest_password_reset_confirm'),
path('password/change', PasswordChangeView.as_view(),
name='rest_password_change'),
]

@ -0,0 +1,16 @@
from rest_auth.registration.serializers import RegisterSerializer
from rest_framework import serializers
class RegisterSerializerEx(RegisterSerializer):
first_name = serializers.CharField(required=False)
last_name = serializers.CharField(required=False)
def get_cleaned_data(self):
data = super().get_cleaned_data()
data.update({
'first_name': self.validated_data.get('first_name', ''),
'last_name': self.validated_data.get('last_name', ''),
})
return data

@ -15,7 +15,6 @@ urlpatterns = [
template_name='login.html', extra_context={'note': settings.AUTH_LOGIN_NOTE}),
name='login'),
path('logout', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
path('csrf', views.get_csrf, name='csrf')
]
if settings.DJANGO_AUTH_TYPE == 'BASIC':

@ -1,13 +1,10 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.shortcuts import render, redirect
from django.conf import settings
from django.http import JsonResponse
from django.contrib.auth import login, authenticate
from django.middleware.csrf import get_token
from . import forms
@ -24,7 +21,3 @@ def register_user(request):
else:
form = forms.NewUserForm()
return render(request, 'register.html', {'form': form})
def get_csrf(request):
return JsonResponse({'csrf': get_token(request)})

@ -33,7 +33,7 @@
{% block head_js_cvat %}
{{ block.super }}
<script type="text/javascript" src="{% static 'engine/js/cvat.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/cvat-core.min.js' %}"></script>
<script type="text/javascript" src="{% static 'dashboard/js/dashboard.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/listener.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/labelsInfo.js' %}"></script>

File diff suppressed because one or more lines are too long

@ -196,9 +196,9 @@ class JobGetAPITestCase(APITestCase):
def test_api_v1_jobs_id_no_auth(self):
response = self._run_api_v1_jobs_id(self.job.id, None)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
response = self._run_api_v1_jobs_id(self.job.id + 10, None)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class JobUpdateAPITestCase(APITestCase):
@ -266,9 +266,9 @@ class JobUpdateAPITestCase(APITestCase):
def test_api_v1_jobs_id_no_auth(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, None, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
response = self._run_api_v1_jobs_id(self.job.id + 10, None, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class JobPartialUpdateAPITestCase(JobUpdateAPITestCase):
def _run_api_v1_jobs_id(self, jid, user, data):
@ -317,7 +317,7 @@ class ServerAboutAPITestCase(APITestCase):
def test_api_v1_server_about_no_auth(self):
response = self._run_api_v1_server_about(None)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class ServerExceptionAPITestCase(APITestCase):
def setUp(self):
@ -360,7 +360,7 @@ class ServerExceptionAPITestCase(APITestCase):
def test_api_v1_server_exception_no_auth(self):
response = self._run_api_v1_server_exception(None)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class ServerLogsAPITestCase(APITestCase):
@ -408,7 +408,7 @@ class ServerLogsAPITestCase(APITestCase):
def test_api_v1_server_logs_no_auth(self):
response = self._run_api_v1_server_logs(None)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class UserListAPITestCase(APITestCase):
@ -438,7 +438,7 @@ class UserListAPITestCase(APITestCase):
def test_api_v1_users_no_auth(self):
response = self._run_api_v1_users(None)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class UserSelfAPITestCase(APITestCase):
def setUp(self):
@ -473,7 +473,7 @@ class UserSelfAPITestCase(APITestCase):
def test_api_v1_users_self_no_auth(self):
response = self._run_api_v1_users_self(None)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class UserGetAPITestCase(APITestCase):
def setUp(self):
@ -520,7 +520,7 @@ class UserGetAPITestCase(APITestCase):
def test_api_v1_users_id_no_auth(self):
response = self._run_api_v1_users_id(None, self.user.id)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class UserUpdateAPITestCase(APITestCase):
def setUp(self):
@ -559,7 +559,7 @@ class UserUpdateAPITestCase(APITestCase):
data = {"username": "user12", "groups": ["user", "observer"],
"first_name": "my name"}
response = self._run_api_v1_users_id(None, self.user.id, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class UserPartialUpdateAPITestCase(UserUpdateAPITestCase):
def _run_api_v1_users_id(self, user, user_id, data):
@ -585,7 +585,7 @@ class UserPartialUpdateAPITestCase(UserUpdateAPITestCase):
def test_api_v1_users_id_no_auth_partial(self):
data = {"username": "user12"}
response = self._run_api_v1_users_id(None, self.user.id, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class TaskListAPITestCase(APITestCase):
def setUp(self):
@ -626,7 +626,7 @@ class TaskListAPITestCase(APITestCase):
def test_api_v1_tasks_no_auth(self):
response = self._run_api_v1_tasks(None)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class TaskGetAPITestCase(APITestCase):
def setUp(self):
@ -667,8 +667,10 @@ class TaskGetAPITestCase(APITestCase):
response = self._run_api_v1_tasks_id(db_task.id, user)
if user and user.has_perm("engine.task.access", db_task):
self._check_response(response, db_task)
else:
elif user:
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
else:
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_api_v1_tasks_id_admin(self):
self._check_api_v1_tasks_id(self.admin)
@ -702,8 +704,10 @@ class TaskDeleteAPITestCase(APITestCase):
response = self._run_api_v1_tasks_id(db_task.id, user)
if user and user.has_perm("engine.task.delete", db_task):
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
else:
elif user:
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
else:
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_api_v1_tasks_id_admin(self):
self._check_api_v1_tasks_id(self.admin)
@ -769,8 +773,10 @@ class TaskUpdateAPITestCase(APITestCase):
response = self._run_api_v1_tasks_id(db_task.id, user, data)
if user and user.has_perm("engine.task.change", db_task):
self._check_response(response, db_task, data)
else:
elif user:
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
else:
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_api_v1_tasks_id_admin(self):
data = {
@ -921,8 +927,10 @@ class TaskCreateAPITestCase(APITestCase):
response = self._run_api_v1_tasks(user, data)
if user and user.has_perm("engine.task.create"):
self._check_response(response, user, data)
else:
elif user:
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
else:
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_api_v1_tasks_admin(self):
data = {
@ -1126,7 +1134,7 @@ class TaskDataAPITestCase(APITestCase):
]
}
response = self._create_task(None, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def compare_objects(self, obj1, obj2, ignore_keys):
if isinstance(obj1, dict):
@ -1233,7 +1241,8 @@ class JobAnnotationAPITestCase(APITestCase):
return response
def _check_response(self, response, data):
if response.status_code != status.HTTP_403_FORBIDDEN:
if not response.status_code in [
status.HTTP_403_FORBIDDEN, status.HTTP_401_UNAUTHORIZED]:
compare_objects(self, data, response.data, ignore_keys=["id"])
def _run_api_v1_jobs_id_annotations(self, owner, assignee, annotator):
@ -1243,9 +1252,9 @@ class JobAnnotationAPITestCase(APITestCase):
HTTP_204_NO_CONTENT = status.HTTP_204_NO_CONTENT
HTTP_400_BAD_REQUEST = status.HTTP_400_BAD_REQUEST
else:
HTTP_200_OK = status.HTTP_403_FORBIDDEN
HTTP_204_NO_CONTENT = status.HTTP_403_FORBIDDEN
HTTP_400_BAD_REQUEST = status.HTTP_403_FORBIDDEN
HTTP_200_OK = status.HTTP_401_UNAUTHORIZED
HTTP_204_NO_CONTENT = status.HTTP_401_UNAUTHORIZED
HTTP_400_BAD_REQUEST = status.HTTP_401_UNAUTHORIZED
job = jobs[0]
data = {
@ -1473,7 +1482,8 @@ class JobAnnotationAPITestCase(APITestCase):
self._check_response(response, data)
data = response.data
if response.status_code != status.HTTP_403_FORBIDDEN:
if not response.status_code in [
status.HTTP_403_FORBIDDEN, status.HTTP_401_UNAUTHORIZED]:
data["tags"][0]["label_id"] = task["labels"][0]["id"]
data["shapes"][0]["points"] = [1, 2, 3.0, 100, 120, 1, 2, 4.0]
data["shapes"][0]["type"] = "polygon"
@ -1650,7 +1660,8 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
return response
def _check_response(self, response, data):
if response.status_code != status.HTTP_403_FORBIDDEN:
if not response.status_code in [
status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]:
compare_objects(self, data, response.data, ignore_keys=["id"])
def _run_api_v1_tasks_id_annotations(self, owner, assignee, annotator):
@ -1662,11 +1673,11 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
HTTP_202_ACCEPTED = status.HTTP_202_ACCEPTED
HTTP_201_CREATED = status.HTTP_201_CREATED
else:
HTTP_200_OK = status.HTTP_403_FORBIDDEN
HTTP_204_NO_CONTENT = status.HTTP_403_FORBIDDEN
HTTP_400_BAD_REQUEST = status.HTTP_403_FORBIDDEN
HTTP_202_ACCEPTED = status.HTTP_403_FORBIDDEN
HTTP_201_CREATED = status.HTTP_403_FORBIDDEN
HTTP_200_OK = status.HTTP_401_UNAUTHORIZED
HTTP_204_NO_CONTENT = status.HTTP_401_UNAUTHORIZED
HTTP_400_BAD_REQUEST = status.HTTP_401_UNAUTHORIZED
HTTP_202_ACCEPTED = status.HTTP_401_UNAUTHORIZED
HTTP_201_CREATED = status.HTTP_401_UNAUTHORIZED
data = {
"version": 0,
@ -1894,7 +1905,8 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
self._check_response(response, data)
data = response.data
if response.status_code != status.HTTP_403_FORBIDDEN:
if not response.status_code in [
status.HTTP_403_FORBIDDEN, status.HTTP_401_UNAUTHORIZED]:
data["tags"][0]["label_id"] = task["labels"][0]["id"]
data["shapes"][0]["points"] = [1, 2, 3.0, 100, 120, 1, 2, 4.0]
data["shapes"][0]["type"] = "polygon"
@ -2197,4 +2209,4 @@ class ServerShareAPITestCase(APITestCase):
def test_api_v1_server_share_no_auth(self):
response = self._run_api_v1_server_share(None, "/")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

@ -40,5 +40,6 @@ urlpatterns = [
path('api/docs/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
# entry point for API
path('api/v1/auth/', include('cvat.apps.authentication.api_urls')),
path('api/v1/', include((router.urls, 'cvat'), namespace='v1'))
]

@ -447,10 +447,7 @@ class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
def get_permissions(self):
permissions = [IsAuthenticated]
if self.action in ["self"]:
pass
else:
if not self.action in ["self"]:
user = self.request.user
if self.action != "retrieve" or int(self.kwargs.get("pk", 0)) != user.id:
permissions.append(auth.AdminRolePermission)

@ -1,5 +1,5 @@
click==6.7
Django==2.2.3
Django==2.2.4
django-appconf==1.0.2
django-auth-ldap==1.4.0
django-cacheops==4.0.6
@ -31,10 +31,11 @@ django-filter==2.0.0
Markdown==3.0.1
djangorestframework==3.9.1
Pygments==2.3.1
drf-yasg==1.15.0
drf-yasg==1.16.0
Shapely==1.6.4.post2
pdf2image==1.6.0
pascal_voc_writer==0.1.4
django-rest-auth[with_social]==0.9.5
cython==0.29.13
matplotlib==3.0.3
scikit-image>=0.14.0

@ -1,9 +1,9 @@
-r base.txt
astroid==1.6.2
astroid==2.2.5
isort==4.3.4
lazy-object-proxy==1.3.1
mccabe==0.6.1
pylint==1.8.3
pylint==2.3.1
pylint-django==0.9.4
pylint-plugin-utils==0.2.6
rope==0.11

@ -101,14 +101,27 @@ INSTALLED_APPS = [
'revproxy',
'rules',
'rest_framework',
'rest_framework.authtoken',
'django_filters',
'drf_yasg',
'rest_auth',
'django.contrib.sites',
'allauth',
'allauth.account',
'rest_auth.registration'
]
SITE_ID = 1
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
],
'DEFAULT_VERSIONING_CLASS':
# Don't try to use URLPathVersioning. It will give you /api/{version}
# in path and '/api/docs' will not collapse similar items (flat list
@ -128,6 +141,10 @@ REST_FRAMEWORK = {
'URL_FORMAT_OVERRIDE': None,
}
REST_AUTH_REGISTER_SERIALIZERS = {
'REGISTER_SERIALIZER': 'cvat.apps.authentication.serializers.RegisterSerializerEx'
}
if 'yes' == os.environ.get('TF_ANNOTATION', 'no'):
INSTALLED_APPS += ['cvat.apps.tf_annotation']
@ -191,6 +208,8 @@ AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend'
]
# https://github.com/pennersr/django-allauth
ACCOUNT_EMAIL_VERIFICATION = 'none'
# Django-RQ
# https://github.com/rq/django-rq
@ -274,6 +293,8 @@ USE_L10N = True
USE_TZ = True
CSRF_COOKIE_NAME = "csrftoken"
LOGGING = {
'version': 1,
'disable_existing_loggers': False,

Loading…
Cancel
Save