Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64150da81a | |||
| 6c0acd0596 | |||
| 625e98515b | |||
| 297007162a | |||
| 8fa947f24d | |||
| ee580ad258 | |||
| 155043e0eb | |||
| df03534fb1 | |||
| 714d8188f5 | |||
| 65b36c6116 | |||
| 52d8969602 |
@@ -35,10 +35,20 @@ jobs:
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||
-
|
||||
name: Build and push
|
||||
name: Build and push backend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
target: production
|
||||
push: true
|
||||
tags: app-${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
-
|
||||
name: Build and push frontend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
target: frontend
|
||||
push: true
|
||||
tags: frontend-${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
+35
-1
@@ -11,6 +11,40 @@ RUN apt-get update && \
|
||||
apt-get -y upgrade && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
### ---- Front-end builder image ----
|
||||
FROM node:20 as front-builder
|
||||
|
||||
# Copy frontend app sources
|
||||
COPY ./src/frontend /builder
|
||||
|
||||
WORKDIR /builder/apps/desk
|
||||
|
||||
RUN yarn install --frozen-lockfile && \
|
||||
yarn build
|
||||
|
||||
|
||||
# ---- Front-end image ----
|
||||
FROM nginxinc/nginx-unprivileged:1.25 as frontend
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
COPY --from=front-builder \
|
||||
/builder/apps/desk/out \
|
||||
/usr/share/nginx/html
|
||||
|
||||
COPY ./docker/files/etc/nginx/conf.d /etc/nginx/conf.d:ro
|
||||
|
||||
# Copy entrypoint
|
||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
|
||||
# ---- Back-end builder image ----
|
||||
FROM base as back-builder
|
||||
|
||||
@@ -24,7 +58,7 @@ RUN mkdir /install && \
|
||||
|
||||
|
||||
# ---- mails ----
|
||||
FROM node:18 as mail-builder
|
||||
FROM node:20 as mail-builder
|
||||
|
||||
COPY ./src/mail /mail/app
|
||||
|
||||
|
||||
@@ -92,8 +92,10 @@ bootstrap: \
|
||||
.PHONY: bootstrap
|
||||
|
||||
# -- Docker/compose
|
||||
build: ## build the app-dev container
|
||||
build: ## build the dev and demo containers
|
||||
@$(COMPOSE) build app-dev
|
||||
@$(COMPOSE) build nginx
|
||||
@$(COMPOSE) build app
|
||||
.PHONY: build
|
||||
|
||||
down: ## stop and remove containers, networks, images, and volumes
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
People is an application to handle users and teams.
|
||||
|
||||
This project is as of yet **not ready for production**. Expect breaking changes.
|
||||
As of today, this project is **not yet ready for production**. Expect breaking changes.
|
||||
|
||||
People is built on top of [Django Rest
|
||||
Framework](https://www.django-rest-framework.org/).
|
||||
|
||||
+17
-6
@@ -66,10 +66,13 @@ services:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: people:production
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=ContinuousIntegration
|
||||
- DJANGO_CONFIGURATION=Demo
|
||||
- DJANGO_CORS_ALLOWED_ORIGINS=http://localhost:8088
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/postgresql
|
||||
ports:
|
||||
- "8082:8000"
|
||||
volumes:
|
||||
- ./data/media:/data/media
|
||||
depends_on:
|
||||
@@ -81,7 +84,7 @@ services:
|
||||
image: people:production
|
||||
command: ["celery", "-A", "people.celery_app", "worker", "-l", "INFO"]
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=ContinuousIntegration
|
||||
- DJANGO_CONFIGURATION=Demo
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/postgresql
|
||||
@@ -89,16 +92,18 @@ services:
|
||||
- app
|
||||
|
||||
nginx:
|
||||
image: nginx:1.25
|
||||
build:
|
||||
context: .
|
||||
target: frontend
|
||||
image: people:frontend
|
||||
ports:
|
||||
- "8082:8082"
|
||||
- "8088:8088"
|
||||
- "8083:8083"
|
||||
volumes:
|
||||
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./src/frontend/apps/desk/out:/home/desk
|
||||
- ./data/media:/data/media:ro
|
||||
depends_on:
|
||||
- app
|
||||
- keycloak
|
||||
|
||||
dockerize:
|
||||
image: jwilder/dockerize
|
||||
@@ -152,6 +157,11 @@ services:
|
||||
- start-dev
|
||||
- --features=preview
|
||||
- --import-realm
|
||||
- --proxy=edge
|
||||
- --hostname-url=http://localhost:8083
|
||||
- --hostname-admin-url=http://localhost:8083/
|
||||
- --hostname-strict=false
|
||||
- --hostname-strict-https=false
|
||||
environment:
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||
@@ -161,6 +171,7 @@ services:
|
||||
KC_DB_PASSWORD: pass
|
||||
KC_DB_USERNAME: people
|
||||
KC_DB_SCHEMA: public
|
||||
PROXY_ADDRESS_FORWARDING: true
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
|
||||
+32
-12
@@ -313,7 +313,6 @@
|
||||
],
|
||||
"security-admin-console": [],
|
||||
"admin-cli": [],
|
||||
"people-front": [],
|
||||
"account-console": [],
|
||||
"broker": [
|
||||
{
|
||||
@@ -326,6 +325,7 @@
|
||||
"attributes": {}
|
||||
}
|
||||
],
|
||||
"people": [],
|
||||
"account": [
|
||||
{
|
||||
"id": "63b1a4e1-a594-4571-99c3-7c5c3efd61ce",
|
||||
@@ -580,7 +580,9 @@
|
||||
"publicClient": true,
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {},
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
@@ -618,7 +620,9 @@
|
||||
"publicClient": false,
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {},
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
@@ -638,7 +642,7 @@
|
||||
},
|
||||
{
|
||||
"id": "869481d0-5774-4e64-bc30-fedc7c58958f",
|
||||
"clientId": "people-front",
|
||||
"clientId": "people",
|
||||
"name": "",
|
||||
"description": "",
|
||||
"rootUrl": "",
|
||||
@@ -648,9 +652,10 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"secret": "ThisIsAnExampleKeyForDevPurposeOnly",
|
||||
"redirectUris": [
|
||||
"",
|
||||
"http://localhost:8070/*",
|
||||
"http://localhost:8071/*",
|
||||
"http://localhost:3200/*",
|
||||
"http://localhost:8088/*",
|
||||
"http://localhost:3000/*"
|
||||
@@ -666,18 +671,29 @@
|
||||
"consentRequired": false,
|
||||
"standardFlowEnabled": true,
|
||||
"implicitFlowEnabled": false,
|
||||
"directAccessGrantsEnabled": true,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"serviceAccountsEnabled": false,
|
||||
"publicClient": true,
|
||||
"publicClient": false,
|
||||
"frontchannelLogout": true,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"access.token.lifespan": "-1",
|
||||
"client.secret.creation.time": "1707820779",
|
||||
"user.info.response.signature.alg": "RS256",
|
||||
"post.logout.redirect.uris": "http://localhost:8070/*##http://localhost:3200/*##http://localhost:3000/*",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"use.jwks.url": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false",
|
||||
"use.refresh.tokens": "true",
|
||||
"tls-client-certificate-bound-access-tokens": "false",
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"post.logout.redirect.uris": "http://localhost:8070/*##http://localhost:3200/*##http://localhost:3000/*",
|
||||
"client_credentials.use_refresh_token": "false",
|
||||
"acr.loa.map": "{}",
|
||||
"require.pushed.authorization.requests": "false",
|
||||
"display.on.consent.screen": "false",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
"client.session.idle.timeout": "-1",
|
||||
"token.response.type.bearer.lower-case": "false"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": true,
|
||||
@@ -716,7 +732,9 @@
|
||||
"publicClient": false,
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {},
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
@@ -887,7 +905,8 @@
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true"
|
||||
"access.token.claim": "true",
|
||||
"userinfo.token.claim": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1207,6 +1226,7 @@
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"multivalued": "true",
|
||||
"userinfo.token.claim": "true",
|
||||
"user.attribute": "foo",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
|
||||
@@ -1,27 +1,9 @@
|
||||
server {
|
||||
|
||||
listen 8082;
|
||||
server_name localhost;
|
||||
charset utf-8;
|
||||
|
||||
location /media {
|
||||
alias /data/media;
|
||||
}
|
||||
|
||||
# location / {
|
||||
# proxy_pass http://app:8000;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8088;
|
||||
server_name localhost;
|
||||
|
||||
root /home/desk;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location / {
|
||||
try_files $uri index.html $uri/ =404;
|
||||
}
|
||||
@@ -32,3 +14,17 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
server {
|
||||
listen 8083;
|
||||
server_name localhost;
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
proxy_pass http://keycloak:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@ DJANGO_SUPERUSER_PASSWORD=admin
|
||||
# Python
|
||||
PYTHONPATH=/app
|
||||
|
||||
#JWT
|
||||
DJANGO_JWT_PRIVATE_SIGNING_KEY=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
|
||||
# People settings
|
||||
|
||||
# Mail
|
||||
@@ -19,5 +16,20 @@ DJANGO_EMAIL_PORT=1025
|
||||
# Backend url
|
||||
PEOPLE_BASE_URL="http://localhost:8072"
|
||||
|
||||
# Keycloak
|
||||
SIMPLE_JWT_JWK_URL="http://keycloak:8080/realms/people/protocol/openid-connect/certs"
|
||||
# OIDC
|
||||
OIDC_OP_JWKS_ENDPOINT=http://nginx:8083/realms/people/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8083/realms/people/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT=http://nginx:8083/realms/people/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT=http://nginx:8083/realms/people/protocol/openid-connect/userinfo
|
||||
|
||||
OIDC_RP_CLIENT_ID=people
|
||||
OIDC_RP_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_RP_SIGN_ALGO=RS256
|
||||
OIDC_RP_SCOPES="openid email"
|
||||
|
||||
LOGIN_REDIRECT_URL=http://localhost:3000
|
||||
LOGIN_REDIRECT_URL_FAILURE=http://localhost:3000
|
||||
LOGOUT_REDIRECT_URL=http://localhost:3000
|
||||
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
|
||||
|
||||
@@ -1,59 +1,111 @@
|
||||
"""Authentication for the People core app."""
|
||||
from django.conf import settings
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from drf_spectacular.authentication import SessionScheme, TokenScheme
|
||||
from drf_spectacular.plumbing import build_bearer_security_scheme_object
|
||||
from rest_framework import authentication
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
import requests
|
||||
from mozilla_django_oidc.auth import (
|
||||
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
||||
)
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken
|
||||
|
||||
from .models import Identity
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DelegatedJWTAuthentication(JWTAuthentication):
|
||||
"""Override JWTAuthentication to create missing users on the fly."""
|
||||
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
"""Custom OpenID Connect (OIDC) Authentication Backend.
|
||||
|
||||
def get_user(self, validated_token):
|
||||
This class overrides the default OIDC Authentication Backend to accommodate differences
|
||||
in the User and Identity models, as well as the userinfo endpoint behavior in Agent Connect.
|
||||
"""
|
||||
|
||||
def get_userinfo(self, access_token, id_token, payload):
|
||||
"""Return user details dictionary.
|
||||
|
||||
Parameters:
|
||||
- access_token (str): The access token.
|
||||
- id_token (str): The id token (unused).
|
||||
- payload (dict): The token payload (unused).
|
||||
|
||||
Note: The id_token and payload parameters are unused in this implementation,
|
||||
but were kept to preserve base method signature.
|
||||
|
||||
Note: Userinfo endpoint returns a signed JWT, and not 'application/json'. This
|
||||
constraint comes from Agent Connect. It forces us to override the base method,
|
||||
which deal with 'application/json' response.
|
||||
|
||||
Returns:
|
||||
- dict: User details dictionary obtained from the OpenID Connect user endpoint.
|
||||
"""
|
||||
Return the user related to the given validated token, creating or updating it if necessary.
|
||||
|
||||
user_response = requests.get(
|
||||
self.OIDC_OP_USER_ENDPOINT,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
verify=self.get_settings("OIDC_VERIFY_SSL", True),
|
||||
timeout=self.get_settings("OIDC_TIMEOUT", None),
|
||||
proxies=self.get_settings("OIDC_PROXY", None),
|
||||
)
|
||||
user_response.raise_for_status()
|
||||
userinfo = self.verify_token(user_response.text)
|
||||
return userinfo
|
||||
|
||||
def get_or_create_user(self, access_token, id_token, payload):
|
||||
"""Return a User based on userinfo. Get or create a new user if no user matches the Sub.
|
||||
|
||||
Parameters:
|
||||
- access_token (str): The access token.
|
||||
- id_token (str): The ID token.
|
||||
- payload (dict): The user payload.
|
||||
|
||||
Returns:
|
||||
- User: An existing or newly created User instance.
|
||||
|
||||
Raises:
|
||||
- Exception: Raised when user creation is not allowed and no existing user is found.
|
||||
"""
|
||||
get_user = import_string(settings.JWT_USER_GETTER)
|
||||
return SimpleLazyObject(lambda: get_user(validated_token))
|
||||
|
||||
user_info = self.get_userinfo(access_token, id_token, payload)
|
||||
|
||||
class OpenApiJWTAuthenticationExtension(TokenScheme):
|
||||
"""Extension for specifying JWT authentication schemes."""
|
||||
email = user_info.get("email")
|
||||
sub = user_info.get("sub")
|
||||
|
||||
target_class = "core.authentication.DelegatedJWTAuthentication"
|
||||
name = "DelegatedJWTAuthentication"
|
||||
if sub is None:
|
||||
raise InvalidToken(
|
||||
_("User info contained no recognizable user identification")
|
||||
)
|
||||
|
||||
def get_security_definition(self, auto_schema):
|
||||
"""Return the security definition for JWT authentication."""
|
||||
return build_bearer_security_scheme_object(
|
||||
header_name="Authorization",
|
||||
token_prefix="Bearer", # noqa S106
|
||||
user = (
|
||||
self.UserModel.objects.filter(identities__sub=sub)
|
||||
.annotate(identity_email=models.F("identities__email"))
|
||||
.distinct()
|
||||
.first()
|
||||
)
|
||||
|
||||
if user:
|
||||
if email and email != user.identity_email:
|
||||
Identity.objects.filter(sub=sub).update(email=email)
|
||||
|
||||
class SessionAuthenticationWithAuthenticateHeader(authentication.SessionAuthentication):
|
||||
"""
|
||||
This class is needed, because REST Framework's default SessionAuthentication does
|
||||
never return 401's, because they cannot fill the WWW-Authenticate header with a
|
||||
valid value in the 401 response. As a result, we cannot distinguish calls that are
|
||||
not unauthorized (401 unauthorized) and calls for which the user does not have
|
||||
permission (403 forbidden).
|
||||
See https://github.com/encode/django-rest-framework/issues/5968
|
||||
elif self.get_settings("OIDC_CREATE_USER", True):
|
||||
user = self.create_user(user_info)
|
||||
|
||||
We do set authenticate_header function in SessionAuthentication, so that a value
|
||||
for the WWW-Authenticate header can be retrieved and the response code is
|
||||
automatically set to 401 in case of unauthenticated requests.
|
||||
"""
|
||||
return user
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return "Session"
|
||||
def create_user(self, claims):
|
||||
"""Return a newly created User instance."""
|
||||
|
||||
email = claims.get("email")
|
||||
sub = claims.get("sub")
|
||||
|
||||
class OpenApiSessionAuthenticationExtension(SessionScheme):
|
||||
"""Extension for specifying session authentication schemes."""
|
||||
if sub is None:
|
||||
raise InvalidToken(
|
||||
_("Claims contained no recognizable user identification")
|
||||
)
|
||||
|
||||
target_class = "core.api.authentication.SessionAuthenticationWithAuthenticateHeader"
|
||||
user = self.UserModel.objects.create(password="!", email=email) # noqa: S106
|
||||
Identity.objects.create(user=user, sub=sub, email=email)
|
||||
|
||||
return user
|
||||
|
||||
@@ -15,8 +15,6 @@ from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import jsonschema
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken
|
||||
from rest_framework_simplejwt.settings import api_settings
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -227,7 +225,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
)
|
||||
|
||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||
"""Send an email to this user."""
|
||||
"""Email this user."""
|
||||
main_identity = self.identities.get(is_main=True)
|
||||
mail.send_mail(subject, message, from_email, [main_identity.email], **kwargs)
|
||||
|
||||
@@ -450,40 +448,3 @@ class TeamAccess(BaseModel):
|
||||
"put": bool(set_role_to),
|
||||
"set_role_to": set_role_to,
|
||||
}
|
||||
|
||||
|
||||
def oidc_user_getter(validated_token):
|
||||
"""
|
||||
Given a valid OIDC token , retrieve, create or update corresponding user/contact/email from db.
|
||||
|
||||
The token is expected to have the following fields in payload:
|
||||
- sub
|
||||
- email
|
||||
- ...
|
||||
"""
|
||||
try:
|
||||
user_id = validated_token[api_settings.USER_ID_CLAIM]
|
||||
except KeyError as exc:
|
||||
raise InvalidToken(
|
||||
_("Token contained no recognizable user identification")
|
||||
) from exc
|
||||
|
||||
try:
|
||||
email_param = {"email": validated_token["email"]}
|
||||
except KeyError:
|
||||
email_param = {}
|
||||
|
||||
user = (
|
||||
User.objects.filter(identities__sub=user_id)
|
||||
.annotate(identity_email=models.F("identities__email"))
|
||||
.distinct()
|
||||
.first()
|
||||
)
|
||||
|
||||
if user is None:
|
||||
user = User.objects.create(password="!", **email_param) # noqa: S106
|
||||
Identity.objects.create(user=user, sub=user_id, **email_param)
|
||||
elif email_param and validated_token["email"] != user.identity_email:
|
||||
Identity.objects.filter(sub=user_id).update(email=validated_token["email"])
|
||||
|
||||
return user
|
||||
|
||||
@@ -11,7 +11,6 @@ from rest_framework.test import APIClient
|
||||
|
||||
from core.factories import IdentityFactory, TeamFactory
|
||||
from core.models import Team
|
||||
from core.tests.utils import OIDCToken
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -36,15 +35,16 @@ def test_api_teams_create_authenticated():
|
||||
"""
|
||||
identity = IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
response = APIClient().post(
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/teams/",
|
||||
{
|
||||
"name": "my team",
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_201_CREATED
|
||||
@@ -58,12 +58,12 @@ def test_api_teams_create_authenticated_slugify_name():
|
||||
Creating teams should automatically generate a slug.
|
||||
"""
|
||||
identity = IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = APIClient().post(
|
||||
response = client.post(
|
||||
"/api/v1.0/teams/",
|
||||
{"name": "my team"},
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_201_CREATED
|
||||
@@ -87,14 +87,15 @@ def test_api_teams_create_authenticated_expected_slug(param):
|
||||
Creating teams should automatically create unaccented, no unicode, lower-case slug.
|
||||
"""
|
||||
identity = IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
response = APIClient().post(
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/teams/",
|
||||
{
|
||||
"name": param[0],
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_201_CREATED
|
||||
@@ -109,14 +110,15 @@ def test_api_teams_create_authenticated_unique_slugs():
|
||||
"""
|
||||
TeamFactory(name="existing team")
|
||||
identity = IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
response = APIClient().post(
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/teams/",
|
||||
{
|
||||
"name": "èxisting team",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
||||
@@ -11,7 +11,6 @@ from rest_framework.status import (
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core.tests.utils import OIDCToken
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -34,13 +33,14 @@ def test_api_teams_delete_authenticated_unrelated():
|
||||
related.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
|
||||
response = APIClient().delete(
|
||||
f"/api/v1.0/teams/{team.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
@@ -55,12 +55,14 @@ def test_api_teams_delete_authenticated_member():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "member")])
|
||||
|
||||
response = APIClient().delete(
|
||||
f"/api/v1.0/teams/{team.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
@@ -77,12 +79,14 @@ def test_api_teams_delete_authenticated_administrator():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "administrator")])
|
||||
|
||||
response = APIClient().delete(
|
||||
f"/api/v1.0/teams/{team.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
@@ -99,12 +103,14 @@ def test_api_teams_delete_authenticated_owner():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "owner")])
|
||||
|
||||
response = APIClient().delete(
|
||||
f"/api/v1.0/teams/{team.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
|
||||
@@ -8,10 +8,7 @@ from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core.api import serializers
|
||||
|
||||
from ..utils import OIDCToken
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -32,7 +29,9 @@ def test_api_teams_list_authenticated():
|
||||
"""Authenticated users should be able to list teams they are an owner/administrator/member of."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
expected_ids = {
|
||||
str(access.team.id)
|
||||
@@ -40,8 +39,8 @@ def test_api_teams_list_authenticated():
|
||||
}
|
||||
factories.TeamFactory.create_batch(2) # Other teams
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/teams/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/teams/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -58,7 +57,9 @@ def test_api_teams_list_pagination(
|
||||
"""Pagination should work as expected."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team_ids = [
|
||||
str(access.team.id)
|
||||
@@ -66,8 +67,8 @@ def test_api_teams_list_pagination(
|
||||
]
|
||||
|
||||
# Get page 1
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/teams/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/teams/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -82,8 +83,8 @@ def test_api_teams_list_pagination(
|
||||
team_ids.remove(item["id"])
|
||||
|
||||
# Get page 2
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/teams/?page=2", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/teams/?page=2",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -102,14 +103,16 @@ def test_api_teams_list_authenticated_distinct():
|
||||
"""A team with several related users should only be listed once."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
team = factories.TeamFactory(users=[user, other_user])
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/teams/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/teams/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
@@ -6,7 +6,6 @@ from rest_framework.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED, HTTP_404_N
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.tests.utils import OIDCToken
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -28,13 +27,14 @@ def test_api_teams_retrieve_authenticated_unrelated():
|
||||
not related.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/teams/{team.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
f"/api/v1.0/teams/{team.id!s}/",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
@@ -47,14 +47,16 @@ def test_api_teams_retrieve_authenticated_related():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
access1 = factories.TeamAccessFactory(team=team, user=user)
|
||||
access2 = factories.TeamAccessFactory(team=team)
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/teams/{team.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
f"/api/v1.0/teams/{team.id!s}/",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
content = response.json()
|
||||
|
||||
@@ -15,7 +15,6 @@ from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.api import serializers
|
||||
from core.tests.utils import OIDCToken
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -46,18 +45,18 @@ def test_api_teams_update_authenticated_unrelated():
|
||||
Authenticated users should not be allowed to update a team to which they are not related.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
old_team_values = serializers.TeamSerializer(instance=team).data
|
||||
|
||||
new_team_values = serializers.TeamSerializer(instance=factories.TeamFactory()).data
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/",
|
||||
new_team_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
@@ -75,17 +74,18 @@ def test_api_teams_update_authenticated_members():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "member")])
|
||||
old_team_values = serializers.TeamSerializer(instance=team).data
|
||||
|
||||
new_team_values = serializers.TeamSerializer(instance=factories.TeamFactory()).data
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/",
|
||||
new_team_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
@@ -102,18 +102,19 @@ def test_api_teams_update_authenticated_administrators():
|
||||
"""Administrators of a team should be allowed to update it."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "administrator")])
|
||||
initial_values = serializers.TeamSerializer(instance=team).data
|
||||
|
||||
# generate new random values
|
||||
new_values = serializers.TeamSerializer(instance=factories.TeamFactory.build()).data
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/",
|
||||
new_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
@@ -132,7 +133,9 @@ def test_api_teams_update_authenticated_owners():
|
||||
apart from read-only fields."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "owner")])
|
||||
old_team_values = serializers.TeamSerializer(instance=team).data
|
||||
@@ -140,11 +143,10 @@ def test_api_teams_update_authenticated_owners():
|
||||
new_team_values = serializers.TeamSerializer(
|
||||
instance=factories.TeamFactory.build()
|
||||
).data
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/",
|
||||
new_team_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
@@ -165,18 +167,19 @@ def test_api_teams_update_administrator_or_owner_of_another():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.TeamFactory(users=[(user, random.choice(["administrator", "owner"]))])
|
||||
team = factories.TeamFactory(name="Old name")
|
||||
old_team_values = serializers.TeamSerializer(instance=team).data
|
||||
|
||||
new_team_values = serializers.TeamSerializer(instance=factories.TeamFactory()).data
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/",
|
||||
new_team_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
@@ -194,7 +197,9 @@ def test_api_teams_update_existing_slug_should_return_error():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.TeamFactory(name="Existing team", users=[(user, "administrator")])
|
||||
my_team = factories.TeamFactory(name="New team", users=[(user, "administrator")])
|
||||
@@ -202,11 +207,10 @@ def test_api_teams_update_existing_slug_should_return_error():
|
||||
updated_values = serializers.TeamSerializer(instance=my_team).data
|
||||
# Update my team's name for existing team. Creates a duplicate slug
|
||||
updated_values["name"] = "existing team"
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{my_team.id!s}/",
|
||||
updated_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["slug"] == ["Team with this Slug already exists."]
|
||||
|
||||
@@ -9,8 +9,6 @@ from rest_framework.test import APIClient
|
||||
from core import factories, models
|
||||
from core.api import serializers
|
||||
|
||||
from .utils import OIDCToken
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@@ -75,7 +73,6 @@ def test_api_contacts_list_authenticated_no_query():
|
||||
contact = factories.ContactFactory(owner=user)
|
||||
user.profile_contact = contact
|
||||
user.save()
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
# Let's have 5 contacts in database:
|
||||
assert user.profile_contact is not None # Excluded because profile contact
|
||||
@@ -87,9 +84,10 @@ def test_api_contacts_list_authenticated_no_query():
|
||||
base=base_contact, owner=user, full_name="Bernard"
|
||||
) # Included
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/contacts/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
@@ -111,7 +109,6 @@ def test_api_contacts_list_authenticated_by_full_name():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.BaseContactFactory(full_name="David Bowman")
|
||||
nicole = factories.BaseContactFactory(full_name="Nicole Foole")
|
||||
@@ -119,36 +116,31 @@ def test_api_contacts_list_authenticated_by_full_name():
|
||||
factories.BaseContactFactory(full_name="Heywood Floyd")
|
||||
|
||||
# Full query should work
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=David%20Bowman", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/contacts/?q=David%20Bowman")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
assert contact_ids == [str(dave.id)]
|
||||
|
||||
# Partial query should work
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=ank", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=ank")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
assert contact_ids == [str(frank.id)]
|
||||
|
||||
# Result that matches a trigram twice ranks better than result that matches once
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=ole", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=ole")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
# "Nicole Foole" matches twice on "ole"
|
||||
assert contact_ids == [str(nicole.id), str(frank.id)]
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=ool", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=ool")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
@@ -159,23 +151,21 @@ def test_api_contacts_list_authenticated_uppercase_content():
|
||||
"""Upper case content should be found by lower case query."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.BaseContactFactory(full_name="EEE", short_name="AAA")
|
||||
|
||||
# Unaccented full name
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=eee", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/contacts/?q=eee")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
assert contact_ids == [str(dave.id)]
|
||||
|
||||
# Unaccented short name
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=aaa", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=aaa")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
@@ -186,23 +176,21 @@ def test_api_contacts_list_authenticated_capital_query():
|
||||
"""Upper case query should find lower case content."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.BaseContactFactory(full_name="eee", short_name="aaa")
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Unaccented full name
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=EEE", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=EEE")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
assert contact_ids == [str(dave.id)]
|
||||
|
||||
# Unaccented short name
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=AAA", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=AAA")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
@@ -213,23 +201,21 @@ def test_api_contacts_list_authenticated_accented_content():
|
||||
"""Accented content should be found by unaccented query."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.BaseContactFactory(full_name="ééé", short_name="ààà")
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Unaccented full name
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=eee", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=eee")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
assert contact_ids == [str(dave.id)]
|
||||
|
||||
# Unaccented short name
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=aaa", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=aaa")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
@@ -240,23 +226,21 @@ def test_api_contacts_list_authenticated_accented_query():
|
||||
"""Accented query should find unaccented content."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.BaseContactFactory(full_name="eee", short_name="aaa")
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Unaccented full name
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=ééé", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=ééé")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
assert contact_ids == [str(dave.id)]
|
||||
|
||||
# Unaccented short name
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/contacts/?q=ààà", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/contacts/?q=ààà")
|
||||
|
||||
assert response.status_code == 200
|
||||
contact_ids = [contact["id"] for contact in response.json()]
|
||||
@@ -281,13 +265,13 @@ def test_api_contacts_retrieve_authenticated_owned():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
contact = factories.ContactFactory(owner=user)
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"/api/v1.0/contacts/{contact.id!s}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -305,13 +289,12 @@ def test_api_contacts_retrieve_authenticated_public():
|
||||
Authenticated users should be able to retrieve public contacts.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
contact = factories.BaseContactFactory()
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.get(f"/api/v1.0/contacts/{contact.id!s}/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": str(contact.id),
|
||||
@@ -328,13 +311,12 @@ def test_api_contacts_retrieve_authenticated_other():
|
||||
Authenticated users should not be allowed to retrieve another user's contacts.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
contact = factories.ContactFactory()
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.get(f"/api/v1.0/contacts/{contact.id!s}/")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
@@ -357,17 +339,17 @@ def test_api_contacts_create_anonymous_forbidden():
|
||||
def test_api_contacts_create_authenticated_missing_base():
|
||||
"""Anonymous users should be able to create users."""
|
||||
identity = factories.IdentityFactory(user__profile_contact=None)
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
response = APIClient().post(
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/contacts/",
|
||||
{
|
||||
"full_name": "David Bowman",
|
||||
"short_name": "Dave",
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert models.Contact.objects.exists() is False
|
||||
@@ -379,14 +361,15 @@ def test_api_contacts_create_authenticated_successful():
|
||||
"""Authenticated users should be able to create contacts."""
|
||||
identity = factories.IdentityFactory(user__profile_contact=None)
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
base_contact = factories.BaseContactFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Existing override for another user should not interfere
|
||||
factories.ContactFactory(base=base_contact)
|
||||
|
||||
response = APIClient().post(
|
||||
response = client.post(
|
||||
"/api/v1.0/contacts/",
|
||||
{
|
||||
"base": str(base_contact.id),
|
||||
@@ -395,7 +378,6 @@ def test_api_contacts_create_authenticated_successful():
|
||||
"data": CONTACT_DATA,
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
@@ -426,12 +408,14 @@ def test_api_contacts_create_authenticated_existing_override():
|
||||
"""
|
||||
identity = factories.IdentityFactory(user__profile_contact=None)
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
base_contact = factories.BaseContactFactory()
|
||||
factories.ContactFactory(base=base_contact, owner=user)
|
||||
|
||||
response = APIClient().post(
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/contacts/",
|
||||
{
|
||||
"base": str(base_contact.id),
|
||||
@@ -440,7 +424,6 @@ def test_api_contacts_create_authenticated_existing_override():
|
||||
"data": CONTACT_DATA,
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
@@ -481,7 +464,9 @@ def test_api_contacts_update_authenticated_owned():
|
||||
"""
|
||||
identity = factories.IdentityFactory(user__profile_contact=None)
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
contact = factories.ContactFactory(owner=user) # Owned by the logged-in user
|
||||
old_contact_values = serializers.ContactSerializer(instance=contact).data
|
||||
@@ -491,11 +476,10 @@ def test_api_contacts_update_authenticated_owned():
|
||||
).data
|
||||
new_contact_values["base"] = str(factories.ContactFactory().id)
|
||||
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/",
|
||||
new_contact_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -511,11 +495,13 @@ def test_api_contacts_update_authenticated_owned():
|
||||
|
||||
def test_api_contacts_update_authenticated_profile():
|
||||
"""
|
||||
Authenticated users should be allowed to update their prodile contact.
|
||||
Authenticated users should be allowed to update their profile contact.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
contact = factories.ContactFactory(owner=user)
|
||||
user.profile_contact = contact
|
||||
@@ -527,11 +513,10 @@ def test_api_contacts_update_authenticated_profile():
|
||||
).data
|
||||
new_contact_values["base"] = str(factories.ContactFactory().id)
|
||||
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/",
|
||||
new_contact_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -550,7 +535,9 @@ def test_api_contacts_update_authenticated_other():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
contact = factories.ContactFactory() # owned by another user
|
||||
old_contact_values = serializers.ContactSerializer(instance=contact).data
|
||||
@@ -560,11 +547,10 @@ def test_api_contacts_update_authenticated_other():
|
||||
).data
|
||||
new_contact_values["base"] = str(factories.ContactFactory().id)
|
||||
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/",
|
||||
new_contact_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -588,13 +574,13 @@ def test_api_contacts_delete_list_authenticated():
|
||||
"""Authenticated users should not be allowed to delete a list of contacts."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.ContactFactory.create_batch(2)
|
||||
|
||||
response = APIClient().delete(
|
||||
"/api/v1.0/contacts/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.delete("/api/v1.0/contacts/")
|
||||
|
||||
assert response.status_code == 405
|
||||
assert models.Contact.objects.count() == 4
|
||||
@@ -617,13 +603,14 @@ def test_api_contacts_delete_authenticated_public():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
contact = factories.BaseContactFactory()
|
||||
|
||||
response = APIClient().delete(
|
||||
response = client.delete(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -636,13 +623,13 @@ def test_api_contacts_delete_authenticated_owner():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
contact = factories.ContactFactory(owner=user)
|
||||
|
||||
response = APIClient().delete(
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
@@ -656,15 +643,15 @@ def test_api_contacts_delete_authenticated_profile():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
contact = factories.ContactFactory(owner=user, base=None)
|
||||
user.profile_contact = contact
|
||||
user.save()
|
||||
|
||||
response = APIClient().delete(
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
@@ -677,13 +664,13 @@ def test_api_contacts_delete_authenticated_other():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
contact = factories.ContactFactory()
|
||||
|
||||
response = APIClient().delete(
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/contacts/{contact.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
@@ -10,8 +10,6 @@ from rest_framework.test import APIClient
|
||||
from core import factories, models
|
||||
from core.api import serializers
|
||||
|
||||
from .utils import OIDCToken
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@@ -34,18 +32,18 @@ def test_api_team_accesses_list_authenticated_unrelated():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
factories.TeamAccessFactory.create_batch(3, team=team)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Accesses for other teams to which the user is related should not be listed either
|
||||
other_access = factories.TeamAccessFactory(user=user)
|
||||
factories.TeamAccessFactory(team=other_access.team)
|
||||
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -63,7 +61,9 @@ def test_api_team_accesses_list_authenticated_related():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
user_access = models.TeamAccess.objects.create(team=team, user=user) # random role
|
||||
@@ -73,9 +73,8 @@ def test_api_team_accesses_list_authenticated_related():
|
||||
other_access = factories.TeamAccessFactory(user=user)
|
||||
factories.TeamAccessFactory(team=other_access.team)
|
||||
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -129,15 +128,14 @@ def test_api_team_accesses_retrieve_authenticated_unrelated():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
access = factories.TeamAccessFactory(team=team)
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
response = client.get(f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
@@ -148,9 +146,8 @@ def test_api_team_accesses_retrieve_authenticated_unrelated():
|
||||
factories.TeamAccessFactory(),
|
||||
factories.TeamAccessFactory(user=user),
|
||||
]:
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
@@ -164,14 +161,15 @@ def test_api_team_accesses_retrieve_authenticated_related():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[user])
|
||||
access = factories.TeamAccessFactory(team=team)
|
||||
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -211,18 +209,19 @@ def test_api_team_accesses_create_authenticated_unrelated():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
team = factories.TeamFactory()
|
||||
|
||||
response = APIClient().post(
|
||||
response = client.post(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -236,21 +235,21 @@ def test_api_team_accesses_create_authenticated_member():
|
||||
"""Members of a team should not be allowed to create team accesses."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "member")])
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
api_client = APIClient()
|
||||
for role in [role[0] for role in models.RoleChoices.choices]:
|
||||
response = api_client.post(
|
||||
response = client.post(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -267,22 +266,21 @@ def test_api_team_accesses_create_authenticated_administrator():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "administrator")])
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
api_client = APIClient()
|
||||
|
||||
# It should not be allowed to create an owner access
|
||||
response = api_client.post(
|
||||
response = client.post(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": "owner",
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -295,14 +293,13 @@ def test_api_team_accesses_create_authenticated_administrator():
|
||||
[role[0] for role in models.RoleChoices.choices if role[0] != "owner"]
|
||||
)
|
||||
|
||||
response = api_client.post(
|
||||
response = client.post(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
@@ -322,21 +319,22 @@ def test_api_team_accesses_create_authenticated_owner():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "owner")])
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||
|
||||
response = APIClient().post(
|
||||
response = client.post(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
@@ -382,7 +380,9 @@ def test_api_team_accesses_update_authenticated_unrelated():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
access = factories.TeamAccessFactory()
|
||||
old_values = serializers.TeamAccessSerializer(instance=access).data
|
||||
@@ -393,13 +393,11 @@ def test_api_team_accesses_update_authenticated_unrelated():
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
api_client = APIClient()
|
||||
for field, value in new_values.items():
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{access.team.id!s}/accesses/{access.id!s}/",
|
||||
{**old_values, field: value},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
@@ -412,7 +410,9 @@ def test_api_team_accesses_update_authenticated_member():
|
||||
"""Members of a team should not be allowed to update its accesses."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "member")])
|
||||
access = factories.TeamAccessFactory(team=team)
|
||||
@@ -424,13 +424,11 @@ def test_api_team_accesses_update_authenticated_member():
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
api_client = APIClient()
|
||||
for field, value in new_values.items():
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{access.team.id!s}/accesses/{access.id!s}/",
|
||||
{**old_values, field: value},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
@@ -446,7 +444,9 @@ def test_api_team_accesses_update_administrator_except_owner():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "administrator")])
|
||||
access = factories.TeamAccessFactory(
|
||||
@@ -461,14 +461,12 @@ def test_api_team_accesses_update_administrator_except_owner():
|
||||
"role": random.choice(["administrator", "member"]),
|
||||
}
|
||||
|
||||
api_client = APIClient()
|
||||
for field, value in new_values.items():
|
||||
new_data = {**old_values, field: value}
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
data=new_data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -493,7 +491,9 @@ def test_api_team_accesses_update_administrator_from_owner():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "administrator")])
|
||||
other_user = factories.UserFactory()
|
||||
@@ -506,13 +506,11 @@ def test_api_team_accesses_update_administrator_from_owner():
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
api_client = APIClient()
|
||||
for field, value in new_values.items():
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
data={**old_values, field: value},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
access.refresh_from_db()
|
||||
@@ -527,7 +525,9 @@ def test_api_team_accesses_update_administrator_to_owner():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "administrator")])
|
||||
other_user = factories.UserFactory()
|
||||
@@ -544,14 +544,12 @@ def test_api_team_accesses_update_administrator_to_owner():
|
||||
"role": "owner",
|
||||
}
|
||||
|
||||
api_client = APIClient()
|
||||
for field, value in new_values.items():
|
||||
new_data = {**old_values, field: value}
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
data=new_data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
# We are not allowed or not really updating the role
|
||||
if field == "role" or new_data["role"] == old_values["role"]:
|
||||
@@ -571,7 +569,9 @@ def test_api_team_accesses_update_owner_except_owner():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "owner")])
|
||||
factories.UserFactory()
|
||||
@@ -587,14 +587,12 @@ def test_api_team_accesses_update_owner_except_owner():
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
api_client = APIClient()
|
||||
for field, value in new_values.items():
|
||||
new_data = {**old_values, field: value}
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
data=new_data,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -620,7 +618,9 @@ def test_api_team_accesses_update_owner_for_owners():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "owner")])
|
||||
access = factories.TeamAccessFactory(team=team, role="owner")
|
||||
@@ -632,13 +632,11 @@ def test_api_team_accesses_update_owner_for_owners():
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
api_client = APIClient()
|
||||
for field, value in new_values.items():
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
data={**old_values, field: value},
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
access.refresh_from_db()
|
||||
@@ -653,19 +651,19 @@ def test_api_team_accesses_update_owner_self():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
access = factories.TeamAccessFactory(team=team, user=user, role="owner")
|
||||
old_values = serializers.TeamAccessSerializer(instance=access).data
|
||||
new_role = random.choice(["administrator", "member"])
|
||||
|
||||
api_client = APIClient()
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
data={**old_values, "role": new_role},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -675,11 +673,10 @@ def test_api_team_accesses_update_owner_self():
|
||||
# Add another owner and it should now work
|
||||
factories.TeamAccessFactory(team=team, role="owner")
|
||||
|
||||
response = api_client.put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
data={**old_values, "role": new_role},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -709,13 +706,14 @@ def test_api_team_accesses_delete_authenticated():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
access = factories.TeamAccessFactory()
|
||||
|
||||
response = APIClient().delete(
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{access.team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -729,7 +727,9 @@ def test_api_team_accesses_delete_member():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "member")])
|
||||
access = factories.TeamAccessFactory(team=team)
|
||||
@@ -737,9 +737,8 @@ def test_api_team_accesses_delete_member():
|
||||
assert models.TeamAccess.objects.count() == 2
|
||||
assert models.TeamAccess.objects.filter(user=access.user).exists()
|
||||
|
||||
response = APIClient().delete(
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -753,7 +752,9 @@ def test_api_team_accesses_delete_administrators():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "administrator")])
|
||||
access = factories.TeamAccessFactory(
|
||||
@@ -763,9 +764,8 @@ def test_api_team_accesses_delete_administrators():
|
||||
assert models.TeamAccess.objects.count() == 2
|
||||
assert models.TeamAccess.objects.filter(user=access.user).exists()
|
||||
|
||||
response = APIClient().delete(
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
@@ -779,7 +779,9 @@ def test_api_team_accesses_delete_owners_except_owners():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "owner")])
|
||||
access = factories.TeamAccessFactory(
|
||||
@@ -789,9 +791,8 @@ def test_api_team_accesses_delete_owners_except_owners():
|
||||
assert models.TeamAccess.objects.count() == 2
|
||||
assert models.TeamAccess.objects.filter(user=access.user).exists()
|
||||
|
||||
response = APIClient().delete(
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
@@ -805,7 +806,9 @@ def test_api_team_accesses_delete_owners_for_owners():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "owner")])
|
||||
access = factories.TeamAccessFactory(team=team, role="owner")
|
||||
@@ -813,9 +816,8 @@ def test_api_team_accesses_delete_owners_for_owners():
|
||||
assert models.TeamAccess.objects.count() == 2
|
||||
assert models.TeamAccess.objects.filter(user=access.user).exists()
|
||||
|
||||
response = APIClient().delete(
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -828,15 +830,16 @@ def test_api_team_accesses_delete_owners_last_owner():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
team = factories.TeamFactory()
|
||||
access = factories.TeamAccessFactory(team=team, user=user, role="owner")
|
||||
|
||||
assert models.TeamAccess.objects.count() == 1
|
||||
response = APIClient().delete(
|
||||
response = client.delete(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
@@ -16,8 +16,6 @@ from core import factories, models
|
||||
from core.api import serializers
|
||||
from core.api.viewsets import Pagination
|
||||
|
||||
from .utils import OIDCToken
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@@ -37,11 +35,13 @@ def test_api_users_list_authenticated():
|
||||
Authenticated users should be able to list all users.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
factories.UserFactory.create_batch(2)
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/users/",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert len(response.json()["results"]) == 3
|
||||
@@ -54,7 +54,9 @@ def test_api_users_authenticated_list_by_email():
|
||||
"""
|
||||
user = factories.UserFactory(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
dave = factories.IdentityFactory(email="david.bowman@work.com")
|
||||
nicole = factories.IdentityFactory(email="nicole_foole@work.com")
|
||||
@@ -62,9 +64,8 @@ def test_api_users_authenticated_list_by_email():
|
||||
factories.IdentityFactory(email="heywood_floyd@work.com")
|
||||
|
||||
# Full query should work
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=david.bowman@work.com",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -72,18 +73,14 @@ def test_api_users_authenticated_list_by_email():
|
||||
assert user_ids[0] == str(dave.user.id)
|
||||
|
||||
# Partial query should work
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=fran", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/users/?q=fran")
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids[0] == str(frank.user.id)
|
||||
|
||||
# Result that matches a trigram twice ranks better than result that matches once
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=ole", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/users/?q=ole")
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
@@ -91,9 +88,7 @@ def test_api_users_authenticated_list_by_email():
|
||||
assert user_ids == [str(nicole.user.id), str(frank.user.id)]
|
||||
|
||||
# Even with a low similarity threshold, query should match expected emails
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=ool", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
response = client.get("/api/v1.0/users/?q=ool")
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
@@ -106,16 +101,17 @@ def test_api_users_authenticated_list_multiple_identities_single_user():
|
||||
"""
|
||||
user = factories.UserFactory(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
dave = factories.UserFactory()
|
||||
factories.IdentityFactory(user=dave, email="david.bowman@work.com")
|
||||
factories.IdentityFactory(user=dave, email="david.bowman@fun.fr")
|
||||
|
||||
# Full query should work
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=david.bowman@work.com",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -131,7 +127,9 @@ def test_api_users_authenticated_list_multiple_identities_multiple_users():
|
||||
"""
|
||||
user = factories.UserFactory(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
dave = factories.UserFactory()
|
||||
davina = factories.UserFactory()
|
||||
@@ -142,9 +140,8 @@ def test_api_users_authenticated_list_multiple_identities_multiple_users():
|
||||
factories.IdentityFactory(user=prudence, email="prudence.crandall@work.com")
|
||||
|
||||
# Full query should work
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=david.bowman@work.com",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -157,14 +154,15 @@ def test_api_users_authenticated_list_uppercase_content():
|
||||
"""Upper case content should be found by lower case query."""
|
||||
user = factories.UserFactory(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
dave = factories.IdentityFactory(email="DAVID.BOWMAN@INTENSEWORK.COM")
|
||||
|
||||
# Unaccented full address
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=david.bowman@intensework.com",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -172,8 +170,8 @@ def test_api_users_authenticated_list_uppercase_content():
|
||||
assert user_ids == [str(dave.user.id)]
|
||||
|
||||
# Partial query
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=david", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=david",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -185,14 +183,15 @@ def test_api_users_list_authenticated_capital_query():
|
||||
"""Upper case query should find lower case content."""
|
||||
user = factories.UserFactory(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
dave = factories.IdentityFactory(email="david.bowman@work.com")
|
||||
|
||||
# Full uppercase query
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=DAVID.BOWMAN@WORK.COM",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -200,8 +199,8 @@ def test_api_users_list_authenticated_capital_query():
|
||||
assert user_ids == [str(dave.user.id)]
|
||||
|
||||
# Partial uppercase email
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=DAVID", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=DAVID",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -213,14 +212,15 @@ def test_api_contacts_list_authenticated_accented_query():
|
||||
"""Accented content should be found by unaccented query."""
|
||||
user = factories.UserFactory(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
helene = factories.IdentityFactory(email="helene.bowman@work.com")
|
||||
|
||||
# Accented full query
|
||||
response = APIClient().get(
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=hélène.bowman@work.com",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -228,8 +228,8 @@ def test_api_contacts_list_authenticated_accented_query():
|
||||
assert user_ids == [str(helene.user.id)]
|
||||
|
||||
# Unaccented partial email
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=hélène", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=hélène",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -244,13 +244,15 @@ def test_api_users_list_pagination(
|
||||
"""Pagination should work as expected."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.UserFactory.create_batch(4)
|
||||
|
||||
# Get page 1
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/users/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -262,8 +264,8 @@ def test_api_users_list_pagination(
|
||||
assert content["previous"] is None
|
||||
|
||||
# Get page 2
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?page=2", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?page=2",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -291,7 +293,9 @@ def test_api_users_retrieve_me_authenticated():
|
||||
"""Authenticated users should be able to retrieve their own user via the "/users/me" path."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Define profile contact
|
||||
contact = factories.ContactFactory(owner=user)
|
||||
@@ -299,8 +303,8 @@ def test_api_users_retrieve_me_authenticated():
|
||||
user.save()
|
||||
|
||||
factories.UserFactory.create_batch(2)
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/me/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
"/api/v1.0/users/me/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -334,10 +338,12 @@ def test_api_users_retrieve_authenticated_self():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/users/{user.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
)
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.json() == {"detail": 'Method "GET" not allowed.'}
|
||||
@@ -349,12 +355,14 @@ def test_api_users_retrieve_authenticated_other():
|
||||
limited information.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/users/{other_user.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
response = client.get(
|
||||
f"/api/v1.0/users/{other_user.id!s}/",
|
||||
)
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.json() == {"detail": 'Method "GET" not allowed.'}
|
||||
@@ -380,16 +388,17 @@ def test_api_users_create_authenticated():
|
||||
"""Authenticated users should not be able to create users via the API."""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
response = APIClient().post(
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/users/",
|
||||
{
|
||||
"language": "fr-fr",
|
||||
"password": "mypassword",
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.json() == {"detail": 'Method "POST" not allowed.'}
|
||||
@@ -427,18 +436,19 @@ def test_api_users_update_authenticated_self():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = dict(
|
||||
serializers.UserSerializer(instance=factories.UserFactory()).data
|
||||
)
|
||||
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
new_user_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
@@ -454,17 +464,18 @@ def test_api_users_update_authenticated_self():
|
||||
def test_api_users_update_authenticated_other():
|
||||
"""Authenticated users should not be allowed to update other users."""
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
user = factories.UserFactory()
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = serializers.UserSerializer(instance=factories.UserFactory()).data
|
||||
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
new_user_values,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
@@ -507,7 +518,9 @@ def test_api_users_patch_authenticated_self():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = dict(
|
||||
@@ -515,11 +528,10 @@ def test_api_users_patch_authenticated_self():
|
||||
)
|
||||
|
||||
for key, new_value in new_user_values.items():
|
||||
response = APIClient().patch(
|
||||
response = client.patch(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
{key: new_value},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
@@ -535,7 +547,9 @@ def test_api_users_patch_authenticated_self():
|
||||
def test_api_users_patch_authenticated_other():
|
||||
"""Authenticated users should not be allowed to patch other users."""
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
user = factories.UserFactory()
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
@@ -544,11 +558,10 @@ def test_api_users_patch_authenticated_other():
|
||||
)
|
||||
|
||||
for key, new_value in new_user_values.items():
|
||||
response = APIClient().put(
|
||||
response = client.put(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
{key: new_value},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
|
||||
@@ -573,11 +586,12 @@ def test_api_users_delete_list_authenticated():
|
||||
"""Authenticated users should not be allowed to delete a list of users."""
|
||||
factories.UserFactory.create_batch(2)
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.delete(
|
||||
"/api/v1.0/users/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
"/api/v1.0/users/",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
@@ -599,12 +613,12 @@ def test_api_users_delete_authenticated():
|
||||
Authenticated users should not be allowed to delete a user other than themselves.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
response = APIClient().delete(
|
||||
f"/api/v1.0/users/{other_user.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.delete(f"/api/v1.0/users/{other_user.id!s}/")
|
||||
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert models.User.objects.count() == 2
|
||||
@@ -613,11 +627,12 @@ def test_api_users_delete_authenticated():
|
||||
def test_api_users_delete_self():
|
||||
"""Authenticated users should not be able to delete their own user."""
|
||||
identity = factories.IdentityFactory()
|
||||
jwt_token = OIDCToken.for_user(identity.user)
|
||||
|
||||
response = APIClient().delete(
|
||||
client = APIClient()
|
||||
client.force_login(identity.user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/users/{identity.user.id!s}/",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Unit tests for the `get_or_create_user` function."""
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken
|
||||
from rest_framework_simplejwt.tokens import AccessToken
|
||||
|
||||
from core import factories, models
|
||||
from core.authentication import OIDCAuthenticationBackend
|
||||
from core.factories import IdentityFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_no_email(
|
||||
django_assert_num_queries, monkeypatch
|
||||
):
|
||||
"""
|
||||
If an existing user matches the user's info sub, the user should be returned.
|
||||
"""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
# Create a user and its identity
|
||||
identity = IdentityFactory()
|
||||
|
||||
# Create multiple identities for a user
|
||||
for _ in range(5):
|
||||
IdentityFactory(user=identity.user)
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": identity.sub}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
user = klass.get_or_create_user(
|
||||
access_token=AccessToken(), id_token=None, payload=None
|
||||
)
|
||||
|
||||
identity.refresh_from_db()
|
||||
assert user == identity.user
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_with_email(
|
||||
django_assert_num_queries, monkeypatch
|
||||
):
|
||||
"""
|
||||
When the user's info contains an email and targets an existing user,
|
||||
it should update the email on the identity but not on the user.
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
identity = IdentityFactory()
|
||||
|
||||
# Create multiple identities for a user
|
||||
for _ in range(5):
|
||||
IdentityFactory(user=identity.user)
|
||||
|
||||
user_email = identity.user.email
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": identity.sub, "email": identity.email}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# Only 1 query if the email has not changed
|
||||
with django_assert_num_queries(1):
|
||||
user = klass.get_or_create_user(
|
||||
access_token=AccessToken(), id_token=None, payload=None
|
||||
)
|
||||
|
||||
new_email = "test@fooo.com"
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": identity.sub, "email": new_email}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# Additional update query if the email has changed
|
||||
with django_assert_num_queries(2):
|
||||
user = klass.get_or_create_user(
|
||||
access_token=AccessToken(), id_token=None, payload=None
|
||||
)
|
||||
|
||||
identity.refresh_from_db()
|
||||
assert identity.email == new_email
|
||||
|
||||
assert models.User.objects.count() == 1
|
||||
assert user == identity.user
|
||||
assert user.email == user_email
|
||||
|
||||
|
||||
def test_authentication_getter_new_user_no_email(monkeypatch):
|
||||
"""
|
||||
If no user matches the user's info sub, a user should be created.
|
||||
User's info doesn't contain an email, created user's email should be empty.
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123"}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
user = klass.get_or_create_user(
|
||||
access_token=AccessToken(), id_token=None, payload=None
|
||||
)
|
||||
|
||||
identity = user.identities.get()
|
||||
assert identity.sub == "123"
|
||||
assert identity.email is None
|
||||
|
||||
assert user.email is None
|
||||
assert user.password == "!"
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_authentication_getter_new_user_with_email(monkeypatch):
|
||||
"""
|
||||
If no user matches the user's info sub, a user should be created.
|
||||
User's info contains an email, created user's email should be set.
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
email = "people@example.com"
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123", "email": email}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
user = klass.get_or_create_user(
|
||||
access_token=AccessToken(), id_token=None, payload=None
|
||||
)
|
||||
|
||||
identity = user.identities.get()
|
||||
assert identity.sub == "123"
|
||||
assert identity.email == email
|
||||
|
||||
assert user.email == email
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_models_oidc_user_getter_invalid_token(django_assert_num_queries, monkeypatch):
|
||||
"""The user's info doesn't contain a sub."""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {
|
||||
"test": "123",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with django_assert_num_queries(0), pytest.raises(
|
||||
InvalidToken, match="User info contained no recognizable user identification"
|
||||
):
|
||||
user = klass.get_or_create_user(
|
||||
access_token=AccessToken(), id_token=None, payload=None
|
||||
)
|
||||
|
||||
assert models.User.objects.exists() is False
|
||||
@@ -52,7 +52,7 @@ def test_models_contacts_base_to_base():
|
||||
|
||||
|
||||
def test_models_contacts_owner_base_unique():
|
||||
"""Their should be only one contact deriving from a given base contact for a given owner."""
|
||||
"""There should be only one contact deriving from a given base contact for a given owner."""
|
||||
contact = factories.ContactFactory()
|
||||
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
|
||||
@@ -107,7 +107,7 @@ def test_models_identities_email_normalization():
|
||||
|
||||
|
||||
def test_models_identities_ordering():
|
||||
"""Identitys should be returned ordered by main status then by their email address."""
|
||||
"""Identities should be returned ordered by main status then by their email address."""
|
||||
user = factories.UserFactory()
|
||||
factories.IdentityFactory.create_batch(5, user=user)
|
||||
|
||||
@@ -142,7 +142,7 @@ def test_models_identities_sub_unique():
|
||||
|
||||
|
||||
def test_models_identities_sub_max_length():
|
||||
"""The sub field should be 255 characters maximum."""
|
||||
"""The subfield should be 255 characters maximum."""
|
||||
factories.IdentityFactory(sub="a" * 255)
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
factories.IdentityFactory(sub="a" * 256)
|
||||
@@ -154,13 +154,13 @@ def test_models_identities_sub_max_length():
|
||||
|
||||
|
||||
def test_models_identities_sub_special_characters():
|
||||
"""The sub field should accept periods, dashes, +, @ and underscores."""
|
||||
"""The subfield should accept periods, dashes, +, @ and underscores."""
|
||||
identity = factories.IdentityFactory(sub="dave.bowman-1+2@hal_9000")
|
||||
assert identity.sub == "dave.bowman-1+2@hal_9000"
|
||||
|
||||
|
||||
def test_models_identities_sub_spaces():
|
||||
"""The sub field should not accept spaces."""
|
||||
"""The subfield should not accept spaces."""
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
factories.IdentityFactory(sub="a b")
|
||||
|
||||
@@ -171,12 +171,12 @@ def test_models_identities_sub_spaces():
|
||||
|
||||
|
||||
def test_models_identities_sub_upper_case():
|
||||
"""The sub field should accept upper case characters."""
|
||||
"""The subfield should accept upper case characters."""
|
||||
identity = factories.IdentityFactory(sub="John")
|
||||
assert identity.sub == "John"
|
||||
|
||||
|
||||
def test_models_identities_sub_ascii():
|
||||
"""The sub field should accept non ASCII letters."""
|
||||
"""The subfield should accept non ASCII letters."""
|
||||
identity = factories.IdentityFactory(sub="rené")
|
||||
assert identity.sub == "rené"
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
"""Unit tests for the `oidc_user_getter` function."""
|
||||
import pytest
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken
|
||||
from rest_framework_simplejwt.tokens import AccessToken
|
||||
|
||||
from core import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_models_oidc_user_getter_existing_user_no_email(django_assert_num_queries):
|
||||
"""
|
||||
When a valid token is passed, an existing user matching the token's sub should be returned.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
factories.IdentityFactory(user=identity.user) # another identity for the user
|
||||
token = AccessToken()
|
||||
token["sub"] = str(identity.sub)
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
user = models.oidc_user_getter(token)
|
||||
|
||||
identity.refresh_from_db()
|
||||
assert user == identity.user
|
||||
|
||||
|
||||
def test_models_oidc_user_getter_existing_user_with_email(django_assert_num_queries):
|
||||
"""
|
||||
When the valid token passed contains an email and targets an existing user,
|
||||
it should update the email on the identity but not on the user.
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
factories.IdentityFactory(user=identity.user) # another identity for the user
|
||||
user_email = identity.user.email
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
token = AccessToken()
|
||||
token["sub"] = str(identity.sub)
|
||||
|
||||
# Only 1 query if the email has not changed
|
||||
token["email"] = identity.email
|
||||
with django_assert_num_queries(1):
|
||||
user = models.oidc_user_getter(token)
|
||||
|
||||
# Additional update query if the email has changed
|
||||
new_email = "people@example.com"
|
||||
token["email"] = new_email
|
||||
with django_assert_num_queries(2):
|
||||
user = models.oidc_user_getter(token)
|
||||
|
||||
identity.refresh_from_db()
|
||||
assert identity.email == new_email
|
||||
|
||||
assert models.User.objects.count() == 1
|
||||
assert user == identity.user
|
||||
assert user.email == user_email
|
||||
|
||||
|
||||
def test_models_oidc_user_getter_new_user_no_email():
|
||||
"""
|
||||
When a valid token is passed, a user should be created if the sub
|
||||
does not match any existing user.
|
||||
"""
|
||||
token = AccessToken()
|
||||
token["sub"] = "123"
|
||||
|
||||
user = models.oidc_user_getter(token)
|
||||
|
||||
identity = user.identities.get()
|
||||
assert identity.sub == "123"
|
||||
assert identity.email is None
|
||||
|
||||
assert user.email is None
|
||||
assert user.password == "!"
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_models_oidc_user_getter_new_user_with_email():
|
||||
"""
|
||||
When the valid token passed contains an email and a new user is created,
|
||||
the email should be set on the user and on the identity.
|
||||
"""
|
||||
email = "people@example.com"
|
||||
token = AccessToken()
|
||||
token["sub"] = "123"
|
||||
token["email"] = email
|
||||
|
||||
user = models.oidc_user_getter(token)
|
||||
|
||||
identity = user.identities.get()
|
||||
assert identity.sub == "123"
|
||||
assert identity.email == email
|
||||
|
||||
assert user.email == email
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_models_oidc_user_getter_invalid_token(django_assert_num_queries):
|
||||
"""The token passed in argument should contain the configured user id claim."""
|
||||
token = AccessToken()
|
||||
|
||||
with django_assert_num_queries(0), pytest.raises(
|
||||
InvalidToken, match="Token contained no recognizable user identification"
|
||||
):
|
||||
models.oidc_user_getter(token)
|
||||
|
||||
assert models.User.objects.exists() is False
|
||||
@@ -77,7 +77,7 @@ def test_models_users_profile_owned_by_other():
|
||||
|
||||
|
||||
def test_models_users_send_mail_main_existing():
|
||||
"""The "email_user' method should send mail to the user's main email address."""
|
||||
"""The 'email_user' method should send mail to the user's main email address."""
|
||||
main_email = factories.IdentityFactory(email="dave@example.com")
|
||||
user = main_email.user
|
||||
factories.IdentityFactory.create_batch(2, user=user)
|
||||
@@ -91,7 +91,7 @@ def test_models_users_send_mail_main_existing():
|
||||
|
||||
|
||||
def test_models_users_send_mail_main_missing():
|
||||
"""The "email_user' method should fail if the user has no email address."""
|
||||
"""The 'email_user' method should fail if the user has no email address."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
with pytest.raises(models.Identity.DoesNotExist) as excinfo:
|
||||
|
||||
@@ -8,8 +8,6 @@ from core import factories
|
||||
|
||||
from people.settings import REST_FRAMEWORK # pylint: disable=E0611
|
||||
|
||||
from .utils import OIDCToken
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@@ -19,9 +17,9 @@ def test_throttle():
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
endpoint = "/api/v1.0/users/"
|
||||
|
||||
# loop to activate throttle protection
|
||||
@@ -29,8 +27,8 @@ def test_throttle():
|
||||
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["burst"].replace("/minute", "")
|
||||
)
|
||||
for _ in range(0, throttle_limit):
|
||||
client.get(endpoint, HTTP_AUTHORIZATION=f"Bearer {jwt_token}")
|
||||
client.get(endpoint)
|
||||
|
||||
# this call should err
|
||||
response = client.get(endpoint, HTTP_AUTHORIZATION=f"Bearer {jwt_token}")
|
||||
response = client.get(endpoint)
|
||||
assert response.status_code == 429 # too many requests
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tokens for Magnify's core app."""
|
||||
"""Tokens for People's core app."""
|
||||
from rest_framework_simplejwt.settings import api_settings
|
||||
from rest_framework_simplejwt.tokens import Token
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from django.conf import settings
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from mozilla_django_oidc.urls import urlpatterns as oidc_urls
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from core.api import viewsets
|
||||
@@ -26,6 +27,7 @@ urlpatterns = [
|
||||
include(
|
||||
[
|
||||
*router.urls,
|
||||
*oidc_urls,
|
||||
re_path(
|
||||
r"^teams/(?P<team_id>[0-9a-z-]*)/",
|
||||
include(team_related_router.urls),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Django settings for People project.
|
||||
Django's settings for People project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.1.5.
|
||||
|
||||
@@ -175,13 +175,15 @@ class Base(Configuration):
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"dockerflow.django.middleware.DockerflowMiddleware",
|
||||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"core.authentication.OIDCAuthenticationBackend",
|
||||
]
|
||||
|
||||
# Django applications from the highest priority to the lowest
|
||||
# Django's applications from the highest priority to the lowest
|
||||
INSTALLED_APPS = [
|
||||
# People
|
||||
"core",
|
||||
@@ -202,6 +204,8 @@ class Base(Configuration):
|
||||
"django.contrib.sites",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
# OIDC third party
|
||||
"mozilla_django_oidc",
|
||||
]
|
||||
|
||||
# Cache
|
||||
@@ -211,7 +215,8 @@ class Base(Configuration):
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"core.authentication.DelegatedJWTAuthentication",
|
||||
"mozilla_django_oidc.contrib.drf.OIDCAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
),
|
||||
"DEFAULT_PARSER_CLASSES": [
|
||||
"rest_framework.parsers.JSONParser",
|
||||
@@ -242,34 +247,6 @@ class Base(Configuration):
|
||||
"REDOC_DIST": "SIDECAR",
|
||||
}
|
||||
|
||||
# Simple JWT
|
||||
SIMPLE_JWT = {
|
||||
"ALGORITHM": values.Value(
|
||||
"RS256", environ_name="SIMPLE_JWT_ALGORITHM", environ_prefix=None
|
||||
),
|
||||
"JWK_URL": values.Value(
|
||||
None, environ_name="SIMPLE_JWT_JWK_URL", environ_prefix=None
|
||||
),
|
||||
"SIGNING_KEY": values.Value(
|
||||
None, environ_name="SIMPLE_JWT_SIGNING_KEY", environ_prefix=None
|
||||
),
|
||||
"VERIFYING_KEY": values.Value(
|
||||
None, environ_name="SIMPLE_JWT_VERIFYING_KEY", environ_prefix=None
|
||||
),
|
||||
"AUTH_HEADER_TYPES": ("Bearer",),
|
||||
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
|
||||
"TOKEN_TYPE_CLAIM": "typ",
|
||||
"USER_ID_FIELD": "sub",
|
||||
"USER_ID_CLAIM": "sub",
|
||||
"AUTH_TOKEN_CLASSES": ("core.tokens.BearerToken",),
|
||||
}
|
||||
|
||||
JWT_USER_GETTER = values.Value(
|
||||
"core.models.oidc_user_getter",
|
||||
environ_name="PEOPLE_JWT_USER_GETTER",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Mail
|
||||
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
|
||||
EMAIL_HOST = values.Value(None)
|
||||
@@ -284,7 +261,7 @@ class Base(Configuration):
|
||||
# CORS
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False)
|
||||
CORS_ALLOWED_ORIGINS = values.ListValue([])
|
||||
CORS_ALLOWED_ORIGINS = values.ListValue([], environ_name="CORS_ALLOWED_ORIGINS")
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
|
||||
|
||||
# Sentry
|
||||
@@ -299,6 +276,61 @@ class Base(Configuration):
|
||||
CELERY_BROKER_URL = values.Value("redis://redis:6379/0")
|
||||
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({})
|
||||
|
||||
# Session
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 12 # 12 hours to match Agent Connect
|
||||
|
||||
# OIDC - Authorization Code Flow
|
||||
OIDC_CREATE_USER = values.BooleanValue(
|
||||
default=True,
|
||||
environ_name="OIDC_CREATE_USER",
|
||||
)
|
||||
OIDC_RP_SIGN_ALGO = values.Value(
|
||||
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
|
||||
)
|
||||
OIDC_RP_CLIENT_ID = values.Value(
|
||||
"people", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
|
||||
)
|
||||
OIDC_RP_CLIENT_SECRET = values.Value(
|
||||
environ_name="OIDC_RP_CLIENT_SECRET", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_JWKS_ENDPOINT = values.Value(
|
||||
environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value(
|
||||
environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_TOKEN_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_USER_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue(
|
||||
{}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None
|
||||
)
|
||||
OIDC_RP_SCOPES = values.Value(
|
||||
"openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
|
||||
)
|
||||
LOGIN_REDIRECT_URL = values.Value(
|
||||
None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None
|
||||
)
|
||||
LOGIN_REDIRECT_URL_FAILURE = values.Value(
|
||||
None, environ_name="LOGIN_REDIRECT_URL_FAILURE", environ_prefix=None
|
||||
)
|
||||
LOGOUT_REDIRECT_URL = values.Value(
|
||||
None, environ_name="LOGOUT_REDIRECT_URL", environ_prefix=None
|
||||
)
|
||||
OIDC_USE_NONCE = values.BooleanValue(
|
||||
default=True, environ_name="OIDC_USE_NONCE", environ_prefix=None
|
||||
)
|
||||
OIDC_REDIRECT_REQUIRE_HTTPS = values.BooleanValue(
|
||||
default=False, environ_name="OIDC_REDIRECT_REQUIRE_HTTPS", environ_prefix=None
|
||||
)
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS = values.ListValue(
|
||||
default=[], environ_name="OIDC_REDIRECT_ALLOWED_HOSTS", environ_prefix=None
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
@@ -394,10 +426,6 @@ class Development(Base):
|
||||
class Test(Base):
|
||||
"""Test environment settings"""
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"USER_ID_FIELD": "sub",
|
||||
"USER_ID_CLAIM": "sub",
|
||||
}
|
||||
LOGGING = values.DictValue(
|
||||
{
|
||||
"version": 1,
|
||||
@@ -521,3 +549,20 @@ class PreProduction(Production):
|
||||
|
||||
nota bene: it should inherit from the Production environment.
|
||||
"""
|
||||
|
||||
|
||||
class Demo(Production):
|
||||
"""
|
||||
Demonstration environment settings
|
||||
|
||||
nota bene: it should inherit from the Production environment.
|
||||
"""
|
||||
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ dependencies = [
|
||||
"sentry-sdk==1.40.3",
|
||||
"url-normalize==1.4.3",
|
||||
"whitenoise==6.6.0",
|
||||
"mozilla-django-oidc==4.0.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8071/api/v1.0/
|
||||
NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080/
|
||||
NEXT_PUBLIC_KEYCLOAK_REALM=people
|
||||
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=people-front
|
||||
NEXT_PUBLIC_KEYCLOAK_LOGIN=true
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_URL=http://kubernetes-service-name-wip:8071/api/v1.0/
|
||||
@@ -39,7 +39,6 @@
|
||||
"fetch-mock": "9.11.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"keycloak-js": "23.0.6",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.2.5",
|
||||
"stylelint": "16.2.1",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { fetchAPI } from '@/api';
|
||||
import { useAuthStore } from '@/features/';
|
||||
|
||||
import { fetchAPI } from '../fetchApi';
|
||||
|
||||
describe('fetchAPI', () => {
|
||||
beforeEach(() => {
|
||||
process.env.NEXT_PUBLIC_API_URL = 'http://some.api.url/api/v1.0/';
|
||||
@@ -20,29 +19,25 @@ describe('fetchAPI', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('adds the BEARER automatically', () => {
|
||||
useAuthStore.setState({ token: 'my-token' });
|
||||
|
||||
it('adds the credentials automatically', () => {
|
||||
fetchMock.mock('http://some.api.url/api/v1.0/some/url', 200);
|
||||
|
||||
void fetchAPI('some/url', { body: 'some body' });
|
||||
|
||||
expect(fetchMock.lastOptions()).toEqual({
|
||||
body: 'some body',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authorization: 'Bearer my-token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('logout if 401 response', async () => {
|
||||
useAuthStore.setState({ token: 'my-token' });
|
||||
|
||||
fetchMock.mock('http://some.api.url/api/v1.0/some/url', 401);
|
||||
|
||||
await fetchAPI('some/url');
|
||||
|
||||
expect(useAuthStore.getState().token).toBeNull();
|
||||
expect(useAuthStore.getState().userData).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,17 +2,19 @@ import { useAuthStore } from '@/features/';
|
||||
|
||||
export const fetchAPI = async (input: string, init?: RequestInit) => {
|
||||
const apiUrl = `${process.env.NEXT_PUBLIC_API_URL}${input}`;
|
||||
const { token, logout } = useAuthStore.getState();
|
||||
const { logout } = useAuthStore.getState();
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...init?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
// todo - handle 401, redirect to login screen
|
||||
// todo - please have a look to this documentation page https://mozilla-django-oidc.readthedocs.io/en/stable/xhr.html
|
||||
response.status === 401 && logout();
|
||||
|
||||
return response;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { fetchAPI } from '@/api';
|
||||
|
||||
/**
|
||||
* Represents user data retrieved from the API.
|
||||
* This interface is incomplete, and will be
|
||||
* refactored in a near future.
|
||||
*
|
||||
* @interface UserData
|
||||
* @property {string} email - The email of the user.
|
||||
*/
|
||||
export interface UserData {
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves the current user's data from the API.
|
||||
* This function is called during frontend initialization to check
|
||||
* the user's authentication status through a session cookie.
|
||||
*
|
||||
* @async
|
||||
* @function getMe
|
||||
* @throws {Error} Throws an error if the API request fails.
|
||||
* @returns {Promise<UserData>} A promise that resolves to the user data.
|
||||
*/
|
||||
export const getMe = async (): Promise<UserData> => {
|
||||
const response = await fetchAPI(`users/me`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Couldn't fetch user data: ${response.statusText}`);
|
||||
}
|
||||
return response.json() as Promise<UserData>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './getMe';
|
||||
@@ -1,23 +0,0 @@
|
||||
import Keycloak, { KeycloakConfig } from 'keycloak-js';
|
||||
|
||||
const keycloakConfig: KeycloakConfig = {
|
||||
url: process.env.NEXT_PUBLIC_KEYCLOAK_URL,
|
||||
realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM || '',
|
||||
clientId: process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || '',
|
||||
};
|
||||
|
||||
export const initKeycloak = (onAuthSuccess: (_token?: string) => void) => {
|
||||
const keycloak = new Keycloak(keycloakConfig);
|
||||
keycloak
|
||||
.init({
|
||||
onLoad: 'login-required',
|
||||
})
|
||||
.then((authenticated) => {
|
||||
if (authenticated) {
|
||||
onAuthSuccess(keycloak.token);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error('Failed to initialize Keycloak.');
|
||||
});
|
||||
};
|
||||
@@ -1,40 +1,47 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { initKeycloak } from './keycloak';
|
||||
import { UserData, getMe } from '@/features/auth/api';
|
||||
|
||||
interface AuthStore {
|
||||
authenticated: boolean;
|
||||
initAuth: () => void;
|
||||
initialized: boolean;
|
||||
logout: () => void;
|
||||
token: string | null;
|
||||
userData?: UserData;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
authenticated: false,
|
||||
initialized: false,
|
||||
token: null,
|
||||
userData: undefined,
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthStore>((set) => ({
|
||||
authenticated: initialState.authenticated,
|
||||
initialized: initialState.initialized,
|
||||
token: initialState.token,
|
||||
userData: initialState.userData,
|
||||
|
||||
initAuth: () =>
|
||||
set((state) => {
|
||||
if (process.env.NEXT_PUBLIC_KEYCLOAK_LOGIN && !state.initialized) {
|
||||
initKeycloak((token) => set({ authenticated: true, token }));
|
||||
return { initialized: true };
|
||||
if (state.initialized) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Implement OIDC production authentication
|
||||
*/
|
||||
|
||||
getMe()
|
||||
.then((data: UserData) => {
|
||||
set({ initialized: true, authenticated: true, userData: data });
|
||||
return { initialized: true };
|
||||
})
|
||||
.catch(() => {
|
||||
// todo - implement a proper login screen to prevent automatic navigation.
|
||||
window.location.replace(
|
||||
new URL('authenticate', process.env.NEXT_PUBLIC_API_URL).href,
|
||||
);
|
||||
});
|
||||
return {};
|
||||
}),
|
||||
|
||||
// todo - implement a proper logout to end session
|
||||
// todo - please follow AC instructions here https://github.com/france-connect/Documentation-AgentConnect/blob/26685161a2031f7145e02bede8c4d0aae97ba105/doc_fs/technique_fca/endpoints.md?plain=1#L324
|
||||
logout: () => {
|
||||
set(initialState);
|
||||
},
|
||||
|
||||
@@ -3866,11 +3866,6 @@ balanced-match@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
|
||||
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
|
||||
|
||||
base64-js@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
@@ -6533,11 +6528,6 @@ jest@29.7.0:
|
||||
import-local "^3.0.2"
|
||||
jest-cli "^29.7.0"
|
||||
|
||||
js-sha256@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.10.1.tgz#b40104ba1368e823fdd5f41b66b104b15a0da60d"
|
||||
integrity sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@@ -6663,20 +6653,6 @@ jsonfile@^6.0.1:
|
||||
object.assign "^4.1.4"
|
||||
object.values "^1.1.6"
|
||||
|
||||
jwt-decode@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
|
||||
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
|
||||
|
||||
keycloak-js@23.0.6:
|
||||
version "23.0.6"
|
||||
resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-23.0.6.tgz#a494cc1eddf5462322a9f2247b381bc22fb43747"
|
||||
integrity sha512-Pn7iIEHPn7BcQFCbViKRv+8+v9l82oWNRVQr9wQGjp2BNEl9JpTsXjp84xQjwzaLKghG7QV7VwZrWBhiXJeM0Q==
|
||||
dependencies:
|
||||
base64-js "^1.5.1"
|
||||
js-sha256 "^0.10.1"
|
||||
jwt-decode "^4.0.0"
|
||||
|
||||
keyv@^4.5.3, keyv@^4.5.4:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||
|
||||
Reference in New Issue
Block a user