Skip to content

Commit

Permalink
cvat-ui in docker (serve using nginx) (#658)
Browse files Browse the repository at this point in the history
  • Loading branch information
nmanovic committed Sep 9, 2019
1 parent eaede02 commit 8359db3
Show file tree
Hide file tree
Showing 17 changed files with 191 additions and 22 deletions.
5 changes: 3 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
/.vscode
/db.sqlite3
/keys
package-lock.json
node_modules
**/node_modules
cvat-ui
cvat-canvas
2 changes: 2 additions & 0 deletions cvat-ui/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build
node_modules
3 changes: 0 additions & 3 deletions cvat-ui/.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,4 @@ REACT_APP_API_PORT=7000
REACT_APP_API_HOST_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}
REACT_APP_API_FULL_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}/api/v1

REACT_APP_LOGIN=admin
REACT_APP_PASSWORD=admin

SKIP_PREFLIGHT_CHECK=true
9 changes: 9 additions & 0 deletions cvat-ui/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
REACT_APP_VERSION=${npm_package_version}

REACT_APP_API_PROTOCOL=http
REACT_APP_API_HOST=localhost
REACT_APP_API_PORT=8080
REACT_APP_API_HOST_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}
REACT_APP_API_FULL_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}/api/v1

SKIP_PREFLIGHT_CHECK=true
36 changes: 36 additions & 0 deletions cvat-ui/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
FROM ubuntu:18.04 AS cvat-ui

ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy

ENV TERM=xterm \
http_proxy=${http_proxy} \
https_proxy=${https_proxy} \
no_proxy=${no_proxy} \
socks_proxy=${socks_proxy}

ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8'

# Install necessary apt packages
RUN apt update && apt install -yq nodejs npm curl && \
npm install -g n && n 10.16.3

# Create output directory
RUN mkdir /tmp/cvat-ui
WORKDIR /tmp/cvat-ui/

# Install dependencies
COPY package*.json /tmp/cvat-ui/
RUN npm install

# Build source code
COPY . /tmp/cvat-ui/
RUN mv .env.production .env && npm run build

FROM nginx
# Replace default.conf configuration to remove unnecessary rules
COPY react_nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=cvat-ui /tmp/cvat-ui/build /usr/share/nginx/html/
3 changes: 2 additions & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"react-redux": "^7.1.0",
"react-router-dom": "^5.0.1",
"react-scripts": "3.0.1",
"redux": "^4.0.3",
"react-scripts-ts": "^3.1.0",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"source-map-explorer": "^1.8.0",
Expand Down
7 changes: 7 additions & 0 deletions cvat-ui/react_nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
server {
root /usr/share/nginx/html;
# Any route that doesn't have a file extension (e.g. /devices)
location / {
try_files $uri $uri/ /index.html;
}
}
4 changes: 4 additions & 0 deletions cvat-ui/src/components/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const ProtectedRoute = ({ component: Component, ...rest }: any) => {
};

