Skip to content

Commit

Permalink
Merge branch 'core-auth' of 'https://github.com/jjmerchante/grimoirel…
Browse files Browse the repository at this point in the history
…ab-core'

Merges #31
Closes #31
  • Loading branch information
sduenas authored Feb 11, 2025
2 parents ad300c8 + 0de7b40 commit 9783641
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 29 deletions.
27 changes: 26 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ grimoirelab-chronicler = {git = "https://github.com/chaoss/grimoirelab-chronicle
django-cors-headers = "^4.6.0"
djangorestframework = "^3.15.2"
opensearch-py = "^2.8.0"
djangorestframework-simplejwt = "^5.4.0"

[tool.poetry.group.dev.dependencies]
fakeredis = "^2.0.0"
Expand Down
9 changes: 9 additions & 0 deletions src/grimoirelab/core/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@
from django.urls import path, include, re_path
from django.views.generic import TemplateView

from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from ..views import api_login

from grimoirelab.core.scheduler.urls import urlpatterns as sched_urlpatterns
from grimoirelab.core.datasources.urls import urlpatterns as datasources_urlpatterns

urlpatterns = [
path("login", api_login, name="api_login"),
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("scheduler/", include(sched_urlpatterns)),
path("datasources/", include(datasources_urlpatterns)),
re_path(r'^(?!static|scheduler).*$', TemplateView.as_view(template_name="index.html"))
Expand Down
16 changes: 15 additions & 1 deletion src/grimoirelab/core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@

SECRET_KEY = os.environ.get('GRIMOIRELAB_SECRET_KEY', 'fake-key')


# Require authentication when using the API.
# You shouldn't deactivate this option unless you are debugging
# the system or running it in a trusted and safe environment.
GRIMOIRELAB_AUTHENTICATION_REQUIRED = True


#
# Application definition - DO NOT MODIFY
#
Expand Down Expand Up @@ -257,7 +264,14 @@
'rest_framework.renderers.JSONRenderer',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 100
'PAGE_SIZE': 100,
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'grimoirelab.core.permissions.IsAuthenticated',
],
}

#
Expand Down
21 changes: 7 additions & 14 deletions src/grimoirelab/core/datasources/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,17 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import json
from rest_framework.response import Response
from rest_framework.decorators import api_view

from django.conf import settings
from django.db import IntegrityError
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from .models import Repository
from grimoirelab.core.scheduler.scheduler import schedule_task


@require_http_methods(["POST"])
@csrf_exempt
@api_view(['POST'])
def add_repository(request):
"""Create a Repository and start a Task to fetch items
Expand All @@ -44,10 +40,7 @@ def add_repository(request):
}
}
"""
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON format."}, status=400)
data = request.data

# Get POST data
job_interval = settings.GRIMOIRELAB_JOB_INTERVAL
Expand All @@ -60,15 +53,15 @@ def add_repository(request):
datasource_type = data.get('datasource_type')
datasource_category = data.get('datasource_category')
if not uri or not datasource_type or not datasource_category:
return JsonResponse({"error": "Missing parameters"}, status=400)
return Response({"error": "Missing parameters"}, status=400)

# Create the task and the repository
try:
repository = Repository.objects.create(uri=uri,
datasource_type=datasource_type,
datasource_category=datasource_category)
except IntegrityError:
return JsonResponse({"error": "Repository already exists"}, status=405)
return Response({"error": "Repository already exists"}, status=405)

task_args = {
'uri': data['uri']
Expand All @@ -88,4 +81,4 @@ def add_repository(request):
'task_id': repository.task.uuid,
'message': f"Repository {uri} added correctly"
}
return JsonResponse(response, safe=False)
return Response(response)
32 changes: 32 additions & 0 deletions src/grimoirelab/core/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) GrimoireLab Contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

from django.conf import settings
from rest_framework.permissions import BasePermission


