diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index ca4d37dc7c..38a3d559a0 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -6,10 +6,12 @@ from django.http import JsonResponse from django.utils.translation import gettext_lazy as _ from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics, permissions +from rest_framework import filters, permissions from rest_framework.response import Response from rest_framework.serializers import ValidationError +from InvenTree.mixins import ListCreateAPI + from .status import is_worker_running from .version import (inventreeApiVersion, inventreeInstanceName, inventreeVersion) @@ -134,7 +136,7 @@ class BulkDeleteMixin: ) -class ListCreateDestroyAPIView(BulkDeleteMixin, generics.ListCreateAPIView): +class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI): """Custom API endpoint which provides BulkDelete functionality in addition to List and Create""" ... diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index 4e3d38d1b0..6c82137d3b 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -6,7 +6,7 @@ import InvenTree.status from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, SalesOrderStatus, StockHistoryCode, StockStatus) -from users.models import RuleSet +from users.models import RuleSet, check_user_role def health_status(request): @@ -83,31 +83,13 @@ def user_roles(request): roles = { } - if user.is_superuser: - for ruleset in RuleSet.RULESET_MODELS.keys(): # pragma: no cover - roles[ruleset] = { - 'view': True, - 'add': True, - 'change': True, - 'delete': True, - } - else: - for group in user.groups.all(): - for rule in group.rule_sets.all(): + for role in RuleSet.RULESET_MODELS.keys(): - # Ensure the role name is in the dict - if rule.name not in roles: - roles[rule.name] = { - 'view': user.is_superuser, - 'add': user.is_superuser, - 'change': user.is_superuser, - 'delete': user.is_superuser - } + permissions = {} - # Roles are additive across groups - roles[rule.name]['view'] |= rule.can_view - roles[rule.name]['add'] |= rule.can_add - roles[rule.name]['change'] |= rule.can_change - roles[rule.name]['delete'] |= rule.can_delete + for perm in ['view', 'add', 'change', 'delete']: + permissions[perm] = user.is_superuser or check_user_role(user, role, perm) + + roles[role] = permissions return {'roles': roles} diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 6116cd0b36..d0d725c2f8 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -124,21 +124,31 @@ class EditUserForm(HelperForm): class SetPasswordForm(HelperForm): """Form for setting user password.""" - enter_password = forms.CharField(max_length=100, - min_length=8, - required=True, - initial='', - widget=forms.PasswordInput(attrs={'autocomplete': 'off'}), - label=_('Enter password'), - help_text=_('Enter new password')) + enter_password = forms.CharField( + max_length=100, + min_length=8, + required=True, + initial='', + widget=forms.PasswordInput(attrs={'autocomplete': 'off'}), + label=_('Enter password'), + help_text=_('Enter new password') + ) - confirm_password = forms.CharField(max_length=100, - min_length=8, - required=True, - initial='', - widget=forms.PasswordInput(attrs={'autocomplete': 'off'}), - label=_('Confirm password'), - help_text=_('Confirm new password')) + confirm_password = forms.CharField( + max_length=100, + min_length=8, + required=True, + initial='', + widget=forms.PasswordInput(attrs={'autocomplete': 'off'}), + label=_('Confirm password'), + help_text=_('Confirm new password') + ) + + old_password = forms.CharField( + label=_("Old password"), + strip=False, + widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'autofocus': True}), + ) class Meta: """Metaclass options.""" @@ -146,7 +156,8 @@ class SetPasswordForm(HelperForm): model = User fields = [ 'enter_password', - 'confirm_password' + 'confirm_password', + 'old_password', ] diff --git a/InvenTree/InvenTree/mixins.py b/InvenTree/InvenTree/mixins.py new file mode 100644 index 0000000000..584b3ac5ed --- /dev/null +++ b/InvenTree/InvenTree/mixins.py @@ -0,0 +1,90 @@ +"""Mixins for (API) views in the whole project.""" + +from bleach import clean +from rest_framework import generics, status +from rest_framework.response import Response + + +class CleanMixin(): + """Model mixin class which cleans inputs.""" + + # Define a map of fields avaialble for import + SAFE_FIELDS = {} + + def create(self, request, *args, **kwargs): + """Override to clean data before processing it.""" + serializer = self.get_serializer(data=self.clean_data(request.data)) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def update(self, request, *args, **kwargs): + """Override to clean data before processing it.""" + partial = kwargs.pop('partial', False) + instance = self.get_object() + serializer = self.get_serializer(instance, data=self.clean_data(request.data), partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + return Response(serializer.data) + + def clean_data(self, data: dict) -> dict: + """Clean / sanitize data. + + This uses mozillas bleach under the hood to disable certain html tags by + encoding them - this leads to script tags etc. to not work. + The results can be longer then the input; might make some character combinations + `ugly`. Prevents XSS on the server-level. + + Args: + data (dict): Data that should be sanatized. + + Returns: + dict: Profided data sanatized; still in the same order. + """ + clean_data = {} + for k, v in data.items(): + if isinstance(v, str): + ret = clean(v) + elif isinstance(v, dict): + ret = self.clean_data(v) + else: + ret = v + clean_data[k] = ret + return clean_data + + +class ListAPI(generics.ListAPIView): + """View for list API.""" + + +class ListCreateAPI(CleanMixin, generics.ListCreateAPIView): + """View for list and create API.""" + + +class CreateAPI(CleanMixin, generics.CreateAPIView): + """View for create API.""" + + +class RetrieveAPI(generics.RetrieveAPIView): + """View for retreive API.""" + pass + + +class RetrieveUpdateAPI(CleanMixin, generics.RetrieveUpdateAPIView): + """View for retrieve and update API.""" + pass + + +class RetrieveUpdateDestroyAPI(CleanMixin, generics.RetrieveUpdateDestroyAPIView): + """View for retrieve, update and destroy API.""" + + +class UpdateAPI(CleanMixin, generics.UpdateAPIView): + """View for update API.""" diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 0a01937e16..acf585848d 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -309,6 +309,11 @@ if DEBUG_TOOLBAR_ENABLED: # pragma: no cover INSTALLED_APPS.append('debug_toolbar') MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') + DEBUG_TOOLBAR_CONFIG = { + 'RESULTS_CACHE_SIZE': 100, + 'OBSERVE_REQUEST_CALLBACK': lambda x: False, + } + # Internal IP addresses allowed to see the debug toolbar INTERNAL_IPS = [ '127.0.0.1', diff --git a/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table.min.css b/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table.min.css new file mode 100644 index 0000000000..0fa2968e0a --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table.min.css @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.18.3 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;width:auto!important;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:LOADING;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:'\2B05'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:'\27A1'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes LOADING{0%{opacity:0}50%{opacity:1}to{opacity:0}} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table.min.js b/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table.min.js new file mode 100644 index 0000000000..8c88245fbb --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table.min.js @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.18.3 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).BootstrapTable=e(t.jQuery)}(this,(function(t){"use strict";function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var i=e(t);function n(t){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function a(t,e){for(var i=0;it.length)&&(e=t.length);for(var i=0,n=new Array(e);i=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,r=!1;return{s:function(){i=t[Symbol.iterator]()},n:function(){var t=i.next();return s=t.done,t},e:function(t){r=!0,a=t},f:function(){try{s||null==i.return||i.return()}finally{if(r)throw a}}}}var d="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function f(t,e){return t(e={exports:{}},e.exports),e.exports}var p=function(t){return t&&t.Math==Math&&t},g=p("object"==typeof globalThis&&globalThis)||p("object"==typeof window&&window)||p("object"==typeof self&&self)||p("object"==typeof d&&d)||function(){return this}()||Function("return this")(),v=function(t){try{return!!t()}catch(t){return!0}},b=!v((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),m={}.propertyIsEnumerable,y=Object.getOwnPropertyDescriptor,w={f:y&&!m.call({1:2},1)?function(t){var e=y(this,t);return!!e&&e.enumerable}:m},S=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},x={}.toString,k=function(t){return x.call(t).slice(8,-1)},O="".split,C=v((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==k(t)?O.call(t,""):Object(t)}:Object,T=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},P=function(t){return C(T(t))},I=function(t){return"object"==typeof t?null!==t:"function"==typeof t},A=function(t,e){if(!I(t))return t;var i,n;if(e&&"function"==typeof(i=t.toString)&&!I(n=i.call(t)))return n;if("function"==typeof(i=t.valueOf)&&!I(n=i.call(t)))return n;if(!e&&"function"==typeof(i=t.toString)&&!I(n=i.call(t)))return n;throw TypeError("Can't convert object to primitive value")},$={}.hasOwnProperty,E=function(t,e){return $.call(t,e)},R=g.document,j=I(R)&&I(R.createElement),_=function(t){return j?R.createElement(t):{}},N=!b&&!v((function(){return 7!=Object.defineProperty(_("div"),"a",{get:function(){return 7}}).a})),F=Object.getOwnPropertyDescriptor,D={f:b?F:function(t,e){if(t=P(t),e=A(e,!0),N)try{return F(t,e)}catch(t){}if(E(t,e))return S(!w.f.call(t,e),t[e])}},V=function(t){if(!I(t))throw TypeError(String(t)+" is not an object");return t},B=Object.defineProperty,L={f:b?B:function(t,e,i){if(V(t),e=A(e,!0),V(i),N)try{return B(t,e,i)}catch(t){}if("get"in i||"set"in i)throw TypeError("Accessors not supported");return"value"in i&&(t[e]=i.value),t}},H=b?function(t,e,i){return L.f(t,e,S(1,i))}:function(t,e,i){return t[e]=i,t},M=function(t,e){try{H(g,t,e)}catch(i){g[t]=e}return e},U="__core-js_shared__",z=g[U]||M(U,{}),q=Function.toString;"function"!=typeof z.inspectSource&&(z.inspectSource=function(t){return q.call(t)});var W,G,K,Y=z.inspectSource,X=g.WeakMap,J="function"==typeof X&&/native code/.test(Y(X)),Q=f((function(t){(t.exports=function(t,e){return z[t]||(z[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.9.1",mode:"global",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"})})),Z=0,tt=Math.random(),et=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++Z+tt).toString(36)},it=Q("keys"),nt=function(t){return it[t]||(it[t]=et(t))},ot={},at=g.WeakMap;if(J){var st=z.state||(z.state=new at),rt=st.get,lt=st.has,ct=st.set;W=function(t,e){return e.facade=t,ct.call(st,t,e),e},G=function(t){return rt.call(st,t)||{}},K=function(t){return lt.call(st,t)}}else{var ht=nt("state");ot[ht]=!0,W=function(t,e){return e.facade=t,H(t,ht,e),e},G=function(t){return E(t,ht)?t[ht]:{}},K=function(t){return E(t,ht)}}var ut={set:W,get:G,has:K,enforce:function(t){return K(t)?G(t):W(t,{})},getterFor:function(t){return function(e){var i;if(!I(e)||(i=G(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return i}}},dt=f((function(t){var e=ut.get,i=ut.enforce,n=String(String).split("String");(t.exports=function(t,e,o,a){var s,r=!!a&&!!a.unsafe,l=!!a&&!!a.enumerable,c=!!a&&!!a.noTargetGet;"function"==typeof o&&("string"!=typeof e||E(o,"name")||H(o,"name",e),(s=i(o)).source||(s.source=n.join("string"==typeof e?e:""))),t!==g?(r?!c&&t[e]&&(l=!0):delete t[e],l?t[e]=o:H(t,e,o)):l?t[e]=o:M(e,o)})(Function.prototype,"toString",(function(){return"function"==typeof this&&e(this).source||Y(this)}))})),ft=g,pt=function(t){return"function"==typeof t?t:void 0},gt=function(t,e){return arguments.length<2?pt(ft[t])||pt(g[t]):ft[t]&&ft[t][e]||g[t]&&g[t][e]},vt=Math.ceil,bt=Math.floor,mt=function(t){return isNaN(t=+t)?0:(t>0?bt:vt)(t)},yt=Math.min,wt=function(t){return t>0?yt(mt(t),9007199254740991):0},St=Math.max,xt=Math.min,kt=function(t,e){var i=mt(t);return i<0?St(i+e,0):xt(i,e)},Ot=function(t){return function(e,i,n){var o,a=P(e),s=wt(a.length),r=kt(n,s);if(t&&i!=i){for(;s>r;)if((o=a[r++])!=o)return!0}else for(;s>r;r++)if((t||r in a)&&a[r]===i)return t||r||0;return!t&&-1}},Ct={includes:Ot(!0),indexOf:Ot(!1)},Tt=Ct.indexOf,Pt=function(t,e){var i,n=P(t),o=0,a=[];for(i in n)!E(ot,i)&&E(n,i)&&a.push(i);for(;e.length>o;)E(n,i=e[o++])&&(~Tt(a,i)||a.push(i));return a},It=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],At=It.concat("length","prototype"),$t={f:Object.getOwnPropertyNames||function(t){return Pt(t,At)}},Et={f:Object.getOwnPropertySymbols},Rt=gt("Reflect","ownKeys")||function(t){var e=$t.f(V(t)),i=Et.f;return i?e.concat(i(t)):e},jt=function(t,e){for(var i=Rt(e),n=L.f,o=D.f,a=0;a0&&(!a.multiline||a.multiline&&"\n"!==t[a.lastIndex-1])&&(l="(?: "+l+")",h=" "+h,c++),i=new RegExp("^(?:"+l+")",r)),le&&(i=new RegExp("^"+l+"$(?!\\s)",r)),se&&(e=a.lastIndex),n=ne.call(s?i:a,h),s?n?(n.input=n.input.slice(c),n[0]=n[0].slice(c),n.index=a.lastIndex,a.lastIndex+=n[0].length):a.lastIndex=0:se&&n&&(a.lastIndex=a.global?n.index+n[0].length:e),le&&n&&n.length>1&&oe.call(n[0],i,(function(){for(o=1;o=74)&&(he=fe.match(/Chrome\/(\d+)/))&&(ue=he[1]);var be=ue&&+ue,me=!!Object.getOwnPropertySymbols&&!v((function(){return!Symbol.sham&&(de?38===be:be>37&&be<41)})),ye=me&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,we=Q("wks"),Se=g.Symbol,xe=ye?Se:Se&&Se.withoutSetter||et,ke=function(t){return E(we,t)&&(me||"string"==typeof we[t])||(me&&E(Se,t)?we[t]=Se[t]:we[t]=xe("Symbol."+t)),we[t]},Oe=ke("species"),Ce=!v((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")})),Te="$0"==="a".replace(/./,"$0"),Pe=ke("replace"),Ie=!!/./[Pe]&&""===/./[Pe]("a","$0"),Ae=!v((function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var i="ab".split(t);return 2!==i.length||"a"!==i[0]||"b"!==i[1]})),$e=function(t,e,i,n){var o=ke(t),a=!v((function(){var e={};return e[o]=function(){return 7},7!=""[t](e)})),s=a&&!v((function(){var e=!1,i=/a/;return"split"===t&&((i={}).constructor={},i.constructor[Oe]=function(){return i},i.flags="",i[o]=/./[o]),i.exec=function(){return e=!0,null},i[o](""),!e}));if(!a||!s||"replace"===t&&(!Ce||!Te||Ie)||"split"===t&&!Ae){var r=/./[o],l=i(o,""[t],(function(t,e,i,n,o){return e.exec===ce?a&&!o?{done:!0,value:r.call(e,i,n)}:{done:!0,value:t.call(i,e,n)}:{done:!1}}),{REPLACE_KEEPS_$0:Te,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:Ie}),c=l[0],h=l[1];dt(String.prototype,t,c),dt(RegExp.prototype,o,2==e?function(t,e){return h.call(t,this,e)}:function(t){return h.call(t,this)})}n&&H(RegExp.prototype[o],"sham",!0)},Ee=ke("match"),Re=function(t){var e;return I(t)&&(void 0!==(e=t[Ee])?!!e:"RegExp"==k(t))},je=function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function");return t},_e=ke("species"),Ne=function(t){return function(e,i){var n,o,a=String(T(e)),s=mt(i),r=a.length;return s<0||s>=r?t?"":void 0:(n=a.charCodeAt(s))<55296||n>56319||s+1===r||(o=a.charCodeAt(s+1))<56320||o>57343?t?a.charAt(s):n:t?a.slice(s,s+2):o-56320+(n-55296<<10)+65536}},Fe={codeAt:Ne(!1),charAt:Ne(!0)}.charAt,De=function(t,e,i){return e+(i?Fe(t,e).length:1)},Ve=function(t,e){var i=t.exec;if("function"==typeof i){var n=i.call(t,e);if("object"!=typeof n)throw TypeError("RegExp exec method returned something other than an Object or null");return n}if("RegExp"!==k(t))throw TypeError("RegExp#exec called on incompatible receiver");return ce.call(t,e)},Be=[].push,Le=Math.min,He=4294967295,Me=!v((function(){return!RegExp(He,"y")}));$e("split",2,(function(t,e,i){var n;return n="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,i){var n=String(T(this)),o=void 0===i?He:i>>>0;if(0===o)return[];if(void 0===t)return[n];if(!Re(t))return e.call(n,t,o);for(var a,s,r,l=[],c=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),h=0,u=new RegExp(t.source,c+"g");(a=ce.call(u,n))&&!((s=u.lastIndex)>h&&(l.push(n.slice(h,a.index)),a.length>1&&a.index=o));)u.lastIndex===a.index&&u.lastIndex++;return h===n.length?!r&&u.test("")||l.push(""):l.push(n.slice(h)),l.length>o?l.slice(0,o):l}:"0".split(void 0,0).length?function(t,i){return void 0===t&&0===i?[]:e.call(this,t,i)}:e,[function(e,i){var o=T(this),a=null==e?void 0:e[t];return void 0!==a?a.call(e,o,i):n.call(String(o),e,i)},function(t,o){var a=i(n,t,this,o,n!==e);if(a.done)return a.value;var s=V(t),r=String(this),l=function(t,e){var i,n=V(t).constructor;return void 0===n||null==(i=V(n)[_e])?e:je(i)}(s,RegExp),c=s.unicode,h=(s.ignoreCase?"i":"")+(s.multiline?"m":"")+(s.unicode?"u":"")+(Me?"y":"g"),u=new l(Me?s:"^(?:"+s.source+")",h),d=void 0===o?He:o>>>0;if(0===d)return[];if(0===r.length)return null===Ve(u,r)?[r]:[];for(var f=0,p=0,g=[];pa;)L.f(t,i=n[a++],e[i]);return t},We=gt("document","documentElement"),Ge=nt("IE_PROTO"),Ke=function(){},Ye=function(t){return" - - + +{% include "third_party_js.html" %} + - - - - - - - - - - - - - - - - +{% include "third_party_js.html" %} - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + {% block js_load %} {% endblock %} - - - - - - - +{% include "third_party_js.html" %} diff --git a/InvenTree/templates/stats.html b/InvenTree/templates/stats.html index 7b966dd12b..b4f38c7ebc 100644 --- a/InvenTree/templates/stats.html +++ b/InvenTree/templates/stats.html @@ -74,7 +74,7 @@ {% trans "Email Settings" %} - + {% trans "Email settings not configured" %} diff --git a/InvenTree/templates/third_party_js.html b/InvenTree/templates/third_party_js.html new file mode 100644 index 0000000000..c32b1c10e8 --- /dev/null +++ b/InvenTree/templates/third_party_js.html @@ -0,0 +1,37 @@ +{% load static %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InvenTree/users/api.py b/InvenTree/users/api.py index 11267e4d8f..db6a629b77 100644 --- a/InvenTree/users/api.py +++ b/InvenTree/users/api.py @@ -5,17 +5,18 @@ from django.core.exceptions import ObjectDoesNotExist from django.urls import include, path, re_path from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics, permissions, status +from rest_framework import filters, permissions, status from rest_framework.authtoken.models import Token from rest_framework.response import Response from rest_framework.views import APIView +from InvenTree.mixins import ListAPI, RetrieveAPI from InvenTree.serializers import UserSerializer from users.models import Owner, RuleSet, check_user_role from users.serializers import OwnerSerializer -class OwnerList(generics.ListAPIView): +class OwnerList(ListAPI): """List API endpoint for Owner model. Cannot create. @@ -54,7 +55,7 @@ class OwnerList(generics.ListAPIView): return results -class OwnerDetail(generics.RetrieveAPIView): +class OwnerDetail(RetrieveAPI): """Detail API endpoint for Owner model. Cannot edit or delete @@ -107,7 +108,7 @@ class RoleDetails(APIView): return Response(data) -class UserDetail(generics.RetrieveAPIView): +class UserDetail(RetrieveAPI): """Detail endpoint for a single user.""" queryset = User.objects.all() @@ -115,7 +116,7 @@ class UserDetail(generics.RetrieveAPIView): permission_classes = (permissions.IsAuthenticated,) -class UserList(generics.ListAPIView): +class UserList(ListAPI): """List endpoint for detail on all users.""" queryset = User.objects.all() diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 24c8a6d062..ca9ee7c240 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -6,6 +6,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType +from django.core.cache import cache from django.db import models from django.db.models import Q, UniqueConstraint from django.db.models.signals import post_delete, post_save @@ -474,13 +475,19 @@ def update_group_roles(group, debug=False): logger.info(f"Adding permission {child_perm} to group {group.name}") -@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets') -def create_missing_rule_sets(sender, instance, **kwargs): - """Called *after* a Group object is saved. +def clear_user_role_cache(user): + """Remove user role permission information from the cache. - As the linked RuleSet instances are saved *before* the Group, then we can now use these RuleSet values to update the group permissions. + - This function is called whenever the user / group is updated + + Args: + user: The User object to be expunged from the cache """ - update_group_roles(instance) + + for role in RuleSet.RULESET_MODELS.keys(): + for perm in ['add', 'change', 'view', 'delete']: + key = f"role_{user}_{role}_{perm}" + cache.delete(key) def check_user_role(user, role, permission): @@ -491,6 +498,17 @@ def check_user_role(user, role, permission): if user.is_superuser: return True + # First, check the cache + key = f"role_{user}_{role}_{permission}" + + result = cache.get(key) + + if result is not None: + return result + + # Default for no match + result = False + for group in user.groups.all(): for rule in group.rule_sets.all(): @@ -498,19 +516,24 @@ def check_user_role(user, role, permission): if rule.name == role: if permission == 'add' and rule.can_add: - return True + result = True + break if permission == 'change' and rule.can_change: - return True + result = True + break if permission == 'view' and rule.can_view: - return True + result = True + break if permission == 'delete' and rule.can_delete: - return True + result = True + break - # No matching permissions found - return False + # Save result to cache + cache.set(key, result, timeout=3600) + return result class Owner(models.Model): @@ -659,3 +682,22 @@ def delete_owner(sender, instance, **kwargs): """Callback function to delete an owner instance after either a new group or user instance is deleted.""" owner = Owner.get_owner(instance) owner.delete() + + +@receiver(post_save, sender=get_user_model(), dispatch_uid='clear_user_cache') +def clear_user_cache(sender, instance, **kwargs): + """Callback function when a user object is saved""" + + clear_user_role_cache(instance) + + +@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets') +def create_missing_rule_sets(sender, instance, **kwargs): + """Called *after* a Group object is saved. + + As the linked RuleSet instances are saved *before* the Group, then we can now use these RuleSet values to update the group permissions. + """ + update_group_roles(instance) + + for user in get_user_model().objects.filter(groups__name=instance.name): + clear_user_role_cache(user) diff --git a/requirements.txt b/requirements.txt index 26bfca213b..765c774284 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ 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-2fa==0.8 # MFA / 2FA +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 django-crispy-forms==1.11.2 # Form helpers