class App extends PureComponent<any, any> {
componentDidMount() {
(window as any).cvat.config.backendAPI = process.env.REACT_APP_API_FULL_URL;
}

render() {
return(
<Router>
Expand Down
2 changes: 2 additions & 0 deletions cvat/apps/authentication/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
LoginView, LogoutView, PasswordChangeView,
PasswordResetView, PasswordResetConfirmView)
from rest_auth.registration.views import RegisterView
from .views import SigningView

urlpatterns = [
path('login', LoginView.as_view(), name='rest_login'),
path('logout', LogoutView.as_view(), name='rest_logout'),
path('signing', SigningView.as_view(), name='signing')
]

if settings.DJANGO_AUTH_TYPE == 'BASIC':
Expand Down
27 changes: 27 additions & 0 deletions cvat/apps/authentication/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
from django.db.models import Q
import rules
from . import AUTH_ROLE
from . import signature
from rest_framework.permissions import BasePermission
from django.core import signing
from rest_framework import authentication, exceptions

def register_signals():
from django.db.models.signals import post_migrate, post_save
Expand All @@ -30,6 +33,30 @@ def create_groups(sender, **kwargs):

django_auth_ldap.backend.populate_user.connect(create_user)

class SignatureAuthentication(authentication.BaseAuthentication):
"""
Authentication backend for signed URLs.
"""
def authenticate(self, request):
"""
Returns authenticated user if URL signature is valid.
"""
signer = signature.Signer()
sign = request.query_params.get(signature.QUERY_PARAM)
if not sign:
return

try:
user = signer.unsign(sign, request.build_absolute_uri())
except signing.SignatureExpired:
raise exceptions.AuthenticationFailed('This URL has expired.')
except signing.BadSignature:
raise exceptions.AuthenticationFailed('Invalid signature.')
if not user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted.')

return (user, None)

# AUTH PREDICATES
has_admin_role = rules.is_group_member(str(AUTH_ROLE.ADMIN))
has_user_role = rules.is_group_member(str(AUTH_ROLE.USER))
Expand Down
44 changes: 44 additions & 0 deletions cvat/apps/authentication/signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from django.contrib.auth import get_user_model
from django.core import signing
from furl import furl
import hashlib

QUERY_PARAM = 'sign'
MAX_AGE = 30

# Got implementation ideas in https://github.com/marcgibbons/drf_signed_auth
class Signer:
@classmethod
def get_salt(cls, url):
normalized_url = furl(url).remove(QUERY_PARAM).url.encode('utf-8')
salt = hashlib.sha256(normalized_url).hexdigest()
return salt

def sign(self, user, url):
"""
Create a signature for a user object.
"""
data = {
'user_id': user.pk,
'username': user.get_username()
}

return signing.dumps(data, salt=self.get_salt(url))

def unsign(self, signature, url):
"""
Return a user object for a valid signature.
"""
User = get_user_model()
data = signing.loads(signature, salt=self.get_salt(url), max_age=MAX_AGE)

if not isinstance(data, dict):
raise signing.BadSignature()

try:
return User.objects.get(**{
'pk': data.get('user_id'),
User.USERNAME_FIELD: data.get('username')
})
except User.DoesNotExist:
raise signing.BadSignature()
18 changes: 18 additions & 0 deletions cvat/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
from django.shortcuts import render, redirect
from django.conf import settings
from django.contrib.auth import login, authenticate
from rest_framework import views
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from furl import furl

from . import forms
from . import signature

def register_user(request):
if request.method == 'POST':
Expand All @@ -21,3 +26,16 @@ def register_user(request):
else:
form = forms.NewUserForm()
return render(request, 'register.html', {'form': form})

class SigningView(views.APIView):
def post(self, request):
url = request.data.get('url')
if not url:
raise ValidationError('Please provide `url` parameter')

signer = signature.Signer()
url = self.request.build_absolute_uri(url)
sign = signer.sign(self.request.user, url)

url = furl(url).add({signature.QUERY_PARAM: sign}).url
return Response(url)
2 changes: 2 additions & 0 deletions cvat/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ cython==0.29.13
matplotlib==3.0.3
scikit-image>=0.14.0
tensorflow==1.12.3
django-cors-headers==3.0.2
furl==2.0.0
3 changes: 1 addition & 2 deletions cvat/requirements/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@ pylint-plugin-utils==0.2.6
rope==0.11
wrapt==1.10.11
django-extensions==2.0.6
Werkzeug==0.14.1
Werkzeug==0.15.3
snakeviz==0.4.2
django-cors-headers==3.0.2
12 changes: 12 additions & 0 deletions cvat/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def generate_ssh_keys():
'django.contrib.sites',
'allauth',
'allauth.account',
'corsheaders',
'allauth.socialaccount',
'rest_auth.registration'
]
Expand All @@ -123,6 +124,7 @@ def generate_ssh_keys():
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'cvat.apps.authentication.auth.SignatureAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
],
Expand Down Expand Up @@ -173,8 +175,18 @@ def generate_ssh_keys():
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'dj_pagination.middleware.PaginationMiddleware',
'corsheaders.middleware.CorsMiddleware',
]

# Cross-Origin Resource Sharing settings for CVAT UI
UI_SCHEME = os.environ.get('UI_SCHEME', 'http')
UI_HOST = os.environ.get('UI_HOST', 'localhost')
UI_PORT = os.environ.get('UI_PORT', '3000')
CORS_ALLOW_CREDENTIALS = True
CSRF_TRUSTED_ORIGINS = [UI_HOST]
UI_URL = '{}://{}:{}'.format(UI_SCHEME, UI_HOST, UI_PORT)
CORS_ORIGIN_WHITELIST = [UI_URL]

STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
Expand Down
14 changes: 0 additions & 14 deletions cvat/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,6 @@

INSTALLED_APPS += [
'django_extensions',
'corsheaders',
]

MIDDLEWARE += [
'corsheaders.middleware.CorsMiddleware',
]

CORS_ALLOW_CREDENTIALS = True

CSRF_TRUSTED_ORIGINS = [
'http://localhost:3000'
]
CORS_ORIGIN_WHITELIST = [
"http://localhost:3000",
]

ALLOWED_HOSTS.append('testserver')
Expand Down
22 changes: 22 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,34 @@ services:
OPENVINO_TOOLKIT: "no"
environment:
DJANGO_MODWSGI_EXTRA_ARGS: ""
UI_PORT: 9080

volumes:
- cvat_data:/home/django/data
- cvat_keys:/home/django/keys
- cvat_logs:/home/django/logs
- cvat_models:/home/django/models

cvat_ui:
container_name: cvat_ui
image: nginx
build:
context: cvat-ui
args:
http_proxy:
https_proxy:
no_proxy:
socks_proxy:
dockerfile: Dockerfile
networks:
default:
aliases:
- ui
depends_on:
- cvat
ports:
- "9080:80"

volumes:
cvat_db:
cvat_data:
Expand Down

0 comments on commit 8359db3

Please sign in to comment.