diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index 38a3d559a0..ff2a489c5f 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -11,6 +11,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from InvenTree.mixins import ListCreateAPI
+from InvenTree.permissions import RolePermission
from .status import is_worker_running
from .version import (inventreeApiVersion, inventreeInstanceName,
@@ -182,7 +183,10 @@ class APIDownloadMixin:
class AttachmentMixin:
"""Mixin for creating attachment objects, and ensuring the user information is saved correctly."""
- permission_classes = [permissions.IsAuthenticated]
+ permission_classes = [
+ permissions.IsAuthenticated,
+ RolePermission,
+ ]
filter_backends = [
DjangoFilterBackend,
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index acf585848d..142f0e97ac 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -841,12 +841,13 @@ for app in SOCIAL_BACKENDS:
SOCIALACCOUNT_PROVIDERS = CONFIG.get('social_providers', [])
+SOCIALACCOUNT_STORE_TOKENS = True
+
# settings for allauth
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', CONFIG.get('login_confirm_days', 3))
-
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', CONFIG.get('login_attempts', 5))
-
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
+ACCOUNT_PREVENT_ENUMERATION = True
# override forms / adapters
ACCOUNT_FORMS = {
diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index 248fcec580..f9bac157a8 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -172,7 +172,7 @@
{% trans "Allocate Stock to Build" %}
{% include "spacer.html" %}
- {% if build.active and build.has_untracked_bom_items %}
+ {% if roles.build.add and build.active and build.has_untracked_bom_items %}
@@ -229,7 +229,7 @@
{% trans "Incomplete Build Outputs" %}
{% include "spacer.html" %}
- {% if build.active %}
+ {% if roles.build.add and build.active %}
@@ -249,12 +249,16 @@
diff --git a/InvenTree/templates/js/translated/attachment.js b/InvenTree/templates/js/translated/attachment.js
index 991d3efeba..e070cc65b1 100644
--- a/InvenTree/templates/js/translated/attachment.js
+++ b/InvenTree/templates/js/translated/attachment.js
@@ -136,19 +136,56 @@ function loadAttachmentTable(url, options) {
var table = options.table || '#attachment-table';
- setupFilterList('attachments', $(table), '#filter-list-attachments');
+ var permissions = {};
- addAttachmentButtonCallbacks(url, options.fields || {});
+ // First we determine which permissions the user has for this attachment table
+ $.ajax({
+ url: url,
+ async: false,
+ type: 'OPTIONS',
+ contentType: 'application/json',
+ dataType: 'json',
+ accepts: {
+ json: 'application/json',
+ },
+ success: function(response) {
+ if (response.actions.DELETE) {
+ permissions.delete = true;
+ }
- // Add callback for the 'multi delete' button
- $('#multi-attachment-delete').click(function() {
- var attachments = getTableData(table);
-
- if (attachments.length > 0) {
- deleteAttachments(attachments, url, options);
+ if (response.actions.POST) {
+ permissions.change = true;
+ permissions.add = true;
+ }
+ },
+ error: function(xhr) {
+ showApiError(xhr, url);
}
});
+ setupFilterList('attachments', $(table), '#filter-list-attachments');
+
+ if (permissions.add) {
+ addAttachmentButtonCallbacks(url, options.fields || {});
+ } else {
+ // Hide the buttons
+ $('#new-attachment').hide();
+ $('#new-attachment-link').hide();
+ }
+
+ if (permissions.delete) {
+ // Add callback for the 'multi delete' button
+ $('#multi-attachment-delete').click(function() {
+ var attachments = getTableData(table);
+
+ if (attachments.length > 0) {
+ deleteAttachments(attachments, url, options);
+ }
+ });
+ } else {
+ $('#multi-attachment-actions').hide();
+ }
+
$(table).inventreeTable({
url: url,
name: options.name || 'attachments',
@@ -162,32 +199,36 @@ function loadAttachmentTable(url, options) {
onPostBody: function() {
// Add callback for 'edit' button
- $(table).find('.button-attachment-edit').click(function() {
- var pk = $(this).attr('pk');
+ if (permissions.change) {
+ $(table).find('.button-attachment-edit').click(function() {
+ var pk = $(this).attr('pk');
- constructForm(`${url}${pk}/`, {
- fields: {
- link: {},
- comment: {},
- },
- processResults: function(data, fields, opts) {
- // Remove the "link" field if the attachment is a file!
- if (data.attachment) {
- delete opts.fields.link;
- }
- },
- onSuccess: reloadAttachmentTable,
- title: '{% trans "Edit Attachment" %}',
+ constructForm(`${url}${pk}/`, {
+ fields: {
+ link: {},
+ comment: {},
+ },
+ processResults: function(data, fields, opts) {
+ // Remove the "link" field if the attachment is a file!
+ if (data.attachment) {
+ delete opts.fields.link;
+ }
+ },
+ onSuccess: reloadAttachmentTable,
+ title: '{% trans "Edit Attachment" %}',
+ });
});
- });
+ }
- // Add callback for 'delete' button
- $(table).find('.button-attachment-delete').click(function() {
- var pk = $(this).attr('pk');
+ if (permissions.delete) {
+ // Add callback for 'delete' button
+ $(table).find('.button-attachment-delete').click(function() {
+ var pk = $(this).attr('pk');
- var attachment = $(table).bootstrapTable('getRowByUniqueId', pk);
- deleteAttachments([attachment], url, options);
- });
+ var attachment = $(table).bootstrapTable('getRowByUniqueId', pk);
+ deleteAttachments([attachment], url, options);
+ });
+ }
},
columns: [
{
@@ -261,19 +302,23 @@ function loadAttachmentTable(url, options) {
html = `
`;
- html += makeIconButton(
- 'fa-edit icon-blue',
- 'button-attachment-edit',
- row.pk,
- '{% trans "Edit attachment" %}',
- );
+ if (permissions.change) {
+ html += makeIconButton(
+ 'fa-edit icon-blue',
+ 'button-attachment-edit',
+ row.pk,
+ '{% trans "Edit attachment" %}',
+ );
+ }
- html += makeIconButton(
- 'fa-trash-alt icon-red',
- 'button-attachment-delete',
- row.pk,
- '{% trans "Delete attachment" %}',
- );
+ if (permissions.delete) {
+ html += makeIconButton(
+ 'fa-trash-alt icon-red',
+ 'button-attachment-delete',
+ row.pk,
+ '{% trans "Delete attachment" %}',
+ );
+ }
html += `
`;
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index ca9ee7c240..b3bafb2657 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -222,6 +222,11 @@ class RuleSet(models.Model):
@classmethod
def check_table_permission(cls, user, table, permission):
"""Check if the provided user has the specified permission against the table."""
+
+ # Superuser knows no bounds
+ if user.is_superuser:
+ return True
+
# If the table does *not* require permissions
if table in cls.RULESET_IGNORE:
return True
diff --git a/requirements.txt b/requirements.txt
index 765c774284..eb908235ee 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,7 @@ coverage==5.3 # Unit test coverage
coveralls==2.1.2 # Coveralls linking (for Travis)
cryptography==3.4.8 # Cryptography support
django-admin-shell==0.1.2 # Python shell for the admin interface
-django-allauth==0.45.0 # SSO for external providers via OpenID
+django-allauth==0.48.0 # SSO for external providers via OpenID
django-allauth-2fa==0.9 # MFA / 2FA
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
django-cors-headers==3.2.0 # CORS headers extension for DRF