class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
When `GRIMOIRELAB_AUTHENTICATION_REQUIRED` setting is False it always has permissions.
"""
def has_permission(self, request, view):
if not settings.GRIMOIRELAB_AUTHENTICATION_REQUIRED:
return True

return bool(request.user and request.user.is_authenticated)
73 changes: 73 additions & 0 deletions src/grimoirelab/core/runner/commands/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@

from __future__ import annotations

import getpass
import os
import sys
import typing

import click
import django.core
import django_rq

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import IntegrityError

if typing.TYPE_CHECKING:
from click import Context

Expand Down Expand Up @@ -113,6 +120,72 @@ def _install_static_files():
click.echo()


@admin.command()
@click.option('--username', help="Specifies the login for the user.")
@click.option('--is-admin', is_flag=True, default=False,
help="Specifies if the user is superuser.")
@click.option('--no-interactive', is_flag=True, default=False,
help="Run the command in no interactive mode.")
def create_user(username, is_admin, no_interactive):
"""Create a new user given a username and password"""

try:
if no_interactive:
# Use password from environment variable, if provided.
password = os.environ.get('GRIMOIRELAB_USER_PASSWORD')
if not password or not password.strip():
raise click.ClickException("Password cannot be empty.")
# Use username from environment variable, if not provided in options.
if username is None:
username = os.environ.get('GRIMOIRELAB_USER_USERNAME')
error = _validate_username(username)
if error:
click.ClickException(error)
else:
# Get username
if username is None:
username = input("Username: ")
error = _validate_username(username)
if error:
click.ClickException(error)
# Prompt for a password
password = getpass.getpass()
password2 = getpass.getpass('Password (again): ')
if password != password2:
raise click.ClickException("Error: Your passwords didn't match.")
if password.strip() == '':
raise click.ClickException("Error: Blank passwords aren't allowed.")

extra_fields = {}
if is_admin:
extra_fields['is_staff'] = True
extra_fields['is_superuser'] = True

get_user_model().objects.create_user(username=username,
password=password,
**extra_fields)

click.echo("User created successfully.")
except KeyboardInterrupt:
click.echo("\nOperation cancelled.")
sys.exit(1)
except IntegrityError:
click.echo(f"User '{username}' already exists.")
sys.exit(1)


def _validate_username(username):
"""Check if the username is valid and return the error"""

if not username:
return "Username cannot be empty."
username_field = get_user_model()._meta.get_field(get_user_model().USERNAME_FIELD)
try:
username_field.clean(username, None)
except ValidationError as e:
return '; '.join(e.messages)


@admin.group()
@click.pass_context
def queues(ctx: Context):
Expand Down
17 changes: 5 additions & 12 deletions src/grimoirelab/core/scheduler/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import json

from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from .scheduler import (
schedule_task
)


@require_http_methods(["POST"])
@csrf_exempt
@api_view(['POST'])
def add_task(request):
"""Create a Task to fetch items
Expand All @@ -49,10 +45,7 @@ def add_task(request):
}
}
"""
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON format."}, status=400)
data = request.data

task_type = data['type']

Expand All @@ -77,4 +70,4 @@ def add_task(request):
'status': 'ok',
'message': f"Task {task.id} added correctly"
}
return JsonResponse(response, safe=False)
return Response(response, status=200)
50 changes: 50 additions & 0 deletions src/grimoirelab/core/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) GrimoireLab Contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

from django.contrib.auth import authenticate, login
from rest_framework import permissions
from rest_framework.decorators import (
api_view,
permission_classes,
)
from rest_framework.response import Response


@api_view(['POST'])
@permission_classes([permissions.AllowAny])
def api_login(request):
username = request.data.get('username')
password = request.data.get('password')

if username is None or password is None:
return Response({'detail': 'Please provide username and password.'}, status=400)

user = authenticate(request, username=username, password=password)

if user is None:
response = {
'errors': 'Invalid credentials.'
}
return Response(response, status=403)
else:
login(request, user)
response = {
'user': username,
'isAdmin': user.is_superuser,
}
return Response(response)
Loading

0 comments on commit 9783641

Please sign in to comment.