Compare commits

...

727 Commits

Author SHA1 Message Date
Jon
8d071c68df chore: add Merge/Pull request template
#226
2024-08-14 01:52:22 +09:30
Jon
3b1691ff62 docs(roadmap): update completed features
#226
2024-08-14 01:46:21 +09:30
Jon
a77c43d213 docs(base): detail view template
. #24 #226 closes #22
2024-08-14 01:33:48 +09:30
Jon
086959b431 refactor(itim): services now use details template
. #22 #226
2024-08-14 01:23:17 +09:30
Jon
3f117f9d83 feat(base): create detail view templates
purpose is to aid in the development of a detail form

#22 #24 #226
2024-08-14 00:02:25 +09:30
Jon
6a23845a4f docs: initial adding of template page
#22
2024-08-13 15:03:10 +09:30
Jon
b9c6d04e04 chore: add services navigation icon
!43 #69
2024-08-13 13:23:45 +09:30
Jon
32c0027ecf fix(itim): ensure that the service template config is also rendered as part of device config
!43 #69
2024-08-13 13:23:45 +09:30
Jon
dae52e8646 docs: fluff the port and services
!43 closes #69
2024-08-13 13:23:45 +09:30
Jon
890a5651a0 fix(itim): dont render link if no device
!43 #69
2024-08-13 13:23:45 +09:30
Jon
4cb37f8347 feat(itam): Render Service Config with device config
!43 #69
2024-08-13 13:23:45 +09:30
Jon
a2010b9517 feat(itam): Display deployed services for devices
!43 #69
2024-08-13 13:23:45 +09:30
Jon
c95736ce14 feat(itim): Prevent circular service dependencies
!43 #69
2024-08-13 13:23:45 +09:30
Jon
b46c61954c feat(itim): Port number validation to check for valid port numbers
!43 #69
2024-08-13 13:23:45 +09:30
Jon
afe4266600 feat(itim): Prevent Service template from being assigned as dependent service
!43 #69
2024-08-13 13:23:45 +09:30
Jon
0c8d1c8da1 feat(itim): Add service template support
!43 #69
2024-08-13 13:23:45 +09:30
Jon
eac998b5cc fix(itim): Dont show self within service dependencies
!43 #69
2024-08-13 13:23:45 +09:30
Jon
5914782252 feat(itim): Ports for service management
!43 #69
2024-08-13 13:23:45 +09:30
Jon
73d875c4ac feat(itim): Service Management
!43 #69
2024-08-13 13:23:45 +09:30
Jon
8f439f0675 fix(assistance): Only return distinct values when limiting KB articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
0f102c6aaf docs(assistance): document kb categories for user
!43 closes #10
2024-08-13 13:23:45 +09:30
Jon
4852c6caeb feat(assistance): Filter KB articles to target user
only intended to filter for users whom dont have change perm.

!43 #10
2024-08-13 13:23:45 +09:30
Jon
3fffba2eba feat(assistance): Add date picker to date fields for KB articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
a1293984ea feat(assistance): Dont display expired articles for "view" users
!43 #10
2024-08-13 13:23:45 +09:30
Jon
4876db50c1 docs(assistance): document kb for user
!43 #10
2024-08-13 13:23:45 +09:30
Jon
425cc066af feat(base): add code highlighting to markdown
!43 #10
2024-08-13 13:23:45 +09:30
Jon
1086f517fa feat(assistance): Categorised Knowledge base articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
2fdbf87ddd docs(assistance): added pages for knowledgebase
!43 #10
2024-08-13 13:23:45 +09:30
Jon
86228836c7 chore(base): rename information -> assistance
!43 #10
2024-08-13 13:23:45 +09:30
Jon
a6e6c948a5 feat(itim): Add menu entry
!43 #69 #71
2024-08-13 13:23:45 +09:30
Jon
dcdfa8feb7 feat(itam): Ability to add device configuration
!43 fixes #44
2024-08-13 13:23:45 +09:30
Jon
8388d2e695 test(external_link): add tests
!43 fixes #6
2024-08-13 13:23:45 +09:30
Jon
29f269050f feat(settings): New model to allow adding templated links to devices and software
!43 #6
2024-08-13 13:23:45 +09:30
Jon
93c4fc2009 docs: move settings pages into sub-directory
!43 #6
2024-08-13 13:23:45 +09:30
9d5464b5a9 build: bump version 1.0.0-b13 -> 1.0.0-b14 2024-08-12 06:02:07 +00:00
Jon
7848397ae2 Merge pull request #224 from nofusscomputing/223-fix-api-team-mnotes
CRUD operation for team notes
2024-08-12 15:29:21 +09:30
Jon
f298ce94bf test(access): test field model_notes
closes #223
2024-08-12 15:15:28 +09:30
Jon
3cace8943e fix(api): ensure model_notes is an available field
#223
2024-08-12 15:14:58 +09:30
Jon
aa40d68c88 build: add test to changelog
#209
2024-08-11 19:30:45 +09:30
c0f186db89 build: bump version 1.0.0-b12 -> 1.0.0-b13 2024-08-11 08:01:20 +00:00
Jon
6b35e7808c Merge pull request #222 from nofusscomputing/153-model-validations
fix: Audit models for validations
2024-08-11 17:21:20 +09:30
Jon
467f6fca6b fix(itam): Ensure device name is formatted according to RFC1035 2.3.1
see https://datatracker.ietf.org/doc/html/rfc1035#autoid-6

#222 closes #153
2024-08-11 17:09:06 +09:30
Jon
f86b2d5216 fix(itam): Ensure device UUID is correctly formatted
#153 #222
2024-08-11 17:08:56 +09:30
Jon
e29d8e1ec1 fix(config_management): Ensure that config group can't set self as parent
interface already filters self out, however check still to be done.

. #153 #222
2024-08-11 17:07:39 +09:30
Jon
0fc5f41391 fix(settings): ensure that the api token cant be saved to notes field
#153
2024-08-11 16:26:19 +09:30
Jon
4b29448d84 Merge pull request #220 from nofusscomputing/162-api-field-validtion
test: api field checks
2024-08-11 14:37:46 +09:30
Jon
e9fe4896df ci: mirror repo to gitlab
. #220 closes #214
2024-08-11 14:29:02 +09:30
Jon
b9d32a2c16 docs(tests): update testing docs explaining test types
#220 closes #162
2024-08-11 12:57:33 +09:30
Jon
d6bd99c5de docs: update project badges to reflect github hosted project
. #220
2024-08-11 12:38:20 +09:30
Jon
7de5ab12bf docs(readme): correct status badge icon to github
. #220
2024-08-11 12:37:52 +09:30
Jon
3fe09fb8f9 test(software): api field checks
. #162 #220
2024-08-11 12:27:57 +09:30
Jon
eb6b03f731 docs(development): added api field test note
. #220
2024-08-11 12:05:46 +09:30
40e3078a58 build: bump version 1.0.0-b11 -> 1.0.0-b12 2024-08-10 11:23:21 +00:00
Jon
4ba79c6ae9 Merge pull request #218 from nofusscomputing/162-api-field-validtion
test: api field checks

#128 #162
2024-08-10 20:51:51 +09:30
Jon
b5c31d81d3 fix(api): ensure org mixin is inherited by software view
. #218 fixes #219
2024-08-10 20:35:06 +09:30
Jon
c3b585d416 fix(base): correct project links to github
. #218
2024-08-10 20:24:43 +09:30
Jon
84d21f4af8 test(teams): api field checks
. #162 #218
2024-08-10 19:58:04 +09:30
Jon
262e431834 test(organization): api field checks
. #162
2024-08-10 19:39:28 +09:30
cde2562048 build: bump version 1.0.0-b10 -> 1.0.0-b11 2024-08-10 08:30:02 +00:00
Jon
67d853cf25 Merge pull request #215 from nofusscomputing/dependabot/pip/django-5.0.8
chore(deps): bump django from 5.0.7 to 5.0.8

#209
2024-08-10 17:54:55 +09:30
Jon
3ba6bb5b4b docs(readme): correct build badge
#209 #214
2024-08-10 17:35:53 +09:30
84d4f48c63 chore(deps): bump django from 5.0.7 to 5.0.8
Bumps [django](https://github.com/django/django) from 5.0.7 to 5.0.8.
- [Commits](https://github.com/django/django/compare/5.0.7...5.0.8)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-10 17:22:47 +09:30
Jon
5e8bebbeb1 build(python): update installed packages as part of the build
#209
2024-08-10 17:22:07 +09:30
Jon
b66a8644a0 Merge pull request #217 from nofusscomputing/ci-adjustments
ci: migration items
2024-08-10 15:03:45 +09:30
Jon
f0ae185fc5 docs(readme): fix version badges
#217 #214
2024-08-10 14:02:27 +09:30
Jon
43b7e413a6 ci(project): add issue/pr project triage
https://github.com/nofusscomputing/action_project/pull/1 https://github.com/nofusscomputing/centurion_erp/issues/214 #217
2024-08-10 13:45:58 +09:30
Jon
04dc00d79d ci(gitlab): fix includes
https://github.com/nofusscomputing/centurion_erp/issues/214
2024-08-10 13:35:37 +09:30
8e6fd58107 build: bump version 1.0.0-b9 -> 1.0.0-b10 2024-08-09 12:53:39 +00:00
Jon
bfe9a95038 ci: remove docker.io from image publish name
#209
2024-08-09 22:23:03 +09:30
33687791ec build: bump version 1.0.0-b8 -> 1.0.0-b9 2024-08-09 12:43:24 +00:00
Jon
57bc972b0f ci: use correct docker.io credentials
#209
2024-08-09 22:12:19 +09:30
Jon
f437eeccb8 ci: use full docker.io as publish image name
#209
2024-08-09 22:12:19 +09:30
Jon
4e11ad67d0 ci: use docker.io as publish registry
#209
2024-08-09 22:12:19 +09:30
40ba645a35 build: bump version 1.0.0-b7 -> 1.0.0-b8 2024-08-09 12:26:45 +00:00
Jon
27e73e21d1 ci: add org to docker publish registry
#209
2024-08-09 21:55:41 +09:30
a6c0785de0 build: bump version 1.0.0-b6 -> 1.0.0-b7 2024-08-09 12:14:12 +00:00
Jon
83328be22e ci: fix publish registry
#209
2024-08-09 21:39:53 +09:30
c6ed5c8279 build: bump version 1.0.0-b5 -> 1.0.0-b6 2024-08-09 11:58:50 +00:00
Jon
a4dc7f479a Merge pull request #216 from nofusscomputing/gitlab-migration
chore: gitlab migration tasks

#216 #214
2024-08-09 21:16:01 +09:30
Jon
71726035dc ci: Add Bump workflow
#216 #214
2024-08-09 21:05:38 +09:30
Jon
c624a3617c ci: Add Python workflow
#216 #214
2024-08-09 21:05:16 +09:30
Jon
cf00ab6234 ci: Add Docker workflow
#216 #214
2024-08-09 21:05:05 +09:30
Jon
e8684c5206 ci: Add PR checks workflow
#216 #214
2024-08-09 21:04:25 +09:30
Jon
bb388a1969 ci: remove temp workflows
#216 #214
2024-08-09 21:03:58 +09:30
Jon
d99f2d3c6f docs: update readme to reflect Github as project home
. #216
2024-08-09 21:02:48 +09:30
Jon
81a72773cb ci: remove gitlab pipelines
#216
2024-08-09 15:15:54 +09:30
Jon
5fa88a5209 ci(github): add coverage
!43
2024-08-02 10:49:42 +09:30
Jon
366579c12b ci(github): add unit tests
!43
2024-08-02 04:59:54 +09:30
Jon
fed0c5c3e5 chore: artifacthub preperation
!43
2024-08-01 17:31:00 +09:30
c496d10c1a bump: version 1.0.0-b4 → 1.0.0-b5 2024-07-31 17:02:31 +00:00
Jon
3993cc96a5 Merge branch '160-api-config' into 'development'
feat: add Config groups to API

Closes #161 and #160

See merge request nofusscomputing/projects/centurion_erp!45
2024-07-31 16:44:55 +00:00
Jon
a4b37b34a9 docs: Add dets of collection
!45 closes #161
2024-08-01 02:12:42 +09:30
Jon
2f55024f0b fix(api): Ensure device groups is read only
checks for required fields

!45 #160 #162
2024-08-01 01:54:24 +09:30
Jon
213644a51a test(api): Field existence and type checks for device
checks for required fields

!45 #160 #162
2024-08-01 01:12:33 +09:30
Jon
281d839801 feat(api): Add device config groups to devices
!45 #160 nofusscomputing/projects/ansible/collections/centurion_erp_collection!7 nofusscomputing/projects/ansible/collections/centurion_erp_collection#4
2024-08-01 00:32:16 +09:30
Jon
4fd157a785 test(api): test configgroups API fields
tests for type and existence

!45 closes #160
2024-07-31 22:27:45 +09:30
Jon
968b3a0f92 feat(api): Ability to fetch configgroups from api along with config
!45 #160 nofusscomputing/projects/ansible/collections/centurion_erp_collection#4
2024-07-31 22:27:32 +09:30
Jon
f5ba608ed1 ci: var to export for use in script
!45
2024-07-31 21:19:55 +09:30
289668bb7f bump: version 1.0.0-b3 → 1.0.0-b4 2024-07-29 07:54:33 +00:00
Jon
9e28722dba Merge branch 'b3-fixes' into 'development'
fix: release-b3 fixes

Closes #155

See merge request nofusscomputing/projects/centurion_erp!44
2024-07-29 07:38:00 +00:00
Jon
9b673f4a07 fix(api): cleanup team post/get
!44 #159
2024-07-29 17:03:25 +09:30
Jon
3a9e4b29b3 fix(api): confirm HTTP method is allowed before permission check
return HTTP/405 for logged in user ONLY!!

!44 #159
2024-07-29 17:02:52 +09:30
Jon
8d59462561 fix(api): Ensure that organizations can't be created via the API
!44 fixes #155
2024-07-29 17:02:37 +09:30
Jon
098e41e6a1 feat(swagger): remove {format} suffixed doc entries
!44
2024-07-29 16:49:51 +09:30
Jon
fc3f0b39e2 ci: add debug out to extra command
!44
2024-07-29 16:49:24 +09:30
Jon
de53948cea test: confirm that the tenancymanager is called
!43
2024-07-21 13:27:45 +09:30
Jon
823ebc0eb5 fix(access): Team model class inheritance order corrected
!42
2024-07-21 13:27:36 +09:30
41414438d1 bump: version 1.0.0-b2 → 1.0.0-b3 2024-07-21 01:47:05 +00:00
Jon
5704560beb fix(itam): Limit os version count to devices user has access to
!42
2024-07-21 10:07:36 +09:30
Jon
8a48902b64 ci: return command to release
!42
2024-07-20 13:02:21 +09:30
61fe059513 bump: version 1.0.0-b1 → 1.0.0-b2 2024-07-19 10:41:41 +00:00
Jon
94576cc733 ci: fix additional command as part of release
!42
2024-07-19 19:45:50 +09:30
Jon
3a32c62119 fix(itam): only show os version once
!42 fixes #139
2024-07-19 18:01:10 +09:30
Jon
9ea4fe1adc ci: Create Version labels within repo on release
!42
2024-07-19 18:01:10 +09:30
Jon
0798a672c2 docs(administration): spread the love out
!42
2024-07-19 18:01:10 +09:30
f4e68529ba bump: version 1.0.0-a4 → 1.0.0-b1 2024-07-19 07:15:09 +00:00
Jon
92a411baec docs(administration): explain the magic
!42 #74
2024-07-19 16:27:33 +09:30
Jon
034857d088 ci: dev branch releases now beta
preperation for RC. all dev releases are now beta.

!42 #74
2024-07-19 15:35:32 +09:30
Jon
e5ce86a9bb fix(itam): ensure installed operating system count is limited to users organizations
!42
2024-07-19 11:02:56 +09:30
Jon
5188b3d52e fix(itam): ensure installed software count is limited to users organizations
!42
2024-07-19 00:39:05 +09:30
61b9435d1f bump: version 1.0.0-a3 → 1.0.0-a4 2024-07-18 12:59:03 +00:00
Jon
8244676530 test: ensure inventory upload matches by both serial number and uuid if device name different
!42 #15
2024-07-18 22:05:12 +09:30
Jon
ec1e7cca85 test: placeholder for moving organization
!42 #15
2024-07-18 22:04:27 +09:30
Jon
72ab9253d7 feat(api): When processing uploaded inventory and name does not match, update name to one within inventory file
!43
2024-07-18 17:08:52 +09:30
Jon
4f89255c4f feat(config_management): Group name to be entire breadcrumb
!43
2024-07-18 16:51:22 +09:30
8d6d1d0d56 bump: version 1.0.0-a2 → 1.0.0-a3 2024-07-18 06:25:34 +00:00
Jon
2d0c3a660a fix(config_management): dont attempt to do action during save if group being created
!42
2024-07-18 15:34:25 +09:30
Jon
974a208869 chore(config_management): remove org filter as its not required
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
7f225784c2 chore(settings): remove org filter as its not required
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
a3be95013c fix(itam): remove org filter for device so that user can see installations
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
adefbf3960 fix(itam): remove org filter for operating systems so that user can see installations
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
9a1ca7a104 fix(itam): remove org filter for software so that user can see installations
not required as org filtering is done as part of the initial queryset within the model.

!42
2024-07-18 15:34:25 +09:30
Jon
e84e80cd8f feat(config_management): Prevent a config group from being able to change organization
!42
2024-07-18 15:34:25 +09:30
Jon
ebc266010a feat(itam): On device organization change remove config groups
!42
2024-07-18 15:34:25 +09:30
Jon
519277e18b fix(itam): Device related items should not be global.
!42
2024-07-18 15:34:25 +09:30
Jon
a5a5874211 fix(itam): When changing device organization move related items too.
!42 fixes #137
2024-07-18 15:34:25 +09:30
fa2b90ee7b bump: version 1.0.0-a1 → 1.0.0-a2 2024-07-17 16:53:14 +00:00
Jon
5c74360842 fix(base): dont show user warning bar for non-authenticated user
!42
2024-07-18 01:48:57 +09:30
Jon
8457f15eca fix(api): correct inventory operating system selection by name
!42 #134
2024-07-18 01:48:21 +09:30
Jon
5bc5a4b065 docs(worker): add worker and task logs
!42 fixes #135
2024-07-18 01:28:22 +09:30
Jon
40350d166e feat(api): Inventory matching of device second by uuid
!42 #134
2024-07-18 01:17:51 +09:30
Jon
9a94ba31e4 feat(api): Inventory matching of device first by serial number
!42 #134
2024-07-18 01:17:29 +09:30
Jon
55197e7dcc fix(api): correct inventory operating system and it's linking to device
wasn't updating existing device os

!42 #134
2024-07-18 01:16:34 +09:30
Jon
a67bc70503 fix(api): correct inventory device search to be case insensitive
!42 fixes #134
2024-07-18 01:14:33 +09:30
Jon
60538e1cec feat(base): show warning bar if the user has not set a default organization
!42 fixes #133
2024-07-17 23:51:25 +09:30
Jon
416e029c23 revert: return ci build settings to not include branch alpha
partial revert of a1759ecaaf

!42
2024-07-17 11:00:41 +09:30
fe64c11927 bump: version 0.7.0 → 1.0.0-a1 2024-07-16 05:56:44 +00:00
Jon
1f8244ae40 ci: use updated commitizen
!42 !40 #74
2024-07-16 15:11:39 +09:30
Jon
9871cf248b Merge branch 'v-1-0-0-alpha' into 'development'
refactor: repo preperation for v1.0.0-Alpha-1

See merge request nofusscomputing/projects/centurion_erp!40
2024-07-16 04:24:44 +00:00
Jon
a1759ecaaf ci: add alpha branch to docker builds and publish
!40 !42 #74
2024-07-16 13:39:52 +09:30
Jon
30e0342f52 ci: temp change to release, on dev to be alpha release
!40 !35 #74
2024-07-16 13:31:18 +09:30
Jon
5a201ef548 refactor!: Squash database migrations
BREAKING CHANGE: squashed DB migrations in preparation for v1.0 release.

!40 !35 #74
2024-07-16 13:31:18 +09:30
Jon
7b26fac73d feat: Administratively set global items org/is_global field now read-only
!42 fixes #126
2024-07-16 13:19:30 +09:30
Jon
7c62309a31 fix(core): migrate manufacturer to use new form/view logic
!42 fixes #127
2024-07-16 11:55:25 +09:30
Jon
621cbd2d71 revert: return organization filtering back to forms
!42 #124
2024-07-16 00:02:45 +09:30
Jon
d8e89bee10 test: tenancy objects
!42 #15 closes #124
2024-07-15 23:22:15 +09:30
Jon
4ee62aa399 test: refactor to single abstract model for inclusion.
!42 #15
2024-07-15 23:01:49 +09:30
Jon
f1201e8933 feat(access): Add multi-tennant manager
manager filters results to that of data from the organizations the users is part of.

!42 #124
2024-07-15 16:17:08 +09:30
Jon
9acc4fdfcb docs(gitlab): update MR template
!42 #74
2024-07-14 17:16:16 +09:30
Jon
6776612b66 chore: move docker-compose to deploy directory
intent of dir is that is the location for all avail deploy methods

!42 #74
2024-07-14 17:11:49 +09:30
Jon
af3e770760 fix(settings): correct the permission to view manufacturers
!42 #74
2024-07-14 17:09:18 +09:30
Jon
fbe7e63cc9 fix(access): Correct team form fields
fixes missing name for team

!42 #74
2024-07-14 16:57:25 +09:30
Jon
aec460306b fix(config_management): don't exclude parent from field, only self
!42 #74
2024-07-14 16:48:54 +09:30
46c4419350 bump: version 0.6.0 → 0.7.0 2024-07-14 06:25:37 +00:00
Jon
1f35893db9 chore: footer text to be grey
does not need to be prominent

!35
2024-07-14 15:27:03 +09:30
Jon
935e10dc24 docs(development): add initial forms
!35
2024-07-14 04:56:11 +09:30
Jon
a4617c28f8 chore: complete footer layout
git icon from https://gitlab-org.gitlab.io/gitlab-svgs

!35 closes #25
2024-07-14 04:22:41 +09:30
Jon
d4aaea4dbb docs(development): update views, models and index
!35
2024-07-14 03:10:42 +09:30
Jon
a7168834ba chore: add new exception MissingAttribute
used for development

!35
2024-07-14 02:52:28 +09:30
Jon
329049e81d docs: roadmap update
!35
2024-07-14 01:51:53 +09:30
Jon
e25ec12cb0 ci: correct test report path
!35
2024-07-14 01:15:54 +09:30
Jon
5ae487cd3e fix(config_management): Don't allow a config group to assign itself as its parent
!35 fixes #122
2024-07-14 01:09:19 +09:30
Jon
4c42f77692 feat(core): Filter every form field if associated with an organization to users organizations only
!35 fixes #119
2024-07-14 01:08:00 +09:30
Jon
3aab7b57e8 fix(config_management): correct permission for deleting a host from config group
!35
2024-07-13 23:12:31 +09:30
Jon
931c9864db fix(config_management): use parent group details to work out permissions when adding a host
!35 fixes #120
2024-07-13 23:11:59 +09:30
Jon
65bf994619 fix(config_management): use parent group details to work out permissions
!35 fixes #121
2024-07-13 22:44:54 +09:30
Jon
367c4bebb6 refactor: adjust views missing add/change form to now use forms
!35 #15 #46 #74 #120 #121 fixes #118
2024-07-13 17:32:45 +09:30
Jon
8c1be67974 test: add test test_view_*_attribute_not_exists_fields for add and change views
!25 #15 #46
2024-07-13 17:25:20 +09:30
Jon
789c035a03 test: fix test_view_change_attribute_type_form_class to test if type class
!25 #15 #46
2024-07-13 17:23:56 +09:30
Jon
1cf15f7339 feat(core): add var template_name to common view template for all views that require it
!35
2024-07-13 16:01:12 +09:30
Jon
77ff580f19 fix(itam): Add missing permissions to software categories index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
423ff11d4c fix(itam): Add missing permissions to device types index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
9e4b5185b1 fix(itam): Add missing permissions to device model index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
020441c41a fix(settings): Add missing permissions to app settings view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
d0a3b7b49d fix(itam): Add missing permissions to software index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
960fa5485d fix(itam): Add missing permissions to operating system index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
26db463044 fix(itam): Add missing permissions to device index view
!35 #74
2024-07-13 16:01:12 +09:30
Jon
1193f1d86d fix(config_management): Add missing permissions to group views
!35 #74
2024-07-13 16:01:12 +09:30
Jon
9bece0a811 test(views): add test cases for model views
!35  #46 #15 #118 #120 #121
2024-07-13 16:01:12 +09:30
Jon
f29ec63f46 test: Add Test case abstract classes to models
!35 #46 #15
2024-07-13 16:01:12 +09:30
Jon
e48278e6e9 Merge branch 'docs' into 'development'
docs(centurion_docs): update docs

See merge request nofusscomputing/projects/centurion_erp!41
2024-07-13 04:19:40 +00:00
c41c7ed1f0 docs: update mkdocs
Change Repo name
Update URL and URI

!41
2024-07-13 12:04:59 +09:30
c9190e9a7d docs: Update index
Slight re-word

nofusscomputing/projects/centurion_erp!41
2024-07-13 11:58:15 +09:30
0294f5ed65 docs(centurion): replace Django ITSM -> Centurion ERP
nofusscomputing/projects/centurion_erp!41
2024-07-13 11:57:34 +09:30
Jon
ae4fdcfc58 chore: clean-up readme
!35
2024-07-12 13:50:34 +09:30
Jon
a395f30bd4 fix(security) update djangorestframework 3.15.1 -> 3.15.2
[CVE-2024-21520](https://cwe.mitre.org/data/definitions/79.html)

!35
2024-07-12 13:48:06 +09:30
Jon
c057ffdc9c feat(core): add Display view to common forms abstract class
intended to display generic data

!35
2024-07-12 12:47:17 +09:30
Jon
6837c38303 feat(navigation): always show every menu for super admin
!35
2024-07-12 12:44:41 +09:30
Jon
ee8920a464 fix(navigation): always show settings menu entry
!35
2024-07-12 09:27:29 +09:30
Jon
ccfdf005f7 chore: remove issue#13 links for model notes
!35 closes #13
2024-07-12 09:23:12 +09:30
Jon
0276f9454b refactor: add navigation menu expand arrows
!35 closes #21
2024-07-12 09:19:13 +09:30
Jon
45cc34284a feat(core): only display navigation menu item if use can view model
!35 fixes #114
2024-07-12 07:16:05 +09:30
Jon
7329a65ae7 docs: update roadmap
!35
2024-07-12 05:56:22 +09:30
Jon
9a529a64e2 docs: add bug count badge
!35
2024-07-12 05:53:21 +09:30
Jon
f2640df0d3 feat(django): update 5.0.6 -> 5.0.7
!35
2024-07-12 05:51:21 +09:30
Jon
7d172fb4af refactor: migrate views to use new abstract model view classes
!35 fixes #111
2024-07-12 05:50:36 +09:30
Jon
f848d01b34 refactor: migrate forms to use new abstract model form class
!35
2024-07-12 05:47:49 +09:30
Jon
44f20b28be feat(core): add common forms abstract class
form class for inclusion in our forms

!35
2024-07-12 05:45:39 +09:30
Jon
2e22a484a0 feat(core): add common views abstract class
class for our views

!35
2024-07-12 05:03:11 +09:30
Jon
a62a36ba82 fix(itam): cater for fields that are prefixed
!35 fixes #112
2024-07-11 18:27:07 +09:30
Jon
c00cf16bc8 fix(itam): Ability to view software category
Ensure organization filters to list of orgs

!35
2024-07-11 18:12:07 +09:30
Jon
7784dfede9 fix(itam): correct view permission
!35
2024-07-11 18:01:10 +09:30
Jon
03d350e302 fix(access): When adding a new team to org ensure parent model is fetched
!35
2024-07-11 17:56:12 +09:30
Jon
9b79c9d7ff docs: update readme
!35
2024-07-11 16:57:21 +09:30
Jon
1d5c86f13b fix(access): enable org manager to view orgs
corrects http/500

!35 fixes #105
2024-07-11 16:34:19 +09:30
Jon
9e336d368d fix(settings): restrict user visible organizations to ones they are part of
this includes if they are the org manager

!35 fixes #99
2024-07-11 15:53:50 +09:30
Jon
937e935949 fix(access): enable org manager to view orgs
!35 fixes #105
2024-07-11 15:37:16 +09:30
Jon
860eaa6749 fix(access): fetch object if method exists
enables the setting og self.model with get_object method

!35 fixes #105
2024-07-11 14:25:53 +09:30
Jon
aab94431a9 fix(docs): update docs link to new path
!35 fixes #103
2024-07-11 14:08:18 +09:30
Jon
7cfede45b8 refactor(access): Rename Team Button "new user" -> "Assign User"
!35 fixes #110
2024-07-11 14:00:55 +09:30
Jon
65de93715d refactor(access): model pk and name not required context for adding a device
!35
2024-07-11 13:59:58 +09:30
Jon
fea7ea3119 refactor: rename field "model notes" -> "Notes"
!35 fixes #102 #104
2024-07-11 13:58:14 +09:30
Jon
524a70ba18 fix(access): correctly set team user parent model to team
!35 fixes #109
2024-07-11 13:43:27 +09:30
Jon
29c4b4a0ca fix(access): fallback to django permissions if org permissions check is false
!35 #109 fixes #101
2024-07-11 13:41:36 +09:30
Jon
f5ae01b08d fix(access): Correct logic so that org managers can see orgs they manage
!35 fixes #100
2024-07-10 17:23:24 +09:30
Jon
ee3dd68cfe fix(base): add missing content_title to context
!35 #74
2024-07-10 15:58:18 +09:30
Jon
25efa31493 fix(access): Enable Organization Manager to view organisations they are assigned to
!35 fixes #100
2024-07-10 15:14:53 +09:30
Jon
4a6ce35332 fix(api): correct logic for adding inventory UUID and serial number to device
!35
2024-07-10 14:45:46 +09:30
Jon
332810ffd6 feat: add postgreSQL database support
!35
2024-07-10 14:05:34 +09:30
Jon
f0bbd22cf4 refactor: remove settings model
wasnt required, so removed

!35
2024-07-10 12:02:06 +09:30
Jon
6bf681530d chore: fix docs indent
!35
2024-07-10 03:04:04 +09:30
Jon
9dd2f6a341 docs: fix mkdocs navigation
!35
2024-07-10 02:13:32 +09:30
Jon
23c640a460 docs: add roadmap
!35
2024-07-10 02:13:12 +09:30
Jon
3c6092f776 chore: rename to Centurion ERP
!35
2024-07-10 01:58:19 +09:30
Jon
cb66b9303a feat(ui): add config groups navigation icon
!35
2024-07-09 22:50:38 +09:30
Jon
2d80f02634 fix(ui): navigation alignment and software icon
!35
2024-07-09 22:34:35 +09:30
Jon
abe1ce6948 fix(ui): display organization manager name instead of ID
!35
2024-07-09 22:31:55 +09:30
Jon
fb907283b0 refactor(ui): increase indentation to sub-menu items
!35
2024-07-09 17:32:29 +09:30
Jon
86ed7318ec fix(access): ensure name param exists before attempting to access
!35
2024-07-09 17:29:48 +09:30
Jon
a2a8e12046 feat(ui): add some navigation icons
!35 #21 #22 #23
2024-07-09 17:21:46 +09:30
Jon
c1a8ee65f2 refactor(itam): rename old inventory status icon for use with security
!35
2024-07-09 16:24:52 +09:30
Jon
6a14f78bf7 feat(itam): update inventory status icon
!35
2024-07-09 16:24:10 +09:30
Jon
90a01911da fix(itam): dont show none/nil for device fields containing no value
!35
2024-07-09 15:47:27 +09:30
Jon
de3ed3a881 fix(itam): show device model name instead of ID
!35
2024-07-09 15:42:46 +09:30
Jon
656807e410 feat(itam): ensure device software pagination links keep interface on software tab
!35 closes #81
2024-07-09 14:33:22 +09:30
Jon
f64be2ea33 fix(api): Ensure if serial number from inventory is null that it's not used
!35 fixes #78
2024-07-09 13:21:19 +09:30
Jon
ef9c596ec7 fix(api): ensure checked uuid and serial number is used for updating
!35
2024-07-09 05:28:48 +09:30
Jon
f22e886d92 Merge branch '76-background-worker' into 'development'
feat: "Migrate inventory processing to background worker"

See merge request nofusscomputing/projects/django_template!39
2024-07-08 18:34:33 +00:00
Jon
a2c67541ec test(inventory): add mocks?? for calling background worker
!39 #76
2024-07-09 03:51:21 +09:30
Jon
5f4231ab04 test(view): view permission checks
!39 #76
2024-07-09 02:29:35 +09:30
Jon
b0405c8fd0 test(inventory): update tests for background worker changes
!39 #76
2024-07-09 02:27:24 +09:30
Jon
b42bb3a30e feat(access): enable non-organization django permission checks
!39 #76
2024-07-09 02:26:21 +09:30
Jon
27eb54cc37 docs(api): update swagger docs with inventory changes
!39 #76
2024-07-08 23:10:34 +09:30
Jon
a8e2c687b1 docs(administration): notate rabbitMQ setup
!39 #76
2024-07-08 23:06:51 +09:30
Jon
7aeba34787 refactor(api): migrate inventory processing to background worker
!39 #76
2024-07-08 22:54:34 +09:30
Jon
090c4a5425 feat(settings): Add celery task results index and view page
!39 #76
2024-07-08 22:52:34 +09:30
Jon
87a1f2aa20 feat(base): Add background worker
!39 #76
2024-07-08 22:38:06 +09:30
Jon
70135eaa91 Merge branch 'fixes-inventory' into 'development'
fix: inventory

See merge request nofusscomputing/projects/django_template!38
2024-07-06 06:17:54 +00:00
Jon
f47b97e2a0 refactor(itam): only perform actions on device inventory if DB matches inventory item
!38 #75
2024-07-06 15:45:48 +09:30
Jon
67f20ecb66 fix(itam): only remove device software when not found during inventory upload
!38 #75
2024-07-06 15:45:48 +09:30
Jon
3bceb66600 fix(itam): only update software version if different
!38 #75
2024-07-06 15:45:48 +09:30
Jon
fe34b8274d chore: update submodules to head
!35
2024-07-01 04:03:05 +09:30
Jon
a235aa7ec3 ci: add submodule update job
!35
2024-07-01 04:02:51 +09:30
Jon
f69f883439 Merge branch '66-fix-inventory-uuid-update' into 'development'
fix: existing device without uuid not updated when uploading an inventory

Closes #66

See merge request nofusscomputing/projects/django_template!37
2024-06-30 15:06:02 +00:00
Jon
7b4ed7b135 feat(itam): Update Serial Number from inventory if present and Serial Number not set
!37
2024-07-01 00:27:46 +09:30
Jon
b801c9a49e feat(itam): Update UUID from inventory if present and UUID not set
!37 fixes #66
2024-07-01 00:27:28 +09:30
Jon
583e1767a1 Merge branch '67-fix-device-software-pagination' into 'development'
fix: Device Software tab pagination does not work

Closes #67

See merge request nofusscomputing/projects/django_template!36
2024-06-30 14:33:19 +00:00
Jon
241ba47c80 fix(itam): correct device software pagination
!36 fixes #67
2024-07-01 00:01:28 +09:30
c2d673ca1b bump: version 0.5.0 → 0.6.0 2024-06-30 07:13:40 +00:00
Jon
05c46df0a9 Merge branch '63-feat-user-api-token' into 'development'
feat: user api token

Closes #63 and #65

See merge request nofusscomputing/projects/django_template!34
2024-06-30 06:51:16 +00:00
Jon
53284d456f test(token_auth): test authentication method token
!34 closes #63
2024-06-30 16:16:29 +09:30
Jon
6cfcf1580c fix(user_token): conduct user check on token view access
!34 #63
2024-06-30 16:05:31 +09:30
Jon
4d3a238583 docs: Add user settings documentation
!34 #63
2024-06-30 12:35:58 +09:30
Jon
47d6a3beff docs(api): API Token authentication
!34 #63
2024-06-29 16:24:18 +09:30
Jon
111791438a feat(api): API token authentication
!34 #63
2024-06-29 15:09:30 +09:30
Jon
ce2c6f3b13 feat(api): abilty for user to create/delete api token
!34 #63
2024-06-28 15:52:55 +09:30
Jon
e655f22fac feat(api): create token model
!34 #63
2024-06-28 03:57:09 +09:30
Jon
66b8d9362d refactor(settings): use seperate change/view views
!34
2024-06-28 02:06:30 +09:30
Jon
37d277e149 refactor(settings): use form for user settings
!34
2024-06-28 01:18:48 +09:30
Jon
f686691232 fix(itam): use same form for edit and add
!34 fixes #65
2024-06-27 17:53:38 +09:30
Jon
802f2c410d fix(itam): dont add field inventorydate if adding new item
!34
2024-06-27 17:52:18 +09:30
Jon
be559d3d9d Merge branch 'testing' into 'development'
test: more tests

See merge request nofusscomputing/projects/django_template!33
2024-06-19 19:47:33 +00:00
Jon
d6cfef3a0b test: add .coveragerc to remove non-code files from coverage report
!33 #15
2024-06-20 05:12:04 +09:30
Jon
4fdb3df06e test: Unit Tests TenancyObjects
!33 #15
2024-06-20 03:12:43 +09:30
Jon
7eb0651b89 test: Test Cases for TenancyObjects
!33 #15
2024-06-20 03:07:39 +09:30
Jon
6d3984f6e1 test: tests for checking links from rendered templetes
!33 #15
2024-06-20 02:09:15 +09:30
Jon
58b134ae30 refactor(tests): move unit tests to unit test sub-directory
!33 #15
2024-06-19 22:58:50 +09:30
Jon
50384044c8 test(core): test cases for notes permissions
!33 #15
2024-06-19 17:08:42 +09:30
Jon
2cda4228ce test(config_management): config groups history permissions
!33 #15
2024-06-19 16:09:23 +09:30
Jon
67585b9f89 test(api): Majority of Inventory upload tests
!33 #15
2024-06-19 15:48:10 +09:30
Jon
4e42856027 fix(api): inventory upload requires sanitization
!33
2024-06-19 15:00:30 +09:30
Jon
58051f297f test(access): TenancyObject field tests
!33 #15
2024-06-19 11:51:03 +09:30
Jon
0a9a5b20fa test(access): remove skipped api tests for team users
teamusers not accessible from api

!33 #15
2024-06-19 10:52:24 +09:30
Jon
a0874356fd ci(git_sync): sync on push ro feature branch 14-feat-project-management
!29 !31
2024-06-18 08:55:56 +09:30
Jon
5d8f5e3a51 ci: remove dockerhub publish on bot push
!29
2024-06-18 01:21:30 +09:30
19ae56d92c bump: version 0.4.0 → 0.5.0 2024-06-17 15:14:24 +00:00
Jon
488a12df45 Merge branch '39-org-manager' into 'development'
feat: Setup Organization Managers

Closes #39

See merge request nofusscomputing/projects/django_template!28
2024-06-17 14:50:00 +00:00
Jon
a94856879e docs: correct testing link
!28
2024-06-18 00:18:00 +09:30
Jon
94375dc30e chore: add default MR template
!28
2024-06-18 00:14:23 +09:30
Jon
108398da4b docs: rejig
!28
2024-06-18 00:10:51 +09:30
Jon
8abbf2ff9e docs(access): correct doc warnings
!28
2024-06-17 23:42:54 +09:30
Jon
27b62d1018 docs(access): add link to docs on team page
!28 closes #39
2024-06-17 23:27:11 +09:30
Jon
aef276b76c docs(access): add link to docs on organization page
!28 #39
2024-06-17 23:26:52 +09:30
Jon
c15eca2e58 test(access): team user model permission check for organization manager
!28 #39 #15
2024-06-17 22:59:26 +09:30
Jon
33b10f7109 test(access): team model permission check for organization manager
!28 #39 #15
2024-06-17 22:53:23 +09:30
Jon
9a40d095e8 test(access): organization model permission check for organization manager
!28 #39 #15
2024-06-17 22:52:57 +09:30
Jon
991ddc3d7f refactor(access): relocate permission check to own function
!28 #39
2024-06-17 22:52:02 +09:30
Jon
35c11ed6f0 test(access): add test cases for model delete as organization manager
!28 #39 #15
2024-06-17 22:41:40 +09:30
Jon
e4f5ec4892 test(access): add test cases for model addd as organization manager
!28 #39 #15
2024-06-17 22:40:35 +09:30
Jon
109fc49d76 test(access): add test cases for model change as organization manager
!28 #39 #15
2024-06-17 22:17:59 +09:30
Jon
da8946fcb6 test(access): add test cases for model view as organization manager
!28 #39 #15
2024-06-17 22:13:41 +09:30
Jon
848661856a feat(access): add notes field to organization
!28 #39 #13
2024-06-17 19:40:46 +09:30
Jon
14acea31f2 feat(access): add organization manger
org managager regardless of permissions can conduct all operations against an organization.

!28 #39
2024-06-17 19:38:11 +09:30
Jon
2bbf78d888 Merge branch '15-tests' into 'development'
test: write some more

Closes #57 and #52

See merge request nofusscomputing/projects/django_template!27
2024-06-17 08:44:15 +00:00
Jon
afb5a709d7 docs: add badges to index
!27
2024-06-17 17:58:21 +09:30
Jon
ddead8eb56 docs: restructure to sections administration, user and devlopment
!27 nofusscomputing/infrastructure/website!62
2024-06-17 17:39:01 +09:30
Jon
e517c5fd76 refactor(itam): move device os tab to details tab
!27 #22
2024-06-17 15:23:38 +09:30
Jon
a6e569eaef test(core): skip invalid tests
tests for notes belong within the models that rx notes

!27 #15 #52
2024-06-17 14:46:48 +09:30
Jon
78216116df fix(itam): remove requirement that user needs change device to add notes
!27 fixes #52
2024-06-17 14:35:36 +09:30
Jon
b20b426432 test(itam): tests for device type history entries
!27 #15
2024-06-17 14:10:12 +09:30
Jon
44afa4f7de test(core): tests for manufacturer history entries
!27 #15
2024-06-17 14:06:25 +09:30
Jon
2be4810ed4 test: move manufacturer to it's parent
!27
2024-06-17 13:59:21 +09:30
Jon
f861295b1c docs(development): add device model to api docs
!27
2024-06-16 06:26:37 +09:30
Jon
320d3f1a13 test: refactor api model permission tests to use an abstract class of test cases
!27 #15
2024-06-16 03:43:17 +09:30
Jon
3613318217 test: move tests to the module they belong to
!27 #15
2024-06-16 01:44:33 +09:30
Jon
ceb1929d8c test: refactor history permission tests to use an abstract class of test cases
!27 #15
2024-06-16 01:43:07 +09:30
Jon
dbcb282548 docs: docstrings show category headings
!27
2024-06-16 01:10:17 +09:30
Jon
5eec41fe57 docs(development): Add test case documentation
!27 #15 nofusscomputing/infrastructure/website-template!16 nofusscomputing/projects/django_template#57 nofusscomputing/projects/gitlab-ci!83
2024-06-16 00:15:01 +09:30
Jon
e72eedf077 test: refactor model permission tests to use an abstract class of test cases
!27 #15
2024-06-15 21:40:33 +09:30
Jon
6286b06270 test: refactor history entry to have test cases in abstract classes
!27 #15
2024-06-15 18:50:10 +09:30
Jon
d2bf0e54d7 test(itam): history entry tests for software category
!27 #15
2024-06-15 17:40:31 +09:30
Jon
7f1a7eaa0d test(itam): history entry tests for device operating system version
!27 #15
2024-06-15 17:31:24 +09:30
Jon
29b104a6ce test(itam): history entry tests for device operating system
!27 #15
2024-06-15 17:17:07 +09:30
Jon
1220ddbd00 test(itam): history entry tests for device software
!27 #15
2024-06-15 17:03:51 +09:30
Jon
da746b8977 test(itam): ensure child history is removed on config group software delete
!27 #15
2024-06-15 16:37:18 +09:30
Jon
34db5f863a test: add placeholder tests
!27 #15
2024-06-15 16:31:23 +09:30
Jon
92fe05d083 test(itam): ensure history is removed on software delete
!27 #15
2024-06-15 16:25:38 +09:30
Jon
5280db8767 test(itam): ensure history is removed on operating system delete
!27 #15
2024-06-15 16:24:51 +09:30
Jon
9eda12c232 test(itam): ensure history is removed on device model delete
!27 #15
2024-06-15 16:23:51 +09:30
Jon
54c34a95f5 fix(core): dont attempt to access parent_object if 'None' during history save
!27
2024-06-15 16:19:55 +09:30
Jon
b7a2bfc612 test(config_management): test history on delete for config groups
!27 #15
2024-06-15 16:18:59 +09:30
Jon
3b3ee9fc3d fix(config_management): Add missing parent item getter to model
!27
2024-06-15 16:17:19 +09:30
Jon
d64108331f test(itam): ensure history is removed on device delete
!27 #15
2024-06-15 15:55:28 +09:30
Jon
372eefa5c4 test(access): test team history
!27 #15
2024-06-15 15:54:32 +09:30
Jon
733a31ad71 test(access): ensure team user history is created and removed as required
!27 #15
2024-06-15 15:06:51 +09:30
Jon
4a19bb2ecc test(access): ensure history is removed on team delete
!27 #15
2024-06-15 14:13:37 +09:30
Jon
29a8969288 test(access): ensure history is removed on item delete
!27 #15
2024-06-15 14:12:19 +09:30
Jon
0a1aba7ca8 fix(core): overridden save within SaveHistory to use default attributes
!27
2024-06-15 14:09:17 +09:30
Jon
eb8dca9806 fix(access): overridden save to use default attributes
!27
2024-06-15 14:08:53 +09:30
Jon
8af5975428 feat(config_management): Use breadcrumbs for child group name display
formatted as '<parent name> > <child name>'

!21
2024-06-14 09:13:14 +09:30
Jon
4a10409551 refactor(itam): add device change form and adjust view to be non-form
!21
2024-06-14 09:02:17 +09:30
Jon
ac70715752 feat(config_management): ability to add host to global group
!21
2024-06-14 07:03:59 +09:30
Jon
8ccdf9a8f3 feat(itam): add a status of "bad" for devices
indicates inventory is older than 72-hours

!21
2024-06-14 06:31:27 +09:30
Jon
1200a87913 feat(itam): paginate device software tab
!21
2024-06-14 06:20:13 +09:30
Jon
dfba01aed9 chore: add itim module skeleton
!21 #61
2024-06-14 06:00:13 +09:30
Jon
e8cb685da1 feat(itam): status of device visible on device index page
!21
2024-06-14 04:43:12 +09:30
Jon
7798deaf27 Merge branch '58-browser' into 'development'
feat: API Browser

See merge request nofusscomputing/projects/django_template!26
2024-06-13 17:26:57 +00:00
Jon
8b47d95614 feat(core): add skeleton http browser
get and post intrim functions

!26 #58
2024-06-14 02:20:33 +09:30
Jon
d4c07d08f1 Merge branch '54-fix-delete-history' into 'development'
fix: History does not delete when item deleted

Closes #54

See merge request nofusscomputing/projects/django_template!25
2024-06-13 15:29:11 +00:00
Jon
7239f572a3 fix(core): on object delete remove history entries
!25 fixes #54
2024-06-13 23:38:30 +09:30
Jon
904234c581 refactor(itam): migrate device vie to use manual entered fields in two columns
!21 #13 #22
2024-06-12 12:49:25 +09:30
Jon
fe1a9d07f7 chore: planning for project management
!21 #14
2024-06-12 12:08:03 +09:30
Jon
c570fb114f feat(core): Add a notes field to manufacturer/ publisher
!21 #13
2024-06-12 05:59:33 +09:30
Jon
ea1727f2c7 feat(itam): Add a notes field to software category
!21 #13
2024-06-12 05:59:12 +09:30
Jon
36d7e54547 feat(itam): Add a notes field to device types
!21 #13
2024-06-12 05:58:58 +09:30
Jon
a02fda8413 feat(itam): Add a notes field to device models
!21 #13
2024-06-12 05:58:50 +09:30
Jon
b5bc76b0ab feat(itam): Add a notes field to software
!21 #13
2024-06-12 05:58:32 +09:30
Jon
36c13e18c7 feat(itam): Add a notes field to operating system
!21 #13
2024-06-12 05:58:22 +09:30
Jon
6969b61164 feat(itam): Add a notes field to devices
!21 #13
2024-06-12 05:58:11 +09:30
Jon
85bf1b9907 feat(access): Add a notes field to teams
!21 #13
2024-06-12 05:57:55 +09:30
Jon
ca8e0c07ea feat(base): Add a notes field to TenancyObjetcs class
!21 #13
2024-06-12 05:57:37 +09:30
Jon
da93425c0b feat(settings): add docs icon to application settings page
!21
2024-06-12 05:08:22 +09:30
Jon
8a9899cf66 feat(itam): add docs icon to software page
!21
2024-06-12 05:08:00 +09:30
Jon
38db558be2 feat(itam): add docs icon to operating system page
!21
2024-06-12 05:07:48 +09:30
Jon
67b204e40c feat(itam): add docs icon to devices page
!21
2024-06-12 05:07:36 +09:30
Jon
456fed80a9 feat(config_management): add docs icon to config groups page
!21
2024-06-12 05:07:19 +09:30
Jon
87282ce41c feat(base): add dynamic docs icon
specifying `context['model_docs_path']` with the path will add the docs icon

!21
2024-06-12 05:06:22 +09:30
Jon
4016d4c200 refactor(access): migrate team users view to use forms
!21
2024-06-12 04:31:02 +09:30
Jon
f36662ca82 refactor(access): migrate teams view to use forms
!21
2024-06-12 04:25:32 +09:30
Jon
3e340a47b8 refactor(access): migrate organization view to use form
!21
2024-06-12 04:23:24 +09:30
Jon
60a22f5574 Merge branch '55-fix-inventory' into 'development'
fix: inventory upload cant determin object organization

Closes #55

See merge request nofusscomputing/projects/django_template!24
2024-06-11 16:30:08 +00:00
Jon
2eb50311b4 docs(api): document the inventory endpoint
!24 #55
2024-06-12 01:48:30 +09:30
Jon
36fa364d04 docs(api): notate inventory permission
!24 #55
2024-06-11 22:21:54 +09:30
Jon
65c6065ba1 test(api): Inventory upload permission checks
!24 #55
2024-06-11 22:19:06 +09:30
Jon
505f4cfdd9 fix(api): ensure proper permission checking
!24 fixes #55
2024-06-11 22:18:50 +09:30
Jon
2252c86f71 Merge branch 'ui-minor' into 'development'
chore: Ui minor

See merge request nofusscomputing/projects/django_template!23
2024-06-10 01:08:45 +00:00
Jon
dc4968ee7b fix: dont throw an exception during settings load for an item django already checks
!23
2024-06-10 09:57:10 +09:30
Jon
3fb2706321 refactor(base): cleanup form and prettyfy
!23 #24
2024-06-10 09:37:54 +09:30
Jon
f05e51510a Merge branch 'feat-43-config-group-software' into 'development'
feat: config group software

Closes #43 and #53

See merge request nofusscomputing/projects/django_template!22
2024-06-09 19:10:10 +00:00
Jon
193dbf1e8b test(config_management): testing of config_groups rendered config
!22 #43
2024-06-10 04:25:29 +09:30
Jon
05bb6f8a51 docs: update contributing with further test info
!22
2024-06-10 03:02:51 +09:30
Jon
6b851ded0e test(config_management): history save tests for config groups software
!22 closes #43
2024-06-10 02:58:33 +09:30
Jon
8d6826f7c0 fix(core): Add overrides for delete so delete history saved for items with parent model
!22 fixes #53
2024-06-10 02:44:57 +09:30
Jon
fe0696fee6 feat(models): add property parent_object to models that have a parent
!22
2024-06-10 00:51:59 +09:30
Jon
11ec62feb6 test(config_management): config group software permission for add, change and delete
!22 #43
2024-06-09 18:11:16 +09:30
Jon
e62a570be3 docs(config_management): notate software group actions
!22 #43
2024-06-08 08:46:32 +09:30
Jon
36962109d1 test(base): placeholder tests for config groups software
!22 #43
2024-06-08 08:37:11 +09:30
Jon
b3b5ad6372 test(base): basic test for merge_software helper
!22 #43
2024-06-08 08:31:55 +09:30
Jon
23c43ed8dc fix(config_management): correct delete success url
!22 #43
2024-06-08 08:31:20 +09:30
Jon
1069211d1b feat(config_management): add config group software to group history
!22 #43
2024-06-08 08:30:55 +09:30
Jon
460eff1f71 feat(itam): render group software config within device rendered config
!22 #43
2024-06-08 07:18:16 +09:30
Jon
0c382a73e5 feat(config_management): assign software action to config group
!22 #43
2024-06-08 07:17:20 +09:30
Jon
ae81ee8863 refactor(config_management): relocate groups views to own directory
!22
2024-06-08 04:07:40 +09:30
Jon
07e93243a0 fix(base): remove social auth from nav menu
!21
2024-06-08 02:22:42 +09:30
Jon
579e44f834 fix(access): add a team user permissions to use team organization
use the parent models organization for permissions to work

!21 fixes #51
2024-06-08 02:20:17 +09:30
Jon
156e446608 Merge branch 'feat-sso' into 'development'
feat: sso

See merge request nofusscomputing/projects/django_template!20
2024-06-07 13:19:10 +00:00
Jon
158eb17907 test: during unit tests add SECRET_KEY
required so that tests run

!20
2024-06-07 22:41:23 +09:30
Jon
8b887575c9 feat: add configuration value 'SESSION_COOKIE_AGE'
!20
2024-06-07 21:27:33 +09:30
Jon
d0e8e9a674 feat: remove development SECRET_KEY and enforce checking for user configured one
!20
2024-06-07 21:13:39 +09:30
Jon
d8d75c7db0 feat(base): build CSRF trusted origins from configuration
!20
2024-06-07 20:36:59 +09:30
Jon
3b743a847c refactor: login to use base template
!20
2024-06-07 20:08:07 +09:30
Jon
95a08b2d2c refactor: adjust template block names
renamed so they make sense

!20
2024-06-07 20:01:56 +09:30
Jon
b38984fcb9 feat(base): Enforceable SSO ONLY
this setting removes the login form and forces a redirect to the SSO signin page.

!20 #1
2024-06-07 19:33:57 +09:30
Jon
3040d4afe7 feat(base): configurable SSO
!20 #1
2024-06-07 19:23:39 +09:30
Jon
fa28fd436e ci: dont rebuild on dev on git tag
!19
2024-06-06 07:27:51 +09:30
60ecb2e18f bump: version 0.3.0 → 0.4.0 2024-06-05 21:35:59 +00:00
Jon
4ee6347306 Merge branch '2024-06-05' into 'development'
feat: 2024 06 05

Closes #16, #48, and #47

See merge request nofusscomputing/projects/django_template!18
2024-06-05 21:12:26 +00:00
Jon
adeffff42c feat(database): add mysql support
!19 closes #16
2024-06-06 06:29:18 +09:30
Jon
c0173d6feb feat(api): move invneotry api endpoint to '/api/device/inventory'
!18
2024-06-06 04:55:27 +09:30
Jon
d100c311dd test(api): placeholder test for inventory
!18 #15
2024-06-06 04:54:16 +09:30
Jon
930e5aeb69 test(settings): access permission check for app settings
!18 #15 closes #48
2024-06-06 02:30:29 +09:30
Jon
ff595b0cba test(settings): history view permission check for software category
!18 #48 #15
2024-06-06 01:44:51 +09:30
Jon
a4bc4b1560 test(settings): history view permission check for manufacturer
!18 #48 #15
2024-06-06 01:44:40 +09:30
Jon
735ac287f9 test(settings): history view permission check for device type
!18 #48 #15
2024-06-06 01:44:22 +09:30
Jon
eb6ae13c58 feat(core): support more history types
!18
2024-06-06 01:43:57 +09:30
Jon
dd0c13a65f fix(itam): ensure device type saves history
!18
2024-06-06 01:43:20 +09:30
Jon
cb09252b7b test(settings): user settings
!18 #15
2024-06-06 01:25:44 +09:30
Jon
b24cf33207 test(settings): view permission check for user settings
!18 #48 #15
2024-06-06 01:20:50 +09:30
Jon
f053b9c6a8 test: refactor core test layout
!18 #48 #15
2024-06-05 23:47:31 +09:30
Jon
4da47e9a70 test(itam): view permission check for software
!18 #48 #15
2024-06-05 23:46:58 +09:30
Jon
cbe865d5ce test(itam): view permission check for operating system
!18 #48 #15
2024-06-05 23:46:48 +09:30
Jon
063ffaed43 test(itam): view permission check for device model
!18 #48 #15
2024-06-05 23:46:36 +09:30
Jon
7e3f0e0541 test(itam): view permission check for device
!18 #48 #15
2024-06-05 23:46:28 +09:30
Jon
35cc88857a test(config_management): view permission check for config_groups
!18 #48 #15
2024-06-05 23:46:04 +09:30
Jon
569455c127 test(access): view permission check for team
!18 #48 #15
2024-06-05 23:45:43 +09:30
Jon
378ae32552 test(access): view permission check for organization
!18 #48 #15
2024-06-05 23:45:36 +09:30
Jon
4cafa34d69 fix(core): correct history view permissions
!18 #48 #15
2024-06-05 23:44:48 +09:30
Jon
46bdd488ec feat(core): function to fetch history entry item
required for the checking of user permissions against the organization

!18 #48 #15
2024-06-05 23:44:17 +09:30
Jon
6650434c63 refactor(access): cache object so it doesnt have to be called multiple times
!18
2024-06-05 23:42:34 +09:30
Jon
2c1bbbfc15 fix(config_management): set config dict keys to be valid ansible variables
!18 fixes #47
2024-06-05 19:05:32 +09:30
Jon
dd30a57a9d fix(itam): correct logic for device add dynamic success url
!18
2024-06-05 18:45:49 +09:30
Jon
18e84db63c fix(itam): correct config group link for device
!18
2024-06-05 18:44:54 +09:30
Jon
23a06be3eb Merge branch 'feat-2024-06-02' into 'development'
feat: 2024 06 02

Closes #42 and #45

See merge request nofusscomputing/projects/django_template!17
2024-06-03 06:13:17 +00:00
Jon
0a17329a71 docs(config_management): notate future feature
!17 closes #42
2024-06-03 15:39:22 +09:30
Jon
0d18e974dd docs: correct liniting errors
!17
2024-06-03 15:32:05 +09:30
Jon
d1b6c96d72 test: add history entry creation tests for most models
!17 #15 #46
2024-06-03 15:24:11 +09:30
Jon
df98fbaecc chore: remove old pages
!17
2024-06-03 14:35:01 +09:30
Jon
55f0db2217 feat(config_management): Add button to groups ui for adding child group
!17 #42
2024-06-03 14:28:49 +09:30
Jon
7fe1260308 feat(access): throw error if no organization added
!17
2024-06-03 14:28:12 +09:30
Jon
5873897184 refactor(config_management): move groups to nav menu
!17 #42
2024-06-03 14:27:12 +09:30
Jon
62e605d417 docs(config_management): document module
!17 #42
2024-06-03 12:19:36 +09:30
Jon
1f35f44f20 test(config_management): when adding a host to config group filter out host that are already members of the group
!17 #42
2024-06-03 11:12:16 +09:30
Jon
7eee0a26a9 test(config_management): unit test for config groups model to ensure permissions are working
!17 #42
2024-06-03 07:50:30 +09:30
Jon
df27a7dfd3 feat(itam): add delete button to config group within ui
!17 #42
2024-06-03 07:49:50 +09:30
Jon
c9098f5d2f fix(config_management): correct model permissions
!17 #42
2024-06-03 07:49:03 +09:30
Jon
5cb155e01f feat(itam): Config groups rendered configuration now part of devices rendered configuration
!17 #42
2024-06-03 06:27:16 +09:30
Jon
39bfbd25cb feat(config_management): Ability to delete a host from a config group
!17 #42
2024-06-03 06:25:58 +09:30
Jon
fff51e38d2 feat(config_management): Ability to add a host to a config group
!17 #42
2024-06-03 06:25:30 +09:30
Jon
746b7ac747 feat(config_management): ensure config doesn't use reserved config keys
!17 #42
2024-06-03 02:01:31 +09:30
Jon
d422f2feee fix(config_management): add config management to navigation
!17 #42
2024-06-03 01:51:10 +09:30
Jon
a7d195dfcb feat(config_management): Config groups rendered config
new tab to display the rendered configuration

!17 #42
2024-06-03 01:35:09 +09:30
Jon
fdeae217fa feat(config_management): add configuration groups
!17 #42
2024-06-03 01:31:51 +09:30
Jon
8061b7c8e2 fix(ui): remove api entries from navigation
!17
2024-06-02 19:49:58 +09:30
Jon
3f68d67ba5 feat(api): add swagger ui for documentation
!17
2024-06-02 19:49:36 +09:30
Jon
4151e0afdc feat(api): filter software to users organizations
!17 closes #45
2024-06-02 19:48:14 +09:30
Jon
89a5e0f4cc feat(api): filter devices to users organizations
!17 #45
2024-06-02 19:46:23 +09:30
Jon
64f4c8f2e8 Merge branch 'randomz' into 'development'
feat: randomz

Closes #41

See merge request nofusscomputing/projects/django_template!16
2024-06-01 18:32:34 +00:00
Jon
f41282d08b fix(api): check for org must by by type None
!16
2024-06-02 03:58:09 +09:30
Jon
e257c11488 refactor(api): migrate devices and software to viewsets
!16
2024-06-02 03:57:38 +09:30
Jon
2dba8997e9 test(api): remove tests for os and manufacturer as they are not used in api
!16 #15
2024-06-02 02:08:48 +09:30
Jon
397ec56028 test(api): check model permissions for software
!16 #15
2024-06-02 02:02:24 +09:30
Jon
8dfb996b24 fix(api): correct software permissions
!16
2024-06-02 02:02:07 +09:30
Jon
c3f3c1247e chore(api): implement workaround for listview
until logic written, all listview access=true

!16
2024-06-02 01:39:23 +09:30
Jon
59b5fea639 test(api): check model permissions for devices
!16 #15
2024-06-02 01:04:06 +09:30
Jon
95dc979419 fix(api): corrct device permissions
!16
2024-06-02 01:03:52 +09:30
Jon
33b1a6c91d refactor(api): move permission check to mixin
!16
2024-06-02 01:03:05 +09:30
Jon
fbdbede429 docs(api): add team/org paths
!16 closes #41
2024-06-01 22:23:36 +09:30
Jon
2bf692788c test(api): check model permissions for teams
!16 #15
2024-06-01 22:08:39 +09:30
Jon
09cc1db665 fix(api): permissions for teams
!16
2024-06-01 22:07:13 +09:30
Jon
e7c535c48d fix(api): correct reverse url lookup to use NS API
!16
2024-06-01 22:04:26 +09:30
Jon
5f3b48ea98 refactor(access): add team option to org permission check
!16
2024-06-01 22:03:05 +09:30
Jon
6437170ee8 test(api): check model permissions for organizations
!16 #15
2024-05-31 23:54:28 +09:30
Jon
e9cd111af6 fix(api): permissions for organization
!16
2024-05-31 23:37:36 +09:30
Jon
3fef74e700 feat(api): add org team view page
!16 #41
2024-05-31 17:54:49 +09:30
Jon
776c5db8ca Merge branch '36-feat-api-permissions' into 'development'
feat: API configuration of permissions

Closes #36

See merge request nofusscomputing/projects/django_template!15
2024-05-31 06:28:20 +00:00
Jon
95f7cb2bfc chore: clean footer bracket
!15
2024-05-31 15:52:06 +09:30
Jon
8e338c7ca0 ci: add pytest coverage report as environment
!15 #37
2024-05-31 15:01:21 +09:30
Jon
9b811ede26 ci: run container build/publish on git tag
!15
2024-05-31 14:56:05 +09:30
Jon
c0a09d5d50 feat(api): configure team permissions
!5 closes #36
2024-05-31 14:55:54 +09:30
8572b3b3c4 bump: version 0.2.0 → 0.3.0 2024-05-29 01:32:36 +00:00
Jon
7afca156c0 Merge branch 'randomz' into 'development'
feat: Randomz

See merge request nofusscomputing/projects/django_template!13
2024-05-29 01:14:11 +00:00
Jon
cb79854027 chore(vscode): add python debugger to extension recommendations
!13
2024-05-29 10:38:25 +09:30
Jon
4b080251e9 test: cleanup duplicate tests and minor reshuffle
!13
2024-05-29 10:15:24 +09:30
Jon
3c36a988ad test(access): unit testing team user permissions
!13 #15
2024-05-29 09:42:34 +09:30
Jon
d379205bff fix(settings): Add correct permissions for team user delete
!13
2024-05-29 09:42:20 +09:30
Jon
ebf4cb7a5d fix(settings): Add correct permissions for team user view/change
!13
2024-05-29 09:42:06 +09:30
Jon
dd0eaae6b3 refactor(access): add to models a get_organization function
!13
2024-05-29 09:39:55 +09:30
Jon
a9ea173e74 test(access): unit testing team permissions
!13 #15
2024-05-29 08:38:04 +09:30
Jon
e34d29987e refactor(access): remove change view
change is part of view

!13
2024-05-29 08:37:43 +09:30
Jon
b5669c8386 fix(settings): Add correct permissions for team view/change
!13
2024-05-29 08:35:29 +09:30
Jon
58e688e0a5 fix(settings): Add correct permissions for team add
!13
2024-05-29 08:35:03 +09:30
Jon
e3c2f712c1 fix(settings): Add correct permissions for team delete
!13
2024-05-29 08:33:14 +09:30
Jon
0abcb4628e fix(access): correct back link within team view
!13
2024-05-29 08:22:34 +09:30
Jon
b9a2d2ac59 fix(access): correct url name to be within naming conventions
!13
2024-05-29 08:22:11 +09:30
Jon
dd49f92a31 test(settings): unit testing manufacturer permissions
!13 #15
2024-05-29 07:30:09 +09:30
Jon
8bfc952f2e fix(settings): Add correct permissions for manufacturer / publisher delete
!13
2024-05-29 07:29:53 +09:30
Jon
6e6bd1070e fix(settings): Add correct permissions for manufacturer / publisher add
!13
2024-05-29 07:29:42 +09:30
Jon
42fd648e4c fix(settings): Add correct permissions for manufacturer / publisher view/update
!13
2024-05-29 07:29:24 +09:30
Jon
3eb6627b40 test(settings): unit testing software category permissions
!13 #15
2024-05-29 07:16:56 +09:30
Jon
9893e5f952 fix(settings): Add correct permissions for software category delete
!13
2024-05-29 07:16:30 +09:30
Jon
e35a2300e2 fix(settings): Add correct permissions for software category add
!13
2024-05-29 07:16:18 +09:30
Jon
0aa78a4c51 fix(settings): Add correct permissions for software category view/update
!13
2024-05-29 07:16:00 +09:30
Jon
46e1c97a44 test(device_model): unit testing device type permissions
!13 #15
2024-05-29 07:02:27 +09:30
Jon
84d895c214 fix(settings): Add correct permissions for device type delete
!13
2024-05-29 07:02:04 +09:30
Jon
cba28108e0 fix(settings): Add correct permissions for device type add
!13
2024-05-29 07:01:48 +09:30
Jon
18339547ba fix(settings): Add correct permissions for device type view/update
!13
2024-05-29 07:00:57 +09:30
Jon
97d38275a4 test(device_model): unit testing device model permissions
!13 #15
2024-05-29 06:46:58 +09:30
Jon
d2e9e1070e fix(settings): Add correct permissions for device model delete
!13
2024-05-29 06:46:26 +09:30
Jon
6880c5e90b fix(settings): Add correct permissions for device model add
!13
2024-05-29 06:46:06 +09:30
Jon
608a38384d fix(settings): Add correct permissions for device model view/update
!13
2024-05-29 06:45:10 +09:30
Jon
7f7f719731 feat(access): during organization permission check, check to ensure user is logged on
!13
2024-05-29 06:22:24 +09:30
Jon
26bea9edb2 test(organization): unit testing organization permissions
!13 #15
2024-05-29 05:34:25 +09:30
Jon
cb7987f841 fix(access): Add correct permissions for organization view/update
!13
2024-05-29 05:33:48 +09:30
Jon
98885a32e7 fix(access): use established view naming
!13
2024-05-29 05:33:10 +09:30
Jon
8d786d4dea feat(history): always create an entry even if user=none
a none user denotes an admin operation, i.e. cli action

!13
2024-05-29 05:11:40 +09:30
Jon
bc18a1b2bb test(operating_system): unit testing operating system permissions
!13 #15
2024-05-29 05:07:20 +09:30
Jon
6b37c952f8 fix(itam): Add correct permissions for operating system delete
!13
2024-05-29 05:06:41 +09:30
Jon
d81d1ba32a fix(itam): Add correct permissions for operating system add
!13
2024-05-29 05:06:28 +09:30
Jon
01c6cd4bdf fix(itam): Add correct permissions for operating system view/update
!13
2024-05-29 05:06:10 +09:30
Jon
729a36ae40 test(software): unit testing software permissions
!13 #15
2024-05-29 04:52:26 +09:30
Jon
8805823405 fix(itam): Add correct permissions for software delete
!13
2024-05-29 04:50:41 +09:30
Jon
7dd2634fac fix(itam): Add correct permissions for software add
!13
2024-05-29 04:50:26 +09:30
Jon
b1cfb9fa59 fix(itam): for non-admin user use correct order by fields for software view/update
!13
2024-05-29 04:50:10 +09:30
Jon
550e6f4080 fix(itam): Add correct permissions for software view/update
!13
2024-05-29 04:49:23 +09:30
Jon
94116fa173 fix(itam): ensure permission_required parameter for view is a list
!13
2024-05-29 04:48:12 +09:30
Jon
0e72668454 fix(core): dont save history when no user information available
!13
2024-05-29 04:47:12 +09:30
Jon
37ceffcb3b fix(access): during organization permission check, check the entire list of permissions
!13
2024-05-29 04:46:33 +09:30
Jon
6dd6a33707 test(device): unit testing device permissions
!13 #15
2024-05-29 04:09:46 +09:30
Jon
c656f5bce5 fix(core): dont save history for anonymous user
anonymous user is intended not to be granted access to any model

!13
2024-05-29 04:09:35 +09:30
Jon
6997232198 chore: add vscode launch for debugging app
!13
2024-05-29 03:30:35 +09:30
Jon
6cb69c627f fix(access): during permission check use post request params for an add action
!13
2024-05-29 03:24:32 +09:30
Jon
80c3af32d5 fix(user): on new-user signal create settings row if not exist
!13
2024-05-29 01:22:22 +09:30
Jon
9d6bd6db83 fix(itam): ensure only user with change permission can change a device
!13
2024-05-29 01:21:36 +09:30
Jon
2750750a0c fix(user): if user settings row doesn't exist on access create
cater for new user

!13
2024-05-28 22:19:03 +09:30
Jon
664ad0ec7d fix(access): adding/deleting team group actions moved to model save/delete method override
!13
2024-05-28 22:17:21 +09:30
Jon
353117aa74 feat(itam): device uuid must be unique
!13
2024-05-28 16:05:19 +09:30
Jon
c4fe218592 feat(itam): device serial number must be unique
!13
2024-05-28 16:05:07 +09:30
Jon
1c9d8b1c7e fix(api): add teams and permissions to org and teams respectively
!13
2024-05-28 15:05:27 +09:30
Jon
a3716b0158 fix(ui): correct repo url used
!13
2024-05-28 15:04:41 +09:30
Jon
752770ec32 fix(api): device inventory date set to read only
!13
2024-05-28 15:04:14 +09:30
Jon
256d6e6c45 Merge branch 'feat-2024-05-26' into 'development'
feat: 2024 05 26

Closes #31, #29, #30, #34, #35, and #32

See merge request nofusscomputing/projects/django_template!12
2024-05-27 05:55:05 +00:00
Jon
bf69a30163 feat(setting): Enable super admin to set ALL manufacturer/publishers as global
sets is_global=true and creates manufacturers/publishers in global organization

!12
2024-05-27 15:06:58 +09:30
Jon
ece6b9e354 feat(setting): Enable super admin to set ALL device types as global
sets is_global=true and creates device types in global organization

!12 closes #31
2024-05-27 14:29:43 +09:30
Jon
abbda7b400 feat(setting): Enable super admin to set ALL device models as global
sets is_global=true and creates device models in global organization

!12 closes #29
2024-05-27 14:20:10 +09:30
Jon
935e119e64 feat(setting): Enable super admin to set ALL software categories as global
sets is_global=true and creates software categories in global organization

!12 closes #30
2024-05-27 13:59:40 +09:30
Jon
da0d3a816d feat(UI): show build details with page footer
!12 #25
2024-05-27 13:32:20 +09:30
Jon
19d24b54a2 ci(docker): build on any change
!12
2024-05-27 13:31:14 +09:30
Jon
baa8bc40ec build(docker): include git tag sha and project url within container
!12 #25
2024-05-27 13:03:33 +09:30
Jon
f453075d20 chore(app): add base app for project management
!12 closes #34
2024-05-27 12:47:38 +09:30
Jon
174f66a397 chore(app): add base app for config management
!12 closes #35
2024-05-27 12:46:52 +09:30
Jon
51e52e69a4 feat(software): Add output to stdout to show what is and has occurred
!12 fixes #32
2024-05-25 22:13:14 +09:30
Jon
46af675f3c fix(software): ensure management command query correct for migration
!12 #32
2024-05-25 22:12:33 +09:30
Jon
96777f1bea Merge branch 'feat-2024-05-25' into 'development'
feat: 2024 05 25

Closes #27 and #28

See merge request nofusscomputing/projects/django_template!11
2024-05-25 10:08:36 +00:00
Jon
668e871e4f refactor(itam): relocation item delete from list to inside device
!11 #23
2024-05-25 19:30:01 +09:30
Jon
b2f7c83155 feat(base): Add delete icon to content header
only display if model_delete_url is set

!11 #23
2024-05-25 19:08:22 +09:30
Jon
af809183c8 chore: set if device has no type, return '-' for listview
!11
2024-05-25 18:38:11 +09:30
Jon
e66e9b8dca feat(itam): Populate initial organization value from user default organization for software category creation
!11 #28
2024-05-25 18:25:59 +09:30
Jon
4c002bc259 feat(itam): Populate initial organization value from user default organization for device type creation
!11 #28
2024-05-25 18:25:43 +09:30
Jon
90f95672aa feat(itam): Populate initial organization value from user default organization for device model creation
!11 #28
2024-05-25 18:25:26 +09:30
Jon
7f3bf95b46 feat(api): Populate initial organization value from user default organization inventory
!11 #28
2024-05-25 18:21:31 +09:30
Jon
9f5e5d25ec feat(itam): Populate initial organization value from user default organization for Software creation
!11 #28
2024-05-25 17:47:33 +09:30
Jon
62c0bb77fe feat(itam): Populate initial organization value from user default organization for operating system creation
!11 #28
2024-05-25 17:46:05 +09:30
Jon
7f4a036a32 fix(device): OS form trying to add last inventory date when empty
!11
2024-05-25 17:42:15 +09:30
Jon
abbd6a49d6 feat(device): Populate initial organization value from user default organization
!11 #28
2024-05-25 17:41:35 +09:30
Jon
395f24f22c feat: Add management command software
allows setting software as global and migrating to global organization

!11 #27
2024-05-25 15:37:17 +09:30
Jon
f36400dbb9 feat(setting): Enable super admin to set ALL software as global
sets is_global=true and creates software in global organization

!11 closes #27
2024-05-25 14:18:01 +09:30
Jon
ee7977fe4a feat(user): Add user settings panel
!11 closes #28
2024-05-25 10:44:49 +09:30
Jon
900412b317 refactor(context_processor): relocate as base
!11
2024-05-25 10:11:48 +09:30
Jon
249b9cbab9 fix: add static files path to urls
!11
2024-05-25 07:57:30 +09:30
Jon
8e2b3b4e2a Merge branch '12-manufacturer-model-info' into 'development'
feat: Manufacturer and Model Information

Closes #17, #18, and #12

See merge request nofusscomputing/projects/django_template!10
2024-05-23 13:09:25 +00:00
Jon
f5d5529c17 fix(inventory): Dont select device_type, use 'null'
!10 fixes #17
2024-05-23 22:35:45 +09:30
Jon
d2dba2f7b8 fix(base): show "content_title - SITE_TITLE" as site title
!10 fixes #18
2024-05-23 22:31:52 +09:30
Jon
3af254d9e8 docs: update software and os
!10 #12
2024-05-23 22:27:54 +09:30
Jon
23e661cef0 refactor(itam): software index does not require created and modified date
info belongs inside item view as a detail

!10
2024-05-23 22:23:06 +09:30
Jon
2fcbb1ead7 feat(itam): Add publisher to software
!10 #12
2024-05-23 22:21:10 +09:30
Jon
53baeb59c9 feat(itam): Add publisher to operating system
!10 #12
2024-05-23 22:20:40 +09:30
Jon
99a559fe6d feat(itam): Add device model
!10 closes #12
2024-05-23 21:42:03 +09:30
Jon
ef463b845d feat(core): Add manufacturers
!10 #12
2024-05-23 20:31:53 +09:30
Jon
a6a0da72b2 refactor(organizations): set org field to null if not set
!10
2024-05-23 20:29:48 +09:30
Jon
bf0fa3f41d feat(settings): add dummy model for permissions
!10
2024-05-23 19:46:32 +09:30
Jon
66e8b29014 refactor(itam): move software categories to settings app
!10
2024-05-23 19:26:09 +09:30
Jon
c83b883673 refactor(itam): move device types to settings app
!10
2024-05-23 19:16:55 +09:30
Jon
ac233e432f feat(settings): new module for whole of application settings/globals
!10
2024-05-23 19:14:07 +09:30
Jon
88f1007a74 Merge branch 'feat-2024-05-21' into 'development'
feat: 2024 05 21-23

Closes #5

See merge request nofusscomputing/projects/django_template!9
2024-05-23 06:20:13 +00:00
Jon
9e1a024a12 test: adjust test layout and update contributing
!9 #15
2024-05-23 15:45:30 +09:30
Jon
724c52b777 feat(access): Save changes to history for organization and teams
!9 closes #5
2024-05-23 14:07:36 +09:30
Jon
f7444892d0 docs(core): Add history docs
!9 #5
2024-05-23 13:10:58 +09:30
Jon
ae4ef9d14d test(core): placeholder tests for history component
!9 #5
2024-05-23 13:10:21 +09:30
Jon
b5470f2cef feat(software): Save changes to history
!9 #5
2024-05-23 12:43:30 +09:30
Jon
e16a4212cc feat(operating_system): Save changes to history
!9 #5
2024-05-23 12:43:25 +09:30
Jon
6cbcd4aa56 feat(device): Save changes to history
!9 #5
2024-05-23 12:43:19 +09:30
Jon
9b2abecac3 feat(core): history model for saving model history
!9 #5
2024-05-23 12:43:04 +09:30
Jon
41621c6a64 chore(notes): ensure blank note cant be saved
!9
2024-05-22 13:56:31 +09:30
Jon
2689c35db3 fix(device): Read Only field set as required=false
!9
2024-05-22 13:55:19 +09:30
Jon
dd063feae9 chore: add description/notes issue link to models
!9 #13
2024-05-21 05:46:16 +09:30
Jon
cf5a5f5e49 chore(ui): add history icon
!9 #5
2024-05-21 03:42:56 +09:30
Jon
5bf2e03c9f Merge branch 'feat-2024-05-19' into 'development'
feat: 2024 05 19/20

Closes #7 and #2

See merge request nofusscomputing/projects/django_template!8
2024-05-20 16:29:07 +00:00
Jon
5dadc3fe98 docs(core): Add details about model notes
!8 #7
2024-05-21 01:53:29 +09:30
Jon
7ae7ffaef4 fix: correct typo in notes templates
!8 #7
2024-05-21 01:46:04 +09:30
Jon
4562e921e9 test(core): place holder tests for notes model
!8 closes #7
2024-05-21 01:37:53 +09:30
Jon
4df25575e8 chore(core): remove serial_number field from notes model
!8 #7
2024-05-21 01:36:40 +09:30
Jon
dec2942996 feat(itam): Ability to add notes to software
!8 #7
2024-05-21 01:22:15 +09:30
Jon
4d5f229fc7 feat(itam): Ability to add notes to operating systems
!8 #7
2024-05-21 01:22:02 +09:30
Jon
725e6b8c92 feat(itam): Ability to add notes on devices
!8 #7
2024-05-21 01:21:22 +09:30
Jon
8e0df948d5 feat(core): notes model added to core
!8 #7
2024-05-21 01:20:43 +09:30
Jon
cad2bfe7da chore(ui): add issue link for manufacturer and model
!8 #12
2024-05-20 20:47:26 +09:30
Jon
fb041f77eb feat(device): Record inventory date and show as part of details
inventory date is added when an inventory is processed

!8 #2
2024-05-20 20:46:44 +09:30
Jon
6b5acc0d57 docs: add inventory details
!8 #2
2024-05-20 18:14:42 +09:30
Jon
6ac8e025b0 test(api): add placeholder tests for inventory
!8 #2
2024-05-20 17:28:49 +09:30
Jon
e93ce07d88 feat(ui): Show inventory details if they exist
!8 closes #2
2024-05-20 16:48:00 +09:30
Jon
c52fd0802e feat(api): API accept computer inventory
!8 closes #2
2024-05-20 16:42:51 +09:30
Jon
bdf40d952e chore: add link for playbook
!8 #11
2024-05-20 10:07:35 +09:30
Jon
7afaa951d3 chore: add link for kb
!8 #10
2024-05-20 09:41:52 +09:30
Jon
e8ab3a0aa5 chore: add skeleton for information app
!8
2024-05-19 08:15:48 +09:30
Jon
191244ed40 refactor(template): content_title can be rendered in base
!8
2024-05-19 07:18:12 +09:30
Jon
5273b58afb fix(ui): Ensure navigation menu entry highlighted for sub items
!8
2024-05-19 06:08:49 +09:30
Jon
2c81007c0a ci(docker): always build on dev branch
!8
2024-05-19 06:07:54 +09:30
9ff8a7721a bump: version 0.1.0 → 0.2.0 2024-05-18 18:26:41 +00:00
Jon
d7c0d304e3 Merge branch 'feat-2024-05-18' into 'development'
feat: 2024 05 18

See merge request nofusscomputing/projects/django_template!6
2024-05-18 18:18:42 +00:00
Jon
fa97286dc8 docs: start to document features
!6
2024-05-19 03:39:03 +09:30
Jon
812250e941 chore: add link to device external system link
!6 #6
2024-05-19 02:14:31 +09:30
Jon
a0b5a08f0d feat(itam): Add Operating System to ITAM models
!6
2024-05-19 01:07:11 +09:30
Jon
377c78d6b8 feat(api): force content type to be JSON for req/resp
!1
2024-05-18 14:36:00 +09:30
Jon
ce18edaa39 ci: correct junit collection to use wildcard name
!6
2024-05-18 14:35:50 +09:30
Jon
95405283b9 feat(software): view software
!6
2024-05-18 14:35:38 +09:30
Jon
dd8fea30f2 Merge branch 'feat-2024-05-17' into 'development'
feat: 2024 05 17

See merge request nofusscomputing/projects/django_template!5
2024-05-17 14:18:04 +00:00
Jon
794f159a89 chore: update back button and tab text colour
!5
2024-05-17 23:45:26 +09:30
Jon
03b06bb2da chore: update tab text colour
!5
2024-05-17 22:56:59 +09:30
Jon
aade1e80d7 feat(device): Prevent devices from being set global
!5
2024-05-17 22:20:30 +09:30
Jon
0e69a0accc feat(software): if no installations found, denote
!5
2024-05-17 22:10:03 +09:30
Jon
7d007f721a docs: update
!5
2024-05-17 22:09:36 +09:30
Jon
b14a28f1c8 chore: add issue link button for computer inventorying
!5
2024-05-17 21:51:42 +09:30
Jon
b811eedb33 feat(device): configurable software version
!5
2024-05-17 21:10:15 +09:30
Jon
b0e69ee64b feat(software_version): name does not need to be unique
!5
2024-05-17 19:41:28 +09:30
Jon
b1c4e570cf feat(software_version): set is_global to match software
!5
2024-05-17 19:38:24 +09:30
Jon
68b1e15e01 chore: remove style attribute from icons
!5
2024-05-17 19:36:37 +09:30
Jon
b2e1a460c8 feat(software): prettify device software action
!5
2024-05-17 19:34:59 +09:30
Jon
9e801fa9eb fix(device): correct software link
!5
2024-05-17 19:34:32 +09:30
Jon
7f35292f64 feat(software): ability to add software versions
!5
2024-05-17 19:34:09 +09:30
Jon
7302f99753 feat(base): add stylised action button/text
!5
2024-05-17 18:53:57 +09:30
Jon
8b746bb9ff ci: correct junit report name
!5
2024-05-17 16:52:02 +09:30
Jon
6f6031fb1e feat(software): add pagination for index
!5
2024-05-17 16:43:58 +09:30
Jon
789b4a55d6 feat(device): add pagination for index
!5
2024-05-17 16:43:44 +09:30
58a428e6fa bump: version 0.0.1 → 0.1.0 2024-05-17 03:08:17 +00:00
Jon
9f0e03880b Merge branch 'api-token-auth' into 'development'
feat: API token auth

See merge request nofusscomputing/projects/django_template!3
2024-05-17 02:57:55 +00:00
Jon
962ae2b8df feat(api): initial token authentication implementation
!3
2024-05-17 12:21:52 +09:30
Jon
fe797cc66f Merge branch 'api' into 'development'
feat: itam and API setup

See merge request nofusscomputing/projects/django_template!2
2024-05-17 00:54:37 +00:00
Jon
69870e7972 chore: dont track temp files
!2
2024-05-17 10:15:43 +09:30
Jon
4b77e2e63d feat(docker): add settings to store data in separate volume
!2
2024-05-16 16:29:52 +09:30
Jon
a96fc062f2 feat(django): add split settings for specifying additional settings paths
!2
2024-05-16 15:38:24 +09:30
Jon
0c38155c44 feat(api): Add device config to device
!2
2024-05-16 13:15:48 +09:30
Jon
f59ffa581c docs: add base itam pages
!2
2024-05-15 21:57:27 +09:30
Jon
2d67f93d88 feat(itam): add organization to device installs
!1
2024-05-15 21:37:22 +09:30
Jon
dd145bb536 chore: remove org edit from device in api
!2
2024-05-15 21:30:25 +09:30
Jon
d3cafe08aa fix(itam): device software to come from device org or global not users orgs
!1
2024-05-15 21:11:36 +09:30
Jon
195bb5e4ab feat(itam): migrate app from own repo
!1
2024-05-15 20:59:13 +09:30
Jon
f98e3bc9c2 feat: Enable API by default
!1
2024-05-15 19:04:33 +09:30
Jon
9da9bb6c59 Merge branch 'genesis' into 'development'
feat: Genesis

See merge request nofusscomputing/projects/django_template!1
2024-05-15 03:11:56 +00:00
Jon
5a3450f3c0 fix(access): correct team required permissions
!1
2024-05-15 12:33:31 +09:30
Jon
903de5e33f chore: remove test urls
!1
2024-05-15 11:59:23 +09:30
Jon
4b214d0b8c feat(admin): remove team management
not required as groups are teams and managed within the app interface.

also, the permissions within admin do not match what the intent of the app provides.

!1
2024-05-15 11:52:53 +09:30
Jon
736d3930df feat(admin): remove group management
not required as groups are teams and managed within the app interface

!1
2024-05-15 11:52:41 +09:30
Jon
460f59d889 chore: disable the api until it is developed
!1
2024-05-15 11:52:30 +09:30
Jon
de83d7490b ci: sync project to github
!1
2024-05-15 10:53:01 +09:30
Jon
aaddfd0eef chore: open source project
!1
2024-05-15 10:52:44 +09:30
Jon
761afb6f2b refactor: button to use same selection colour
!1
2024-05-15 10:12:36 +09:30
Jon
50371267c1 feat(access): adjustable team permissions
!1
2024-05-15 10:02:58 +09:30
Jon
7e3492c4d1 chore: code review fixes
!1
2024-05-15 09:30:37 +09:30
Jon
c43f41d958 docs: notate global object
!1
2024-05-14 09:55:42 +09:30
Jon
102aa981ce feat(api): initial work on API
!1
2024-05-14 09:49:01 +09:30
Jon
50cc050adf feat(template): add header content icon block
!1
2024-05-14 05:21:06 +09:30
Jon
4582c955b8 chore(style): add content tab formatting
!1
2024-05-14 04:09:56 +09:30
Jon
857aa7af72 feat(tenancy): Add is_ global field
Allow global item creation where all can access if they have the correct permission

!1
2024-05-14 01:40:31 +09:30
Jon
2fe15778cb fix(fields): correct autoslug field so it works
!1
2024-05-14 01:01:20 +09:30
Jon
97fef07010 chore: settings.DEBUG=False by default
!1
2024-05-13 23:11:02 +09:30
Jon
69aec7ba6a fix(docker): build wheels then install
!1
2024-05-13 22:44:58 +09:30
Jon
070ba47de2 feat(access): when modifying a team ad/remove user from linked group
!1
2024-05-13 22:13:45 +09:30
Jon
a0f4940a09 feat(auth): include python social auth django application
allows external authorization

!1
2024-05-13 21:40:20 +09:30
Jon
44044d8510 chore: drop requirements.tx list to only required apps
remove dependencies

!1
2024-05-13 21:28:36 +09:30
Jon
b3b12638ad feat: Build docker container for release
!1
2024-05-13 21:02:41 +09:30
Jon
db5d7e18ad docs: update and include permissions
!1
2024-05-13 20:58:09 +09:30
Jon
0d1b31f9f0 chore: template cleanup
!1
2024-05-13 20:00:17 +09:30
Jon
30e7c8de42 refactor(access): remove inline form for org teams
!1
2024-05-13 18:51:39 +09:30
Jon
8e2542f9a5 ci: correct test path
!1
2024-05-13 18:17:20 +09:30
Jon
ab07fa6bcf chore: remove apps used for debugging from navigation menu
!1
2024-05-13 18:14:48 +09:30
Jon
0edfba604a refactor: rename app from itsm -> app
used app as this is a root application and not a django project app

!1
2024-05-13 17:46:58 +09:30
Jon
eb9eeff4ed ci(coverage): add test coverage to ci
!1
2024-05-13 17:23:09 +09:30
Jon
ca68c2589a feat(access): add permissions to team and user
!1
2024-05-13 17:14:48 +09:30
Jon
9d507d82df feat(style): format check boxes
!1
2024-05-13 17:12:26 +09:30
Jon
7445d8807c feat(access): delete team user form
!1
2024-05-13 14:48:37 +09:30
Jon
86046d6e92 refactor(access): dont use inline formset
!1
2024-05-13 14:48:07 +09:30
Jon
fa5703cb79 feat(view): new user
!1
2024-05-13 14:04:48 +09:30
Jon
c7986328f7 refactor(views): move views to own directory
!1
2024-05-13 14:02:43 +09:30
Jon
8a62c3f6ee feat: user who is 'is_superuser' to view everything and not be denied access
!1
2024-05-13 12:36:51 +09:30
Jon
c9f147d805 refactor(access): addjust org and teams to use different view per action
!1
2024-05-12 21:22:45 +09:30
Jon
af858dcc43 feat(access): add org mixin to current views
!1
2024-05-12 19:33:15 +09:30
Jon
2b5047db2d feat(access): add views for each action for teams
!1
2024-05-12 19:32:29 +09:30
Jon
d715038a88 feat(access): add mixin to check organization permissions against user and object
!1
2024-05-12 19:29:42 +09:30
Jon
0446d39190 feat(account): show admin site link if user is staff
!1
2024-05-11 12:37:38 +09:30
Jon
c021217811 feat(development): added the debug django app
!1
2024-05-11 12:14:51 +09:30
Jon
af5175c4e1 feat(access): rename structure to access and remove organization app in favour of own implementation
!1
2024-05-11 12:13:29 +09:30
Jon
f7bbb122e6 feat(account): Add user password change form
!1
2024-05-10 23:05:27 +09:30
Jon
5ca58f1883 test: interim unit tests
!1
2024-05-10 14:13:33 +09:30
Jon
789777a270 feat(urls): provide option to exclude navigation items
!1
2024-05-10 13:20:04 +09:30
Jon
dae7f3c47a feat(structure): unregister admin pages from organization app not required
!1
2024-05-09 12:14:39 +09:30
Jon
96a99c9df1 feat(auth): Custom Login Page
!1
2024-05-09 12:13:59 +09:30
Jon
65bd32dfad feat(auth): Add User Account Menu
!1
2024-05-09 12:13:29 +09:30
Jon
283ef9a714 feat(auth): Setup Login required
!1
2024-05-09 12:12:17 +09:30
Jon
71bcd192b3 feat: Dyno-magic build navigation from application urls.py
Will use project urls.py to gather application urls.py to build navigation menu.

!1
2024-05-09 08:41:13 +09:30
Jon
7cdfdab1fc feat(structure): Select and View an individual Organization
!1
2024-05-07 05:59:37 +09:30
Jon
dd54eae8d7 feat(structure): View Organizations
!1
2024-05-07 05:58:49 +09:30
Jon
9092445d0b feat(app): Add new app structure for organizations and teams
!1
2024-05-07 05:57:51 +09:30
Jon
5ef2b9a685 chore: formating updates including template names to html.j2
!1
2024-05-07 05:51:40 +09:30
Jon
85b46034e3 chore: default repo files inc ci
!1
2024-05-07 02:08:46 +09:30
Jon
1a8861846b feat(template): add base template
base for site layout

!1
2024-05-06 15:41:41 +09:30
Jon
81b170cabf feat(django): add organizations app
!1
2024-05-06 15:39:11 +09:30
Jon
2670b64d60 chore: add boilerplate
!1
2024-05-06 15:37:36 +09:30
537 changed files with 38855 additions and 0 deletions

21
.cz.yaml Normal file
View File

@ -0,0 +1,21 @@
---
commitizen:
customize:
change_type_map:
feature: Features
fix: Fixes
refactor: Refactoring
test: Tests
change_type_order:
- BREAKING CHANGE
- feat
- fix
- test
- refactor
commit_parser: ^(?P<change_type>feat|fix|test|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?:\s(?P<message>.*)?
name: cz_customize
prerelease_offset: 1
tag_format: $version
update_changelog_on_bump: false
version: 1.0.0-b14
version_scheme: semver

12
.dockerignore Normal file
View File

@ -0,0 +1,12 @@
.git
.git*
website-template/
gitlab-ci/
venv/
docs/
**/*.sqlite3
**/static/
__pycache__
**__pycache__
**.pyc
** .pytest*

39
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,39 @@
### :books: Summary
<!-- your summary here emojis ref: https://github.com/yodamad/gitlab-emoji -->
### :link: Links / References
<!--
using a list as any links to other references or links as required. if relevant, describe the link/reference
Include any issues or related merge requests. Note: dependent MR's also to be added to "Merge request dependencies"
-->
### :construction_worker: Tasks
- [ ] Add your tasks here if required (delete)
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
- [ ] :firecracker: Contains breaking-change Any Breaking change(s)?
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
- [ ] :notebook: Release notes updated
- [ ] :blue_book: Documentation written
_All features to be documented within the correct section(s). Administration, Development and/or User_
- [ ] :checkered_flag: Milestone assigned
- [ ] :test_tube: [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_
- [ ] :page_facing_up: Roadmap updated

31
.github/workflows/bump.yaml vendored Normal file
View File

@ -0,0 +1,31 @@
---
name: 'Bump'
on:
workflow_dispatch:
inputs:
CZ_PRE_RELEASE:
default: none
required: false
description: Create Pre-Release {alpha,beta,rc,none}
CZ_INCREMENT:
default: none
required: false
description: Type of bump to conduct {MAJOR,MINOR,PATCH,none}
push:
branches:
- 'master'
jobs:
bump:
name: 'Bump'
uses: nofusscomputing/action_bump/.github/workflows/bump.yaml@development
with:
CZ_PRE_RELEASE: ${{ inputs.CZ_PRE_RELEASE }}
CZ_INCREMENT: ${{ inputs.CZ_INCREMENT }}
secrets:
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}

109
.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,109 @@
---
name: 'CI'
on:
push:
branches:
- '**'
tags:
- '*'
env:
GIT_SYNC_URL: "https://${{ secrets.GITLAB_USERNAME_ROBOT }}:${{ secrets.GITLAB_TOKEN_ROBOT }}@gitlab.com/nofusscomputing/projects/centurion_erp.git"
jobs:
docker:
name: 'Docker'
uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development
with:
DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp"
DOCKER_PUBLISH_REGISTRY: "docker.io"
DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp"
secrets:
DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_USERNAME }}
DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_TOKEN }}
python:
name: 'Python'
uses: nofusscomputing/action_python/.github/workflows/python.yaml@development
secrets:
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
gitlab-mirror:
if: ${{ github.repository == 'nofusscomputing/centurion_erp' }}
runs-on: ubuntu-latest
steps:
- name: Checks
shell: bash
run: |
if [ "0${{ env.GIT_SYNC_URL }}" == "0" ]; then
echo "[ERROR] you must define variable GIT_SYNC_URL for mirroring this repository.";
exit 1;
fi
- name: clone
shell: bash
run: |
git clone --mirror https://github.com/${{ github.repository }} repo;
ls -la repo/
- name: add remote
shell: bash
run: |
cd repo;
echo "**************************************** - git remote -v";
git remote -v;
echo "****************************************";
git remote add destination $GIT_SYNC_URL;
- name: push branches
shell: bash
run: |
cd repo;
echo "**************************************** - git branch";
git branch;
echo "****************************************";
# git push destination --all --force;
git push destination --mirror || true;
# - name: push tags
# shell: bash
# run: |
# cd repo;
# echo "**************************************** - git tag";
# git tag;
# echo "****************************************";
# git push destination --tags --force;

14
.github/workflows/pull-requests.yaml vendored Normal file
View File

@ -0,0 +1,14 @@
---
name: Pull Requests
on:
pull_request: {}
jobs:
pull-request:
name: pull-request
uses: nofusscomputing/action_pull_requests/.github/workflows/pull-requests.yaml@development

37
.github/workflows/triage.yaml vendored Normal file
View File

@ -0,0 +1,37 @@
---
name: Triage
on:
issues:
types:
- opened
- reopened
- transferred
- milestoned
- demilestoned
- closed
- assigned
pull_request:
types:
- opened
- edited
- assigned
- reopened
- closed
jobs:
project:
name: Project
uses: nofusscomputing/action_project/.github/workflows/project.yaml@development
with:
PROJECT_URL: https://github.com/orgs/nofusscomputing/projects/3
secrets:
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}

10
.gitignore vendored
View File

@ -1 +1,11 @@
venv/**
*/static/**
__pycache__
**.sqlite3
**.sqlite
**.coverage
artifacts/
**.tmp.*
volumes/
build/
pages/

313
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,313 @@
---
variables:
MY_PROJECT_ID: "57560288"
# GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/centurion_erp.git"
# # Docker Build / Publish
# DOCKER_IMAGE_BUILD_TARGET_PLATFORMS: "linux/amd64,linux/arm64"
# DOCKER_IMAGE_BUILD_NAME: centurion-erp
# DOCKER_IMAGE_BUILD_REGISTRY: $CI_REGISTRY_IMAGE
# DOCKER_IMAGE_BUILD_TAG: $CI_COMMIT_SHA
# # Docker Publish
# DOCKER_IMAGE_PUBLISH_NAME: centurion-erp
# DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing
# DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME
# # Extra release commands
# MY_COMMAND: ./.gitlab/additional_actions_bump.sh
# Docs NFC
PAGES_ENVIRONMENT_PATH: projects/centurion_erp/
# RELEASE_ADDITIONAL_ACTIONS_BUMP: ./.gitlab/additional_actions_bump.sh
include:
# - local: .gitlab/pytest.gitlab-ci.yml
# - local: .gitlab/unit-test.gitlab-ci.yml
- project: nofusscomputing/projects/gitlab-ci
ref: development
file:
- .gitlab-ci_common.yaml
# - template/automagic.gitlab-ci.yaml
- automation/.gitlab-ci-ansible.yaml
- template/mkdocs-documentation.gitlab-ci.yaml
- lint/ansible.gitlab-ci.yaml
# Update Git Submodules:
# extends: .ansible_playbook_git_submodule
# Docker Container:
# extends: .build_docker_container
# resource_group: build
# needs: []
# script:
# - update-binfmts --display
# - |
# echo "[DEBUG] building multiarch/specified arch image";
# docker buildx build --platform=$DOCKER_IMAGE_BUILD_TARGET_PLATFORMS . \
# --label org.opencontainers.image.created="$(date '+%Y-%m-%d %H:%M:%S%:z')" \
# --label org.opencontainers.image.documentation="$CI_PROJECT_URL" \
# --label org.opencontainers.image.source="$CI_PROJECT_URL" \
# --label org.opencontainers.image.revision="$CI_COMMIT_SHA" \
# --push \
# --build-arg CI_PROJECT_URL=$CI_PROJECT_URL \
# --build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \
# --build-arg CI_COMMIT_TAG=$CI_COMMIT_TAG \
# --file $DOCKER_DOCKERFILE \
# --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
# docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
# # during docker multi platform build there are >=3 additional unknown images added to gitlab container registry. cleanup
# DOCKER_MULTI_ARCH_IMAGES=$(docker buildx imagetools inspect "$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG" --format "{{ range .Manifest.Manifests }}{{ if ne (print .Platform) \"&{unknown unknown [] }\" }}$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG@{{ println .Digest }}{{end}} {{end}}");
# docker buildx imagetools create $DOCKER_MULTI_ARCH_IMAGES --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
# docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
# rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag
# # - if: # condition_master_branch_push
# # $CI_COMMIT_BRANCH == "master" &&
# # $CI_PIPELINE_SOURCE == "push"
# # exists:
# # - '{dockerfile,dockerfile.j2}'
# # when: always
# - if:
# $CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
# &&
# $CI_COMMIT_BRANCH == "development"
# when: never
# - if: # condition_not_master_or_dev_push
# $CI_COMMIT_BRANCH != "master" &&
# $CI_COMMIT_BRANCH != "development" &&
# $CI_PIPELINE_SOURCE == "push"
# exists:
# - '{dockerfile,dockerfile.j2}'
# changes:
# paths:
# - '{dockerfile,dockerfile.j2,includes/**/*}'
# compare_to: 'development'
# when: always
# - if: $CI_COMMIT_TAG
# exists:
# - '{dockerfile,dockerfile.j2}'
# when: always
# - if: # condition_dev_branch_push
# (
# $CI_COMMIT_BRANCH == "development"
# ||
# $CI_COMMIT_BRANCH == "master"
# )
# &&
# $CI_PIPELINE_SOURCE == "push"
# exists:
# - '{dockerfile,dockerfile.j2}'
# allow_failure: true
# when: on_success
# - when: never
# .gitlab_release:
# stage: release
# image: registry.gitlab.com/gitlab-org/release-cli:latest
# before_script:
# - if [ "0$JOB_ROOT_DIR" == "0" ]; then ROOT_DIR=gitlab-ci; else ROOT_DIR=$JOB_ROOT_DIR ; fi
# - echo "[DEBUG] ROOT_DIR[$ROOT_DIR]"
# - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME"
# - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests"
# - apk update
# - apk add git curl
# - apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
# - python -m ensurepip && ln -sf pip3 /usr/bin/pip
# - pip install --upgrade pip
# - pip install -r $ROOT_DIR/gitlab_release/requirements.txt
# # - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/.
# - pip install commitizen --force
# - 'CLONE_URL="https://gitlab-ci-token:$GIT_COMMIT_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"'
# - echo "[DEBUG] CLONE_URL[$CLONE_URL]"
# - git clone -b development $CLONE_URL repo
# - cd repo
# - git branch
# - git config --global user.email "helpdesk@nofusscomputing.com"
# - git config --global user.name "nfc_bot"
# - git push --set-upstream origin development
# - RELEASE_VERSION_CURRENT=$(cz version --project)
# script:
# - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi
# - RELEASE_VERSION_NEW=$(cz version --project)
# - RELEASE_TAG=$RELEASE_VERSION_NEW
# - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]"
# - echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]"
# - echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]"
# - echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]"
# - RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H")
# - echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]"
# - |
# if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then
# echo "[DEBUG] not running extra actions, no new version";
# else
# echo "[DEBUG] Creating new Version Label";
# echo "----------------------------";
# echo ${MY_COMMAND};
# echo "----------------------------";
# cat ${MY_COMMAND};
# echo "----------------------------";
# ${MY_COMMAND};
# echo "----------------------------";
# fi
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push --set-upstream origin master; fi
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git merge --no-ff development; fi
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push origin master; fi
# after_script:
# - rm -Rf repo
# rules:
# - if: '$JOB_STOP_GITLAB_RELEASE'
# when: never
# - if: "$CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'"
# when: never
# - if: # condition_master_branch_push
# $CI_COMMIT_BRANCH == "master" &&
# $CI_PIPELINE_SOURCE == "push"
# allow_failure: false
# when: on_success
# - if: # condition_dev_branch_push
# $CI_COMMIT_BRANCH == "development" &&
# $CI_PIPELINE_SOURCE == "push"
# when: manual
# allow_failure: true
# # for testing
# # - if: '$CI_COMMIT_BRANCH != "master"'
# # when: always
# # allow_failure: true
# - when: never
# #
# # Release
# #
# Gitlab Release:
# extends:
# - .gitlab_release
# Docker.Hub.Branch.Publish:
# extends: .publish-docker-hub
# needs: [ "Docker Container" ]
# resource_group: build
# rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag
# # - if: # condition_master_branch_push
# # $CI_COMMIT_BRANCH == "master" &&
# # $CI_PIPELINE_SOURCE == "push"
# # exists:
# # - '{dockerfile,dockerfile.j2}'
# # when: always
# - if:
# $CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
# &&
# $CI_COMMIT_BRANCH == "development"
# when: never
# - if: $CI_COMMIT_TAG
# exists:
# - '{dockerfile,dockerfile.j2}'
# when: always
# - if: # condition_dev_branch_push
# $CI_COMMIT_BRANCH == "development" &&
# $CI_PIPELINE_SOURCE == "push"
# exists:
# - '{dockerfile,dockerfile.j2}'
# allow_failure: true
# when: on_success
# - when: never
# Github (Push --mirror):
# extends:
# - .git_push_mirror
# needs: []
# rules:
# - if: '$JOB_STOP_GIT_PUSH_MIRROR'
# when: never
# - if: $GIT_SYNC_URL == null
# when: never
# - if: # condition_master_or_dev_push
# $CI_COMMIT_BRANCH
# &&
# $CI_PIPELINE_SOURCE == "push"
# when: always
# - when: never
Website.Submodule.Deploy:
extends: .submodule_update_trigger
variables:
SUBMODULE_UPDATE_TRIGGER_PROJECT: nofusscomputing/infrastructure/website
environment:
url: https://nofusscomputing.com/$PAGES_ENVIRONMENT_PATH
name: Documentation
rules:
- if: # condition_dev_branch_push
$CI_COMMIT_BRANCH == "development" &&
$CI_PIPELINE_SOURCE == "push"
exists:
- '{docs/**,pages/**}/*.md'
changes:
paths:
- '{docs/**,pages/**}/*.md'
compare_to: 'master'
when: always
- when: never

View File

@ -0,0 +1,7 @@
#!/bin/sh
# Create Version label wtihn repo
curl \
--data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \
--header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \
"https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"

View File

@ -0,0 +1,39 @@
### :books: Summary
<!-- your summary here emojis ref: https://github.com/yodamad/gitlab-emoji -->
### :link: Links / References
<!--
using a list as any links to other references or links as required. if relevent, describe the link/reference
Include any issues or related merge requests. Note: dependent MR's also to be added to "Merge request dependencies"
-->
### :construction_worker: Tasks
- [ ] Add your tasks here if required (delete)
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
- [ ] Contains ~"breaking-change" Any Breaking change(s)?
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
- [ ] Release notes updated
- [ ] ~Documentation Documentation written
_All features to be documented within the correct section(s). Administration, Development and/or User_
- [ ] Milestone assigned
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_
- [ ] :page_facing_up: Roadmap updated

View File

@ -0,0 +1,81 @@
.pytest:
stage: test
image: python:3.11-alpine3.19
needs: []
before_script:
- pip install -r requirements.txt
- pip install -r requirements_test.txt
- cd app
artifacts:
expire_in: "30 days"
when: always
reports:
junit:
- artifacts/*.JUnit.xml
paths:
- artifacts/
rules:
- if: # Occur on merge
$CI_COMMIT_BRANCH
&&
(
$CI_PIPELINE_SOURCE == "push"
||
$CI_PIPELINE_SOURCE == "web"
)
when: always
- when: never
Unit:
extends: .pytest
script:
- pytest --cov --cov-report term --cov-report xml:../artifacts/coverage.xml --cov-report html:../artifacts/coverage/ --junit-xml=../artifacts/unit.JUnit.xml **/tests/unit
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
expire_in: "30 days"
when: always
reports:
coverage_report:
coverage_format: cobertura
path: artifacts/coverage.xml
junit:
- artifacts/*.JUnit.xml
paths:
- artifacts/
environment:
name: Unit Test Coverage Report
url: https://nofusscomputing.gitlab.io/-/projects/centurion_erp/-/jobs/${CI_JOB_ID}/artifacts/artifacts/coverage/index.html
UI:
extends: .pytest
script:
- apk update
- apk add chromium-chromedriver
- pytest --junit-xml=../artifacts/ui.JUnit.xml **/tests/ui
artifacts:
expire_in: "30 days"
when: always
reports:
junit:
- artifacts/*.JUnit.xml
paths:
- artifacts/
rules:
- if: # Occur on merge
$CI_COMMIT_BRANCH
&&
(
$CI_PIPELINE_SOURCE == "push"
||
$CI_PIPELINE_SOURCE == "web"
)
allow_failure: true
when: always
- when: never

8
.gitmodules vendored Normal file
View File

@ -0,0 +1,8 @@
[submodule "gitlab-ci"]
path = gitlab-ci
url = https://gitlab.com/nofusscomputing/projects/gitlab-ci.git
branch = development
[submodule "website-template"]
path = website-template
url = https://gitlab.com/nofusscomputing/infrastructure/website-template.git
branch = development

10
.nfc_automation.yaml Normal file
View File

@ -0,0 +1,10 @@
---
role_git_conf:
gitlab:
submodule_branch: "development"
default_branch: development
mr_labels: ~"type::automation" ~"impact::0" ~"priority::0"
auto_merge: true
merge_request:
patch_labels: '~"code review::not started"'

11
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"recommendations": [
"ms-python.python",
"ms-python.debugpy",
"njpwerner.autodocstring",
"streetsidesoftware.code-spell-checker-australian-english",
"streetsidesoftware.code-spell-checker",
"qwtel.sqlite-viewer",
"jebbs.markdown-extended",
]
}

37
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,37 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug: Django",
"type": "debugpy",
"request": "launch",
"args": [
"runserver",
"0.0.0.0:8002"
],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Debug: Celery",
"type": "python",
"request": "launch",
"module": "celery",
"console": "integratedTerminal",
"args": [
"-A",
"app",
"worker",
"-l",
"INFO",
"-n",
"debug-itsm@%h"
],
"cwd": "${workspaceFolder}/app"
}
]
}

20
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
"gitlab.aiAssistedCodeSuggestions.enabled": false,
"gitlab.duoChat.enabled": false,
"cSpell.enableFiletypes": [
"!python"
],
"python.testing.pytestArgs": [
// "-v",
// "--cov",
// "--cov-report xml",
"app"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"testing.coverageToolbarEnabled": true,
"cSpell.words": [
"ITSM"
],
"cSpell.language": "en-AU",
}

721
CHANGELOG.md Normal file
View File

@ -0,0 +1,721 @@
## 1.0.0-b14 (2024-08-12)
### Fixes
- **api**: ensure model_notes is an available field
### Tests
- **access**: test field model_notes
## 1.0.0-b13 (2024-08-11)
### Fixes
- Audit models for validations
- **itam**: Ensure device name is formatted according to RFC1035 2.3.1
- **itam**: Ensure device UUID is correctly formatted
- **config_management**: Ensure that config group can't set self as parent
- **settings**: ensure that the api token cant be saved to notes field
### Tests
- api field checks
- **software**: api field checks
## 1.0.0-b12 (2024-08-10)
### Fixes
- **api**: ensure org mixin is inherited by software view
- **base**: correct project links to github
### Tests
- api field checks
#128 #162
- **teams**: api field checks
- **organization**: api field checks
## 1.0.0-b11 (2024-08-10)
## 1.0.0-b10 (2024-08-09)
## 1.0.0-b9 (2024-08-09)
## 1.0.0-b8 (2024-08-09)
## 1.0.0-b7 (2024-08-09)
## 1.0.0-b6 (2024-08-09)
## 1.0.0-b5 (2024-07-31)
### feat
- add Config groups to API
- **api**: Add device config groups to devices
- **api**: Ability to fetch configgroups from api along with config
### Fixes
- **api**: Ensure device groups is read only
### Tests
- **api**: Field existence and type checks for device
- **api**: test configgroups API fields
## 1.0.0-b4 (2024-07-29)
### feat
- **swagger**: remove `{format}` suffixed doc entries
### Fixes
- release-b3 fixes
- **api**: cleanup team post/get
- **api**: confirm HTTP method is allowed before permission check
- **api**: Ensure that organizations can't be created via the API
- **access**: Team model class inheritance order corrected
### Tests
- confirm that the tenancymanager is called
## 1.0.0-b3 (2024-07-21)
### Fixes
- **itam**: Limit os version count to devices user has access to
## 1.0.0-b2 (2024-07-19)
### Fixes
- **itam**: only show os version once
## 1.0.0-b1 (2024-07-19)
### Fixes
- **itam**: ensure installed operating system count is limited to users organizations
- **itam**: ensure installed software count is limited to users organizations
## 1.0.0-a4 (2024-07-18)
### feat
- **api**: When processing uploaded inventory and name does not match, update name to one within inventory file
- **config_management**: Group name to be entire breadcrumb
### Tests
- ensure inventory upload matches by both serial number and uuid if device name different
- placeholder for moving organization
## 1.0.0-a3 (2024-07-18)
### feat
- **config_management**: Prevent a config group from being able to change organization
- **itam**: On device organization change remove config groups
### Fixes
- **config_management**: dont attempt to do action during save if group being created
- **itam**: remove org filter for device so that user can see installations
- **itam**: remove org filter for operating systems so that user can see installations
- **itam**: remove org filter for software so that user can see installations
- **itam**: Device related items should not be global.
- **itam**: When changing device organization move related items too.
## 1.0.0-a2 (2024-07-17)
### feat
- **api**: Inventory matching of device second by uuid
- **api**: Inventory matching of device first by serial number
- **base**: show warning bar if the user has not set a default organization
### Fixes
- **base**: dont show user warning bar for non-authenticated user
- **api**: correct inventory operating system selection by name
- **api**: correct inventory operating system and it's linking to device
- **api**: correct inventory device search to be case insensitive
## 1.0.0-a1 (2024-07-16)
### BREAKING CHANGE
- squashed DB migrations in preparation for v1.0 release.
### feat
- Administratively set global items org/is_global field now read-only
- **access**: Add multi-tennant manager
### Fixes
- **core**: migrate manufacturer to use new form/view logic
- **settings**: correct the permission to view manufacturers
- **access**: Correct team form fields
- **config_management**: don't exclude parent from field, only self
### Refactoring
- repo preperation for v1.0.0-Alpha-1
- Squash database migrations
### Tests
- tenancy objects
- refactor to single abstract model for inclusion.
## 0.7.0 (2024-07-14)
### feat
- **core**: Filter every form field if associated with an organization to users organizations only
- **core**: add var `template_name` to common view template for all views that require it
- **core**: add Display view to common forms abstract class
- **navigation**: always show every menu for super admin
- **core**: only display navigation menu item if use can view model
- **django**: update 5.0.6 -> 5.0.7
- **core**: add common forms abstract class
- **core**: add common views abstract class
- add postgreSQL database support
- **ui**: add config groups navigation icon
- **ui**: add some navigation icons
- **itam**: update inventory status icon
- **itam**: ensure device software pagination links keep interface on software tab
- "Migrate inventory processing to background worker"
- **access**: enable non-organization django permission checks
- **settings**: Add celery task results index and view page
- **base**: Add background worker
- **itam**: Update Serial Number from inventory if present and Serial Number not set
- **itam**: Update UUID from inventory if present and UUID not set
### Fixes
- **config_management**: Don't allow a config group to assign itself as its parent
- **config_management**: correct permission for deleting a host from config group
- **config_management**: use parent group details to work out permissions when adding a host
- **config_management**: use parent group details to work out permissions
- **itam**: Add missing permissions to software categories index view
- **itam**: Add missing permissions to device types index view
- **itam**: Add missing permissions to device model index view
- **settings**: Add missing permissions to app settings view
- **itam**: Add missing permissions to software index view
- **itam**: Add missing permissions to operating system index view
- **itam**: Add missing permissions to device index view
- **config_management**: Add missing permissions to group views
- **navigation**: always show settings menu entry
- **itam**: cater for fields that are prefixed
- **itam**: Ability to view software category
- **itam**: correct view permission
- **access**: When adding a new team to org ensure parent model is fetched
- **access**: enable org manager to view orgs
- **settings**: restrict user visible organizations to ones they are part of
- **access**: enable org manager to view orgs
- **access**: fetch object if method exists
- **docs**: update docs link to new path
- **access**: correctly set team user parent model to team
- **access**: fallback to django permissions if org permissions check is false
- **access**: Correct logic so that org managers can see orgs they manage
- **base**: add missing content_title to context
- **access**: Enable Organization Manager to view organisations they are assigned to
- **api**: correct logic for adding inventory UUID and serial number to device
- **ui**: navigation alignment and software icon
- **ui**: display organization manager name instead of ID
- **access**: ensure name param exists before attempting to access
- **itam**: dont show none/nil for device fields containing no value
- **itam**: show device model name instead of ID
- **api**: Ensure if serial number from inventory is `null` that it's not used
- **api**: ensure checked uuid and serial number is used for updating
- inventory
- **itam**: only remove device software when not found during inventory upload
- **itam**: only update software version if different
- existing device without uuid not updated when uploading an inventory
- Device Software tab pagination does not work
- **itam**: correct device software pagination
### Refactoring
- adjust views missing add/change form to now use forms
- add navigation menu expand arrows
- migrate views to use new abstract model view classes
- migrate forms to use new abstract model form class
- **access**: Rename Team Button "new user" -> "Assign User"
- **access**: model pk and name not required context for adding a device
- rename field "model notes" -> "Notes"
- remove settings model
- **ui**: increase indentation to sub-menu items
- **itam**: rename old inventory status icon for use with security
- **api**: migrate inventory processing to background worker
- **itam**: only perform actions on device inventory if DB matches inventory item
### Tests
- add test test_view_*_attribute_not_exists_fields for add and change views
- fix test_view_change_attribute_type_form_class to test if type class
- **views**: add test cases for model views
- Add Test case abstract classes to models
- **inventory**: add mocks?? for calling background worker
- **view**: view permission checks
- **inventory**: update tests for background worker changes
## 0.6.0 (2024-06-30)
### feat
- user api token
- **api**: API token authentication
- **api**: abilty for user to create/delete api token
- **api**: create token model
### Fixes
- **user_token**: conduct user check on token view access
- **itam**: use same form for edit and add
- **itam**: dont add field inventorydate if adding new item
- **api**: inventory upload requires sanitization
### Refactoring
- **settings**: use seperate change/view views
- **settings**: use form for user settings
- **tests**: move unit tests to unit test sub-directory
### Tests
- **token_auth**: test authentication method token
- more tests
- add .coveragerc to remove non-code files from coverage report
- Unit Tests TenancyObjects
- Test Cases for TenancyObjects
- tests for checking links from rendered templetes
- **core**: test cases for notes permissions
- **config_management**: config groups history permissions
- **api**: Majority of Inventory upload tests
- **access**: TenancyObject field tests
- **access**: remove skipped api tests for team users
## 0.5.0 (2024-06-17)
### feat
- Setup Organization Managers
- **access**: add notes field to organization
- **access**: add organization manger
- **config_management**: Use breadcrumbs for child group name display
- **config_management**: ability to add host to global group
- **itam**: add a status of "bad" for devices
- **itam**: paginate device software tab
- **itam**: status of device visible on device index page
- API Browser
- **core**: add skeleton http browser
- **core**: Add a notes field to manufacturer/ publisher
- **itam**: Add a notes field to software category
- **itam**: Add a notes field to device types
- **itam**: Add a notes field to device models
- **itam**: Add a notes field to software
- **itam**: Add a notes field to operating system
- **itam**: Add a notes field to devices
- **access**: Add a notes field to teams
- **base**: Add a notes field to `TenancyObjetcs` class
- **settings**: add docs icon to application settings page
- **itam**: add docs icon to software page
- **itam**: add docs icon to operating system page
- **itam**: add docs icon to devices page
- **config_management**: add docs icon to config groups page
- **base**: add dynamic docs icon
- config group software
- **models**: add property parent_object to models that have a parent
- **config_management**: add config group software to group history
- **itam**: render group software config within device rendered config
- **config_management**: assign software action to config group
- sso
- add configuration value 'SESSION_COOKIE_AGE'
- remove development SECRET_KEY and enforce checking for user configured one
- **base**: build CSRF trusted origins from configuration
- **base**: Enforceable SSO ONLY
- **base**: configurable SSO
### Fixes
- **itam**: remove requirement that user needs change device to add notes
- **core**: dont attempt to access parent_object if 'None' during history save
- **config_management**: Add missing parent item getter to model
- **core**: overridden save within SaveHistory to use default attributes
- **access**: overridden save to use default attributes
- History does not delete when item deleted
- **core**: on object delete remove history entries
- inventory upload cant determin object organization
- **api**: ensure proper permission checking
- dont throw an exception during settings load for an item django already checks
- **core**: Add overrides for delete so delete history saved for items with parent model
- **config_management**: correct delete success url
- **base**: remove social auth from nav menu
- **access**: add a team user permissions to use team organization
### Refactoring
- **access**: relocate permission check to own function
- **itam**: move device os tab to details tab
- **itam**: add device change form and adjust view to be non-form
- **itam**: migrate device vie to use manual entered fields in two columns
- **access**: migrate team users view to use forms
- **access**: migrate teams view to use forms
- **access**: migrate organization view to use form
- **base**: cleanup form and prettyfy
- **config_management**: relocate groups views to own directory
- login to use base template
- adjust template block names
### Tests
- **access**: team user model permission check for organization manager
- **access**: team model permission check for organization manager
- **access**: organization model permission check for organization manager
- **access**: add test cases for model delete as organization manager
- **access**: add test cases for model addd as organization manager
- **access**: add test cases for model change as organization manager
- **access**: add test cases for model view as organization manager
- write some more
- **core**: skip invalid tests
- **itam**: tests for device type history entries
- **core**: tests for manufacturer history entries
- move manufacturer to it's parent
- refactor api model permission tests to use an abstract class of test cases
- move tests to the module they belong to
- refactor history permission tests to use an abstract class of test cases
- refactor model permission tests to use an abstract class of test cases
- refactor history entry to have test cases in abstract classes
- **itam**: history entry tests for software category
- **itam**: history entry tests for device operating system version
- **itam**: history entry tests for device operating system
- **itam**: history entry tests for device software
- **itam**: ensure child history is removed on config group software delete
- add placeholder tests
- **itam**: ensure history is removed on software delete
- **itam**: ensure history is removed on operating system delete
- **itam**: ensure history is removed on device model delete
- **config_management**: test history on delete for config groups
- **itam**: ensure history is removed on device delete
- **access**: test team history
- **access**: ensure team user history is created and removed as required
- **access**: ensure history is removed on team delete
- **access**: ensure history is removed on item delete
- **api**: Inventory upload permission checks
- **config_management**: testing of config_groups rendered config
- **config_management**: history save tests for config groups software
- **config_management**: config group software permission for add, change and delete
- **base**: placeholder tests for config groups software
- **base**: basic test for merge_software helper
- during unit tests add SECRET_KEY
## 0.4.0 (2024-06-05)
### feat
- 2024 06 05
- **database**: add mysql support
- **api**: move invneotry api endpoint to '/api/device/inventory'
- **core**: support more history types
- **core**: function to fetch history entry item
- 2024 06 02
- **config_management**: Add button to groups ui for adding child group
- **access**: throw error if no organization added
- **itam**: add delete button to config group within ui
- **itam**: Config groups rendered configuration now part of devices rendered configuration
- **config_management**: Ability to delete a host from a config group
- **config_management**: Ability to add a host to a config group
- **config_management**: ensure config doesn't use reserved config keys
- **config_management**: Config groups rendered config
- **config_management**: add configuration groups
- **api**: add swagger ui for documentation
- **api**: filter software to users organizations
- **api**: filter devices to users organizations
- randomz
- **api**: add org team view page
- API configuration of permissions
- **api**: configure team permissions
### Fixes
- **itam**: ensure device type saves history
- **core**: correct history view permissions
- **config_management**: set config dict keys to be valid ansible variables
- **itam**: correct logic for device add dynamic success url
- **itam**: correct config group link for device
- **config_management**: correct model permissions
- **config_management**: add config management to navigation
- **ui**: remove api entries from navigation
- **api**: check for org must by by type None
- **api**: correct software permissions
- **api**: corrct device permissions
- **api**: permissions for teams
- **api**: correct reverse url lookup to use NS API
- **api**: permissions for organization
### Refactoring
- **access**: cache object so it doesnt have to be called multiple times
- **config_management**: move groups to nav menu
- **api**: migrate devices and software to viewsets
- **api**: move permission check to mixin
- **access**: add team option to org permission check
### Tests
- **api**: placeholder test for inventory
- **settings**: access permission check for app settings
- **settings**: history view permission check for software category
- **settings**: history view permission check for manufacturer
- **settings**: history view permission check for device type
- **settings**: user settings
- **settings**: view permission check for user settings
- refactor core test layout
- **itam**: view permission check for software
- **itam**: view permission check for operating system
- **itam**: view permission check for device model
- **itam**: view permission check for device
- **config_management**: view permission check for config_groups
- **access**: view permission check for team
- **access**: view permission check for organization
- add history entry creation tests for most models
- **config_management**: when adding a host to config group filter out host that are already members of the group
- **config_management**: unit test for config groups model to ensure permissions are working
- **api**: remove tests for os and manufacturer as they are not used in api
- **api**: check model permissions for software
- **api**: check model permissions for devices
- **api**: check model permissions for teams
- **api**: check model permissions for organizations
## 0.3.0 (2024-05-29)
### feat
- Randomz
- **access**: during organization permission check, check to ensure user is logged on
- **history**: always create an entry even if user=none
- **itam**: device uuid must be unique
- **itam**: device serial number must be unique
- 2024 05 26
- **setting**: Enable super admin to set ALL manufacturer/publishers as global
- **setting**: Enable super admin to set ALL device types as global
- **setting**: Enable super admin to set ALL device models as global
- **setting**: Enable super admin to set ALL software categories as global
- **UI**: show build details with page footer
- **software**: Add output to stdout to show what is and has occurred
- 2024 05 25
- **base**: Add delete icon to content header
- **itam**: Populate initial organization value from user default organization for software category creation
- **itam**: Populate initial organization value from user default organization for device type creation
- **itam**: Populate initial organization value from user default organization for device model creation
- **api**: Populate initial organization value from user default organization inventory
- **itam**: Populate initial organization value from user default organization for Software creation
- **itam**: Populate initial organization value from user default organization for operating system creation
- **device**: Populate initial organization value from user default organization
- Add management command software
- **setting**: Enable super admin to set ALL software as global
- **user**: Add user settings panel
- Manufacturer and Model Information
- **itam**: Add publisher to software
- **itam**: Add publisher to operating system
- **itam**: Add device model
- **core**: Add manufacturers
- **settings**: add dummy model for permissions
- **settings**: new module for whole of application settings/globals
- 2024 05 21-23
- **access**: Save changes to history for organization and teams
- **software**: Save changes to history
- **operating_system**: Save changes to history
- **device**: Save changes to history
- **core**: history model for saving model history
- 2024 05 19/20
- **itam**: Ability to add notes to software
- **itam**: Ability to add notes to operating systems
- **itam**: Ability to add notes on devices
- **core**: notes model added to core
- **device**: Record inventory date and show as part of details
- **ui**: Show inventory details if they exist
- **api**: API accept computer inventory
### Fixes
- **settings**: Add correct permissions for team user delete
- **settings**: Add correct permissions for team user view/change
- **settings**: Add correct permissions for team view/change
- **settings**: Add correct permissions for team add
- **settings**: Add correct permissions for team delete
- **access**: correct back link within team view
- **access**: correct url name to be within naming conventions
- **settings**: Add correct permissions for manufacturer / publisher delete
- **settings**: Add correct permissions for manufacturer / publisher add
- **settings**: Add correct permissions for manufacturer / publisher view/update
- **settings**: Add correct permissions for software category delete
- **settings**: Add correct permissions for software category add
- **settings**: Add correct permissions for software category view/update
- **settings**: Add correct permissions for device type delete
- **settings**: Add correct permissions for device type add
- **settings**: Add correct permissions for device type view/update
- **settings**: Add correct permissions for device model delete
- **settings**: Add correct permissions for device model add
- **settings**: Add correct permissions for device model view/update
- **access**: Add correct permissions for organization view/update
- **access**: use established view naming
- **itam**: Add correct permissions for operating system delete
- **itam**: Add correct permissions for operating system add
- **itam**: Add correct permissions for operating system view/update
- **itam**: Add correct permissions for software delete
- **itam**: Add correct permissions for software add
- **itam**: for non-admin user use correct order by fields for software view/update
- **itam**: Add correct permissions for software view/update
- **itam**: ensure permission_required parameter for view is a list
- **core**: dont save history when no user information available
- **access**: during organization permission check, check the entire list of permissions
- **core**: dont save history for anonymous user
- **access**: during permission check use post request params for an add action
- **user**: on new-user signal create settings row if not exist
- **itam**: ensure only user with change permission can change a device
- **user**: if user settings row doesn't exist on access create
- **access**: adding/deleting team group actions moved to model save/delete method override
- **api**: add teams and permissions to org and teams respectively
- **ui**: correct repo url used
- **api**: device inventory date set to read only
- **software**: ensure management command query correct for migration
- **device**: OS form trying to add last inventory date when empty
- add static files path to urls
- **inventory**: Dont select device_type, use 'null'
- **base**: show "content_title - SITE_TITLE" as site title
- **device**: Read Only field set as required=false
- correct typo in notes templates
- **ui**: Ensure navigation menu entry highlighted for sub items
### Refactoring
- **access**: add to models a get_organization function
- **access**: remove change view
- **itam**: relocation item delete from list to inside device
- **context_processor**: relocate as base
- **itam**: software index does not require created and modified date
- **organizations**: set org field to null if not set
- **itam**: move software categories to settings app
- **itam**: move device types to settings app
- **template**: content_title can be rendered in base
### Tests
- cleanup duplicate tests and minor reshuffle
- **access**: unit testing team user permissions
- **access**: unit testing team permissions
- **settings**: unit testing manufacturer permissions
- **settings**: unit testing software category permissions
- **device_model**: unit testing device type permissions
- **device_model**: unit testing device model permissions
- **organization**: unit testing organization permissions
- **operating_system**: unit testing operating system permissions
- **software**: unit testing software permissions
- **device**: unit testing device permissions
- adjust test layout and update contributing
- **core**: placeholder tests for history component
- **core**: place holder tests for notes model
- **api**: add placeholder tests for inventory
## 0.2.0 (2024-05-18)
### feat
- 2024 05 18
- **itam**: Add Operating System to ITAM models
- **api**: force content type to be JSON for req/resp
- **software**: view software
- 2024 05 17
- **device**: Prevent devices from being set global
- **software**: if no installations found, denote
- **device**: configurable software version
- **software_version**: name does not need to be unique
- **software_version**: set is_global to match software
- **software**: prettify device software action
- **software**: ability to add software versions
- **base**: add stylised action button/text
- **software**: add pagination for index
- **device**: add pagination for index
### Fixes
- **device**: correct software link
## 0.1.0 (2024-05-17)
### feat
- API token auth
- **api**: initial token authentication implementation
- itam and API setup
- **docker**: add settings to store data in separate volume
- **django**: add split settings for specifying additional settings paths
- **api**: Add device config to device
- **itam**: add organization to device installs
- **itam**: migrate app from own repo
- Enable API by default
- Genesis
- **admin**: remove team management
- **admin**: remove group management
- **access**: adjustable team permissions
- **api**: initial work on API
- **template**: add header content icon block
- **tenancy**: Add is_ global field
- **access**: when modifying a team ad/remove user from linked group
- **auth**: include python social auth django application
- Build docker container for release
- **access**: add permissions to team and user
- **style**: format check boxes
- **access**: delete team user form
- **view**: new user
- user who is 'is_superuser' to view everything and not be denied access
- **access**: add org mixin to current views
- **access**: add views for each action for teams
- **access**: add mixin to check organization permissions against user and object
- **account**: show admin site link if user is staff
- **development**: added the debug django app
- **access**: rename structure to access and remove organization app in favour of own implementation
- **account**: Add user password change form
- **urls**: provide option to exclude navigation items
- **structure**: unregister admin pages from organization app not required
- **auth**: Custom Login Page
- **auth**: Add User Account Menu
- **auth**: Setup Login required
- Dyno-magic build navigation from application urls.py
- **structure**: Select and View an individual Organization
- **structure**: View Organizations
- **app**: Add new app structure for organizations and teams
- **template**: add base template
- **django**: add organizations app
### Fixes
- **itam**: device software to come from device org or global not users orgs
- **access**: correct team required permissions
- **fields**: correct autoslug field so it works
- **docker**: build wheels then install
### Refactoring
- button to use same selection colour
- **access**: remove inline form for org teams
- rename app from itsm -> app
- **access**: dont use inline formset
- **views**: move views to own directory
- **access**: addjust org and teams to use different view per action
### Tests
- interim unit tests
## 0.0.1 (2024-05-06)

61
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,61 @@
# Contribution Guide
## Dev Environment
It's advised to setup a python virtual env for development. this can be done with the following commands.
``` bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
To setup the centurion erp test server run the following
``` bash
cd app
python manage.py runserver 8002
python3 manage.py migrate
python3 manage.py createsuperuser
# If model changes
python3 manage.py makemigrations --noinput
# To update code highlight run
pygmentize -S default -f html -a .codehilite > project-static/code.css
```
Updates to python modules will need to be captured with SCM. This can be done by running `pip freeze > requirements.txt` from the running virtual environment.
## Tests
!!! danger "Requirement"
All models **are** to have tests written for them, Including testing between dependent models.
See [Documentation](https://nofusscomputing.com/projects/django-template/development/testing/) for further information
## Docker Container
``` bash
cd app
docker build . --tag centurion-erp:dev
docker run -d --rm -v ${PWD}/db.sqlite3:/app/db.sqlite3 -p 8002:8000 --name app centurion-erp:dev
```

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 No Fuss Computing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

74
README.md Normal file
View File

@ -0,0 +1,74 @@
<span style="text-align: center;">
# No Fuss Computing - Centurion ERP
<br>
![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=github&style=plastic)
[![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp)
----
<br>
![GitHub forks](https://img.shields.io/github/forks/NofussComputing/centurion_erp?logo=github&style=plastic&color=000000&labell=Forks) ![GitHub stars](https://img.shields.io/github/stars/NofussComputing/centurion_erp?color=000000&logo=github&style=plastic) ![Github Watchers](https://img.shields.io/github/watchers/NofussComputing/centurion_erp?color=000000&label=Watchers&logo=github&style=plastic)
![Gitlab forks count](https://img.shields.io/badge/dynamic/json?label=Forks&query=%24.forks_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) ![Gitlab stars](https://img.shields.io/badge/dynamic/json?label=Stars&query=%24.star_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic)
<br>
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/nofusscomputing/centurion_erp?style=plastic&logo=github&label=Open%20Issues&color=000) ![GitHub Issues or Pull Requests by label](https://img.shields.io/github/issues/nofusscomputing/centurion_erp/type%3A%3Abug?style=plastic&logo=github&label=Bug%20Fixes%20Required&color=000)
This project is hosted on [Github](https://github.com/NofussComputing/centurion_erp) and has a read-only copy hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp).
----
**Stable Branch**
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=master&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_unit_test.json)
----
**Development Branch**
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=development&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_unit_test.json)
----
<br>
</div>
links:
- [Issues](https://github.com/nofusscomputing/centurion_erp/issues)
- [Merge Requests (Pull Requests)](https://github.com/nofusscomputing/centurion_erp/pulls)
An ERP with a large emphasis on the IT Service Management (ITSM) and Automation.
## Contributing
All contributions for this project must conducted from [GitHub](https://github.com/nofusscomputing/centurion_erp).
For further details on contributing please refer to the [contribution guide](CONTRIBUTING.md).
## Other
This repo is release under this [license](LICENSE)

7
Release-Notes.md Normal file
View File

@ -0,0 +1,7 @@
# Version 1.0.0
Initial Release of Centurion ERP.
## Breaking changes
- Nil

17
app/.coveragerc Normal file
View File

@ -0,0 +1,17 @@
[run]
source = .
omit =
*migrations/*
*tests/*/*
[report]
omit =
*/tests/*/*
*/migrations/*
*apps.py
*manage.py
*__init__.py
*asgi*
*wsgi*
*admin.py
*urls.py

0
app/access/__init__.py Normal file
View File

30
app/access/admin.py Normal file
View File

@ -0,0 +1,30 @@
from django.contrib import admin
from django.contrib.auth.models import Group
from .models import *
admin.site.unregister(Group)
class TeamInline(admin.TabularInline):
model = Team
extra = 0
readonly_fields = ['name', 'created', 'modified']
fields = ['team_name']
fk_name = 'organization'
class OrganizationAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["name", 'manager', "slug"]}),
#("Date information", {"fields": ["slug"], "classes": ["collapse"]}),
]
inlines = [TeamInline]
list_display = ["name", "created", "modified"]
list_filter = ["created"]
search_fields = ["team_name"]
admin.site.register(Organization,OrganizationAdmin)

6
app/access/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccessConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'access'

59
app/access/fields.py Normal file
View File

@ -0,0 +1,59 @@
from django.db import models
from django.utils.timezone import now
from django.template.defaultfilters import slugify
class AutoCreatedField(models.DateTimeField):
"""
A DateTimeField that automatically populates itself at
object creation.
By default, sets editable=False, default=datetime.now.
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault("editable", False)
kwargs.setdefault("default", now)
super().__init__(*args, **kwargs)
class AutoLastModifiedField(AutoCreatedField):
"""
A DateTimeField that updates itself on each save() of the model.
By default, sets editable=False and default=datetime.now.
"""
def pre_save(self, model_instance, add):
value = now()
setattr(model_instance, self.attname, value)
return value
class AutoSlugField(models.SlugField):
"""
A DateTimeField that updates itself on each save() of the model.
By default, sets editable=False and default=datetime.now.
"""
def pre_save(self, model_instance, add):
if not model_instance.slug or model_instance.slug == '_':
value = model_instance.name.lower().replace(' ', '_')
setattr(model_instance, self.attname, value)
return value
return model_instance.slug

View File

@ -0,0 +1,38 @@
from django import forms
from django.db.models import Q
from app import settings
from access.models import Organization
from core.forms.common import CommonModelForm
class OrganizationForm(CommonModelForm):
class Meta:
model = Organization
fields = [
'name',
'manager',
'model_notes',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created'] = forms.DateTimeField(
label="Created",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].created,
disabled=True,
required=False,
)
self.fields['modified'] = forms.DateTimeField(
label="Modified",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].modified,
disabled=True,
required=False,
)

103
app/access/forms/team.py Normal file
View File

@ -0,0 +1,103 @@
from django import forms
from django.contrib.auth.models import Permission
from django.db.models import Q
from django.forms import inlineformset_factory
from app import settings
from .team_users import TeamUsersForm, TeamUsers
from access.models import Team
from core.forms.common import CommonModelForm
TeamUserFormSet = inlineformset_factory(
model=TeamUsers,
parent_model= Team,
extra = 1,
fields=[
'user',
'manager'
]
)
class TeamFormAdd(CommonModelForm):
class Meta:
model = Team
fields = [
'team_name',
'model_notes',
]
class TeamForm(CommonModelForm):
class Meta:
model = Team
fields = [
'team_name',
'permissions',
'model_notes',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created'] = forms.DateTimeField(
label="Created",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].created,
disabled=True,
required=False,
)
self.fields['modified'] = forms.DateTimeField(
label="Modified",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].modified,
disabled=True,
required=False,
)
self.fields['permissions'].widget.attrs = {'style': "height: 200px;"}
apps = [
'access',
'assistance',
'config_management',
'core',
'django_celery_results',
'itam',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'groupresult',
'organization'
'settings',
'usersettings',
]
exclude_permissions = [
'add_organization',
'add_taskresult',
'change_organization',
'change_taskresult',
'delete_organization',
'delete_taskresult',
]
self.fields['permissions'].queryset = Permission.objects.filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)

View File

@ -0,0 +1,16 @@
from django.db.models import Q
from app import settings
from access.models import TeamUsers
from core.forms.common import CommonModelForm
class TeamUsersForm(CommonModelForm):
class Meta:
model = TeamUsers
fields = [
'user',
'manager',
]

View File

@ -0,0 +1,73 @@
# Generated by Django 5.0.7 on 2024-07-12 03:54
import access.fields
import access.models
import django.contrib.auth.models
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Organization',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=50, unique=True)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('slug', access.fields.AutoSlugField()),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('manager', models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Organizations',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Team',
fields=[
('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.group')),
('is_global', models.BooleanField(default=False)),
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
('team_name', models.CharField(default='', max_length=50, verbose_name='Name')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'verbose_name_plural': 'Teams',
'ordering': ['team_name'],
},
bases=('auth.group', models.Model),
managers=[
('objects', django.contrib.auth.models.GroupManager()),
],
),
migrations.CreateModel(
name='TeamUsers',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('manager', models.BooleanField(blank=True, default=False, verbose_name='manager')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team', to='access.team')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Team Users',
'ordering': ['user'],
},
),
]

View File

362
app/access/mixin.py Normal file
View File

@ -0,0 +1,362 @@
from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin
from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.utils.functional import cached_property
from .models import Organization, Team
class OrganizationMixin():
"""Base Organization class"""
request = None
user_groups = []
def get_parent_obj(self):
""" Get the Parent Model Object
Use in views where the the model has no organization and the organization should be fetched from the parent model.
Requires attribute `parent_model` within the view with the value of the parent's model class
Returns:
parent_model (Model): with PK from kwargs['pk']
"""
return self.parent_model.objects.get(pk=self.kwargs['pk'])
def object_organization(self) -> int:
id = None
try:
if hasattr(self, 'get_queryset'):
self.get_queryset()
if hasattr(self, 'parent_model'):
obj = self.get_parent_obj()
id = obj.get_organization().id
if obj.is_global:
id = 0
if hasattr(self, 'get_object') and id is None:
obj = self.get_object()
id = obj.get_organization().id
if hasattr(obj, 'is_global'):
if obj.is_global:
id = 0
except AttributeError:
if self.request.method == 'POST':
if self.request.POST.get("organization", ""):
id = int(self.request.POST.get("organization", ""))
for field in self.request.POST.dict(): # cater for fields prefixed '<prefix>-<field name>'
a_field = str(field).split('-')
if len(a_field) == 2:
if a_field[1] == 'organization':
id = int(self.request.POST.get(field))
except:
pass
return id
def is_member(self, organization: int) -> bool:
"""Returns true if the current user is a member of the organization
iterates over the user_organizations list and returns true if the user is a member
Returns:
bool: _description_
"""
is_member = False
if organization in self.user_organizations():
return True
return is_member
def get_permission_required(self):
"""
Override of 'PermissionRequiredMixin' method so that this mixin can obtain the required permission.
"""
if self.permission_required is None:
raise ImproperlyConfigured(
f"{self.__class__.__name__} is missing the "
f"permission_required attribute. Define "
f"{self.__class__.__name__}.permission_required, or override "
f"{self.__class__.__name__}.get_permission_required()."
)
if isinstance(self.permission_required, str):
perms = (self.permission_required,)
else:
perms = self.permission_required
return perms
@cached_property
def is_manager(self) -> bool:
""" Returns true if the current user is a member of the organization"""
is_manager = False
return is_manager
def user_organizations(self) -> list():
"""Current Users organizations
Fetches the Organizations the user is apart of.
Get All groups the user is part of, fetch the associated team,
iterate over the results adding the organization ID to a list to be returned.
Returns:
_type_: User Organizations.
"""
user_organizations = []
teams = Team.objects
for group in self.request.user.groups.all():
team = teams.get(pk=group.id)
self.user_groups = self.user_groups + [group.id]
user_organizations = user_organizations + [team.organization.id]
return user_organizations
# ToDo: Ensure that the group has access to item
def has_organization_permission(self, organization: int=None) -> bool:
has_permission = False
if not organization:
organization = self.object_organization()
if self.is_member(organization) or organization == 0:
groups = Group.objects.filter(pk__in=self.user_groups)
for group in groups:
team = Team.objects.filter(pk=group.id)
team = team.values('organization_id').get()
for permission in group.permissions.values('content_type__app_label', 'codename').all():
assembled_permission = str(permission["content_type__app_label"]) + '.' + str(permission["codename"])
if assembled_permission in self.get_permission_required() and (team['organization_id'] == organization or organization == 0):
return True
return has_permission
def permission_check(self, request, permissions_required: list = None) -> bool:
self.request = request
if permissions_required:
self.permission_required = permissions_required
organization_manager_models = [
'access.organization',
'access.team',
'access.teamusers',
]
is_organization_manager = False
queryset = None
if hasattr(self, 'get_queryset'):
queryset = self.get_queryset()
obj = None
if hasattr(self, 'get_object'):
try:
obj = self.get_object()
except:
pass
if hasattr(self, 'model'):
if self.model._meta.label_lower in organization_manager_models:
organization = Organization.objects.get(pk=self.object_organization())
if organization.manager == request.user:
is_organization_manager = True
return True
if request.user.is_superuser:
return True
perms = self.get_permission_required()
if self.has_organization_permission():
return True
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 and str(self.request.method).lower() == 'get':
return True
for required_permission in self.permission_required:
if required_permission.replace(
'view_', ''
) == 'access.organization' and len(self.kwargs) == 0:
return True
return False
class OrganizationPermission(AccessMixin, OrganizationMixin):
"""## Permission Checking
The base django permissions have not been modified with this app providing Multi-Tenancy. This is done by a mixin, that checks if the item is apart of an organization, if it is; confirmation is made that the user is part of the same organization and as long as they have the correct permission within the organization, access is granted.
### How it works
The overall permissions system of django has not been modified with it remaining fully functional. The multi-tenancy has been setup based off of an organization with teams. A team to the underlying django system is an extension of the django auth group and for every team created a django auth group is created. THe group name is set using the following format: `<organization>_<team name>` and contains underscores `_` instead of spaces.
A User who is added to an team as a "Manager" can modify the team members or if they have permission `access.change_team` which also allows the changing of team permissions. Modification of an organization can be done by the django administrator (super user) or any user with permission `access._change_organization`.
Items can be set as `Global`, meaning that all users who have the correct permission regardless of organization will be able to take action against the object.
Permissions that can be modified for a team have been limited to application permissions only unless adjust the permissions from the django admin site.
### Multi-Tenancy workflow
The workflow is conducted as part of the view and has the following flow:
1. Checks if user is member of organization the object the action is being performed on. Will also return true if the object has field `is_global` set to `true`.
1. Fetches all teams the user is part of.
1. obtains all permissions that are linked to the team.
1. checks if user has the required permission for the action.
1. confirms that the team the permission came from is part of the same organization as the object the action is being conducted on.
1. ONLY on success of the above items, grants access.
"""
permission_required: list = []
""" Permission required for the view
Not specifying this property adjusts the permission check logic so that you can
use the `permission_check()` function directly.
An example of a get request....
``` py
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.view_organization' ]):
raise PermissionDenied('You are not part of this organization')
return super().get(request, *args, **kwargs)
```
this example details manual usage of the `permission_check()` function for a get request.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if len(self.permission_required) > 0:
non_organization_models = [
'TaskResult'
]
if hasattr(self, 'model'):
if hasattr(self.model, '__name__'):
if self.model.__name__ in non_organization_models:
if hasattr(self, 'get_object'):
self.get_object()
perms = self.get_permission_required()
if not self.request.user.has_perms(perms):
return self.handle_no_permission()
return super().dispatch(self.request, *args, **kwargs)
if not self.permission_check(request):
raise PermissionDenied('You are not part of this organization')
return super().dispatch(self.request, *args, **kwargs)

324
app/access/models.py Normal file
View File

@ -0,0 +1,324 @@
from django.conf import settings
from django.db import models
from django.contrib.auth.models import User, Group, Permission
from django.forms import ValidationError
from .fields import *
from core.middleware.get_request import get_request
from core.mixin.history_save import SaveHistory
class Organization(SaveHistory):
class Meta:
verbose_name_plural = "Organizations"
ordering = ['name']
def save(self, *args, **kwargs):
if self.slug == '_':
self.slug = self.name.lower().replace(' ', '_')
super().save(*args, **kwargs)
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
name = models.CharField(
blank = False,
max_length = 50,
unique = True,
)
manager = models.ForeignKey(
User,
on_delete=models.SET_NULL,
blank = False,
null = True,
help_text = 'Organization Manager'
)
model_notes = models.TextField(
blank = True,
default = None,
null= True,
verbose_name = 'Notes',
)
slug = AutoSlugField()
created = AutoCreatedField()
modified = AutoLastModifiedField()
def get_organization(self):
return self
def __str__(self):
return self.name
class TenancyManager(models.Manager):
"""Multi-Tennant Object Manager
This manager specifically caters for the multi-tenancy features of Centurion ERP.
"""
def get_queryset(self):
""" Fetch the data
This function filters the data fetched from the database to that which is from the organizations
the user is a part of.
!!! danger "Requirement"
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
## Workflow
This functions workflow is as follows:
- Fetch the user from the request
- Check if the user is authenticated
- Iterate over the users teams
- Store unique organizations from users teams
- return results
Returns:
(queryset): **super user**: return unfiltered data.
(queryset): **not super user**: return data from the stored unique organizations.
"""
request = get_request()
user_organizations: list(str()) = []
if request:
user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user
if user.is_authenticated:
for team_user in TeamUsers.objects.filter(user=user):
if team_user.team.organization.name not in user_organizations:
if not user_organizations:
self.user_organizations = []
user_organizations += [ team_user.team.organization.id ]
if len(user_organizations) > 0 and not user.is_superuser:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
return super().get_queryset()
class TenancyObject(SaveHistory):
""" Tenancy Model Abstrct class.
This class is for inclusion wihtin **every** model within Centurion ERP.
Provides the required fields, functions and methods for multi tennant objects.
Unless otherwise stated, **no** object within this class may be overridden.
Raises:
ValidationError: User failed to supply organization
"""
objects = TenancyManager()
""" Multi-Tenanant Objects """
class Meta:
abstract = True
def validatate_organization_exists(self):
"""Ensure that the user did provide an organization
Raises:
ValidationError: User failed to supply organization.
"""
if not self:
raise ValidationError('You must provide an organization')
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
blank = False,
null = True,
validators = [validatate_organization_exists],
)
is_global = models.BooleanField(
default = False,
blank = False
)
model_notes = models.TextField(
blank = True,
default = None,
null= True,
verbose_name = 'Notes',
)
def get_organization(self) -> Organization:
return self.organization
class Team(Group, TenancyObject):
class Meta:
# proxy = True
verbose_name_plural = "Teams"
ordering = ['team_name']
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
team_name = models.CharField(
verbose_name = 'Name',
blank = False,
max_length = 50,
unique = False,
default = ''
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
@property
def parent_object(self):
""" Fetch the parent object """
return self.organization
def permission_list(self) -> list:
permission_list = []
for permission in self.permissions.all():
if str(permission.content_type.app_label + '.' + permission.codename) in permission_list:
continue
permission_list += [ str(permission.content_type.app_label + '.' + permission.codename) ]
return [permission_list, self.permissions.all()]
def __str__(self):
return self.team_name
class TeamUsers(SaveHistory):
class Meta:
# proxy = True
verbose_name_plural = "Team Users"
ordering = ['user']
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
team = models.ForeignKey(
Team,
related_name="team",
on_delete=models.CASCADE)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
manager = models.BooleanField(
verbose_name='manager',
default=False,
blank=True
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def delete(self, using=None, keep_parents=False):
""" Delete Team
Overrides, post-action
As teams are an extension of Groups, remove the user to the team.
"""
super().delete(using=using, keep_parents=keep_parents)
group = Group.objects.get(pk=self.team.id)
user = User.objects.get(pk=self.user_id)
user.groups.remove(group)
def get_organization(self) -> Organization:
return self.team.organization
def save(self, *args, **kwargs):
""" Save Team
Overrides, post-action
As teams are an extension of groups, add the user to the matching group.
"""
super().save(*args, **kwargs)
group = Group.objects.get(pk=self.team.id)
user = User.objects.get(pk=self.user_id)
user.groups.add(group)
@property
def parent_object(self):
""" Fetch the parent object """
return self.team
def __str__(self):
return self.user.username

View File

@ -0,0 +1,22 @@
{% extends 'base.html.j2' %}
{% block content_header_icon %}{% endblock %}
{% block content %}
<table class="data">
<tr>
<th>Name</th>
<th>Created</th>
<th>Modified</th>
</tr>
{% for org in organization_list %}
<tr>
<td><a href="/organization/{{ org.id }}/">{{ org.name }}</a></td>
<td>{{ org.created }}</td>
<td>{{ org.modified }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,106 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block title %}Organization - {{ organization.name }}{% endblock %}
{% block content %}
<style>
form div .helptext {
background-color: rgb(0, 140, 255);
display: block;
}
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
/*padding: 10px;*/
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
/*padding: 10px;*/
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
</style>
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.name.label }}</label>
<span>{{ form.name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.manager.label }}</label>
<span>{{ organization.manager }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.created.label }}</label>
<span>{{ form.created.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.modified.label }}</label>
<span>{{ form.modified.value }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
<div style="display: inline-block; text-align: left;">{{ form.model_notes.value | markdown | safe }}</div>
</div>
</div>
<div style="display: block;">
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:Organizations' %}';">
<input type="button" value="New Team" onclick="window.location='{% url 'Access:_team_add' organization.id %}';">
</div>
<hr />
<table>
<thead>
<tr>
<th>Team Name</th>
<th>Created</th>
<th>Modified</th>
</tr>
</thead>
{% for field in teams %}
<tr>
<td><a href="{% url 'Access:_team_view' organization_id=organization.id pk=field.id %}">{{ field.team_name }}</a></td>
<td>{{ field.created }}</td>
<td>{{ field.modified }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends 'base.html.j2' %}
{% block title %}Team - {{ team.team_name }}{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_div }}
<input style="display:unset;" type="submit" value="Submit">
</form>
<hr />
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:_organization_view' pk=organization.id %}';">
<input type="button" value="Delete Team"
onclick="window.location='{% url 'Access:_team_delete' organization_id=organization.id pk=team.id %}';">
<input type="button" value="Assign User"
onclick="window.location='{% url 'Access:_team_user_add' organization_id=organization.id pk=team.id %}';">
{{ formset.management_form }}
<table id="formset" class="form">
<thead>
<tr>
<th>User</th>
<th>Manager</th>
<th>Created</th>
<th>Modified</th>
<th>&nbsp;</th>
</tr>
</thead>
{% for field in teamusers %}
<tr>
<td>{{ field.user }}</td>
<td><input type="checkbox" {% if field.manager %}checked{% endif %} disabled></td>
<td>{{ field.created }}</td>
<td>{{ field.modified }}</td>
<td><a
href="{% url 'Access:_team_user_delete' organization_id=organization.id team_id=field.team_id pk=field.id %}">Delete</a></a>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

View File

View File

@ -0,0 +1,251 @@
import pytest
import unittest
from django.test import Client
from django.shortcuts import reverse
class OrganizationManagerModelPermissionView:
""" Tests for checking Organization Manager model permissions """
app_namespace: str = None
""" Application namespace of the model being tested """
different_organization_is_manager: object
""" User whom is organization Manager of different organization than object """
url_name_view: str
""" url name of the model view to be tested """
url_view_kwargs: dict = None
""" View URL kwargs for model being tested """
user_is_organization_manager: object
""" User whom is organization Manager of the object"""
def test_model_view_different_organizaiton_is_organization_manager_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization whom is an organization Manager.
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_is_manager)
response = client.get(url)
assert response.status_code == 403
def test_model_view_has_no_permission_is_organization_manager(self):
""" Confirm that an organization manager can view the model
Attempt to view as user who is an organization manager and has no permissions assigned.
Object to be within same organization the user is a manager of.
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.user_is_organization_manager)
response = client.get(url)
assert response.status_code == 200
class OrganizationManagerModelPermissionAdd:
""" Tests for checking model Add permissions """
app_namespace: str = None
""" Application namespace of the model being tested """
different_organization_is_manager: object
""" User whom is organization Manager of different organization than object """
url_name_view: str
""" url name of the model view to be tested """
url_view_kwargs: dict = None
""" View URL kwargs for model being tested """
user_is_organization_manager: object
""" User whom is organization Manager of the object"""
def test_model_add_different_organization_is_organization_manager_denied(self):
""" Check correct permission for add
attempt to add as user from different organization whom is an organization Manager.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.different_organization_is_manager)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_model_add_has_no_permission_is_organization_manager(self):
""" Check correct permission for add
Attempt to add as user who is an organization manager and has no permissions assigned.
Object to be within same organization the user is a manager of.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.user_is_organization_manager)
response = client.post(url, data=self.add_data)
assert response.status_code == 200
class OrganizationManagerModelPermissionChange:
""" Tests for checking model change permissions """
app_namespace: str = None
""" Application namespace of the model being tested """
different_organization_is_manager: object
""" User whom is organization Manager of different organization than object """
url_name_change: str
""" url name of the model view to be tested """
url_change_kwargs: dict = None
""" View URL kwargs for model being tested """
user_is_organization_manager: object
""" User whom is organization Manager of the object"""
def test_model_change_different_organization_is_organization_manager_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization whom is an organization Manager.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.different_organization_is_manager)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_has_no_permission_is_organization_manager(self):
""" Check correct permission for change
Make change as user who is an organization manager and has no permissions assigned.
Object to be within same organization the user is a manager of.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.user_is_organization_manager)
response = client.post(url, data=self.change_data)
assert response.status_code == 200
class OrganizationManagerModelPermissionDelete:
""" Tests for checking model delete permissions """
app_namespace: str = None
""" Application namespace of the model being tested """
different_organization_is_manager: object
""" User whom is organization Manager of different organization than object """
url_name_view: str
""" url name of the model view to be tested """
url_view_kwargs: dict = None
""" View URL kwargs for model being tested """
user_is_organization_manager: object
""" User whom is organization Manager of the object"""
def test_model_delete_different_organization_is_organization_manager_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization whom is an organization Manager.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.different_organization_is_manager)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_has_no_permission_is_organization_manager(self):
""" Check correct permission for delete
Delete item as user who is an organization manager and has no permissions assigned.
Object to be within same organization the user is a manager of.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.user_is_organization_manager)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 302 and response.url == self.url_delete_response
class OrganizationManagerModelPermissions(
OrganizationManagerModelPermissionView,
OrganizationManagerModelPermissionAdd,
OrganizationManagerModelPermissionChange,
OrganizationManagerModelPermissionDelete
):
""" Tests for checking Organization Manager model permissions
This class includes all test cases for: Add, Change, Delete and View.
"""
app_namespace: str = None

View File

@ -0,0 +1,88 @@
import pytest
import unittest
from access.models import TenancyManager
class TenancyObject:
""" Tests for checking TenancyObject """
model = None
""" Model to be tested """
def test_has_attr_get_organization(self):
""" TenancyObject attribute check
TenancyObject has function get_organization
"""
assert hasattr(self.model, 'get_organization')
def test_has_attr_is_global(self):
""" TenancyObject attribute check
TenancyObject has field is_global
"""
assert hasattr(self.model, 'is_global')
def test_has_attr_model_notes(self):
""" TenancyObject attribute check
TenancyObject has field model_notes
"""
assert hasattr(self.model, 'model_notes')
def test_has_attr_organization(self):
""" TenancyObject attribute check
TenancyObject has field organization
"""
assert hasattr(self.model, 'organization')
@pytest.mark.skip(reason="to be written")
def test_create_no_organization_fails(self):
""" Devices must be assigned an organization
Must not be able to create an item without an organization
"""
pass
@pytest.mark.skip(reason="to be written")
def test_edit_no_organization_fails(self):
""" Devices must be assigned an organization
Must not be able to edit an item without an organization
"""
pass
def test_has_attr_organization(self):
""" TenancyObject attribute check
TenancyObject has function objects
"""
assert hasattr(self.model, 'objects')
def test_attribute_is_type_objects(self):
""" Attribute Check
attribute `objects` must be set to `access.models.TenancyManager()`
"""
assert type(self.model.objects) is TenancyManager

View File

View File

@ -0,0 +1,371 @@
import pytest
import unittest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.relations import Hyperlink
from access.models import Organization, Team, TeamUsers, Permission
class OrganizationAPI(TestCase):
model = Organization
app_namespace = 'API'
url_name = '_api_organization'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = organization
self.url_view_kwargs = {'pk': self.item.id}
self.url_kwargs = {'pk': self.item.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
self.api_data = response.data
def test_api_field_exists_id(self):
""" Test for existance of API Field
id field must exist
"""
assert 'id' in self.api_data
def test_api_field_type_id(self):
""" Test for type for API Field
id field must be int
"""
assert type(self.api_data['id']) is int
def test_api_field_exists_name(self):
""" Test for existance of API Field
name field must exist
"""
assert 'name' in self.api_data
def test_api_field_type_name(self):
""" Test for type for API Field
name field must be str
"""
assert type(self.api_data['name']) is str
def test_api_field_exists_teams(self):
""" Test for existance of API Field
teams field must exist
"""
assert 'teams' in self.api_data
def test_api_field_type_teams(self):
""" Test for type for API Field
teams field must be list
"""
assert type(self.api_data['teams']) is list
def test_api_field_exists_url(self):
""" Test for existance of API Field
url field must exist
"""
assert 'url' in self.api_data
def test_api_field_type_url(self):
""" Test for type for API Field
url field must be str
"""
assert type(self.api_data['url']) is Hyperlink
def test_api_field_exists_teams_id(self):
""" Test for existance of API Field
teams.id field must exist
"""
assert 'id' in self.api_data['teams'][0]
def test_api_field_type_teams_id(self):
""" Test for type for API Field
teams.id field must be int
"""
assert type(self.api_data['teams'][0]['id']) is int
def test_api_field_exists_teams_team_name(self):
""" Test for existance of API Field
teams.team_name field must exist
"""
assert 'team_name' in self.api_data['teams'][0]
def test_api_field_type_teams_team_name(self):
""" Test for type for API Field
teams.team_name field must be string
"""
assert type(self.api_data['teams'][0]['team_name']) is str
def test_api_field_exists_teams_permissions(self):
""" Test for existance of API Field
teams.permissions field must exist
"""
assert 'permissions' in self.api_data['teams'][0]
def test_api_field_type_teams_permissions(self):
""" Test for type for API Field
teams.permissions field must be list
"""
assert type(self.api_data['teams'][0]['permissions']) is list
def test_api_field_exists_teams_permissions_url(self):
""" Test for existance of API Field
teams.permissions_url field must exist
"""
assert 'permissions_url' in self.api_data['teams'][0]
def test_api_field_type_teams_permissions_url(self):
""" Test for type for API Field
teams.permissions_url field must be url
"""
assert type(self.api_data['teams'][0]['permissions_url']) is str
def test_api_field_exists_teams_url(self):
""" Test for existance of API Field
teams.url field must exist
"""
assert 'url' in self.api_data['teams'][0]
def test_api_field_type_teams_url(self):
""" Test for type for API Field
teams.url field must be url
"""
assert type(self.api_data['teams'][0]['url']) is str
def test_api_field_exists_teams_permissions_id(self):
""" Test for existance of API Field
teams.permissions.id field must exist
"""
assert 'id' in self.api_data['teams'][0]['permissions'][0]
def test_api_field_type_teams_permissions_id(self):
""" Test for type for API Field
teams.permissions.id field must be int
"""
assert type(self.api_data['teams'][0]['permissions'][0]['id']) is int
def test_api_field_exists_teams_permissions_name(self):
""" Test for existance of API Field
teams.permissions.name field must exist
"""
assert 'name' in self.api_data['teams'][0]['permissions'][0]
def test_api_field_type_teams_permissions_name(self):
""" Test for type for API Field
teams.permissions.name field must be str
"""
assert type(self.api_data['teams'][0]['permissions'][0]['name']) is str
def test_api_field_exists_teams_permissions_codename(self):
""" Test for existance of API Field
teams.permissions.codename field must exist
"""
assert 'codename' in self.api_data['teams'][0]['permissions'][0]
def test_api_field_type_teams_permissions_codename(self):
""" Test for type for API Field
teams.permissions.codename field must be str
"""
assert type(self.api_data['teams'][0]['permissions'][0]['codename']) is str
def test_api_field_exists_teams_permissions_content_type(self):
""" Test for existance of API Field
teams.permissions.content_type field must exist
"""
assert 'content_type' in self.api_data['teams'][0]['permissions'][0]
def test_api_field_type_teams_permissions_content_type(self):
""" Test for type for API Field
teams.permissions.content_type field must be dict
"""
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']) is dict
def test_api_field_exists_teams_permissions_content_type_id(self):
""" Test for existance of API Field
teams.permissions.content_type.id field must exist
"""
assert 'id' in self.api_data['teams'][0]['permissions'][0]['content_type']
def test_api_field_type_teams_permissions_content_type_id(self):
""" Test for type for API Field
teams.permissions.content_type.id field must be int
"""
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['id']) is int
def test_api_field_exists_teams_permissions_content_type_app_label(self):
""" Test for existance of API Field
teams.permissions.content_type.app_label field must exist
"""
assert 'app_label' in self.api_data['teams'][0]['permissions'][0]['content_type']
def test_api_field_type_teams_permissions_content_type_app_label(self):
""" Test for type for API Field
teams.permissions.content_type.app_label field must be str
"""
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['app_label']) is str
def test_api_field_exists_teams_permissions_content_type_model(self):
""" Test for existance of API Field
teams.permissions.content_type.model field must exist
"""
assert 'model' in self.api_data['teams'][0]['permissions'][0]['content_type']
def test_api_field_type_teams_permissions_content_type_model(self):
""" Test for type for API Field
teams.permissions.content_type.model field must be str
"""
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['model']) is str

View File

@ -0,0 +1,214 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissionChange, OrganizationManagerModelPermissionView
from app.tests.abstract.model_permissions import ModelPermissionsView, ModelPermissionsChange
class OrganizationPermissions(
TestCase,
ModelPermissionsView,
ModelPermissionsChange,
OrganizationManagerModelPermissionChange,
OrganizationManagerModelPermissionView,
):
model = Organization
app_namespace = 'Access'
url_name_view = '_organization_view'
# url_name_add = '_organization_add'
url_name_change = '_organization_view'
# url_name_delete = '_organization_delete'
# url_delete_response = reverse('ITAM:Operating Systems')
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(
name='test_different_organization'
)
self.different_organization = different_organization
# self.item = self.model.objects.create(
# organization=organization,
# name = 'deviceone'
# )
self.item = organization
self.url_view_kwargs = {'pk': self.item.id}
# self.url_add_kwargs = {'pk': self.item.id}
# self.add_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
self.url_change_kwargs = {'pk': self.item.id}
self.change_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
# self.url_delete_kwargs = {'pk': self.item.id}
# self.delete_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
self.user_is_organization_manager = User.objects.create_user(
username="test_org_manager",
password="password"
)
self.organization.manager = self.user_is_organization_manager
self.organization.save()
self.different_organization_is_manager = User.objects.create_user(
username="test_org_manager_different_org",
password="password"
)
self.different_organization.manager = self.different_organization_is_manager
self.different_organization.save()

View File

@ -0,0 +1,239 @@
import pytest
import unittest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_permissions import APIPermissionChange, APIPermissionView
class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionView):
model = Organization
model_name = 'organization'
app_label = 'access'
app_namespace = 'API'
url_name = '_api_organization'
url_list = '_api_orgs'
change_data = {'name': 'device'}
# delete_data = {'device': 'device'}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = organization
self.url_view_kwargs = {'pk': self.item.id}
self.url_kwargs = {'pk': self.item.id}
# self.add_data = {'name': 'device', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.super_user = User.objects.create_user(username="super_user", password="password", is_superuser=True)
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
def test_add_is_prohibited_anon_user(self):
""" Ensure Organization cant be created
Attempt to create organization as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_list)
# client.force_login(self.add_user)
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
assert response.status_code == 401
def test_add_is_prohibited_diff_org_user(self):
""" Ensure Organization cant be created
Attempt to create organization as user with different org permissions.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
assert response.status_code == 405
def test_add_is_prohibited_super_user(self):
""" Ensure Organization cant be created
Attempt to create organization as user who is super user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.super_user)
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
assert response.status_code == 405
def test_add_is_prohibited_user_same_org(self):
""" Ensure Organization cant be created
Attempt to create organization as user with permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.add_user)
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
assert response.status_code == 405

View File

@ -0,0 +1,10 @@
from django.test import TestCase
import pytest
import requests
import unittest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from access.models import Organization, Team

View File

@ -0,0 +1,187 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from access.models import Organization
class OrganizationHistory(TestCase):
model = Organization
model_name = 'organization'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
self.item_delete = self.model.objects.create(
name = 'test_item_delete_' + self.model_name,
)
self.item_delete.delete()
self.history_delete = History.objects.filter(
item_pk = self.item_delete.pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.item_delete.pk,
item_parent_class = self.model._meta.model_name,
)
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Delete ##################################
def test_device_history_entry_delete(self):
""" When an item is deleted, it's history entries must be removed """
assert self.history_delete.exists() is False
def test_device_history_entry_children_delete(self):
""" When an item is deleted, it's history entries must be removed """
assert self.history_delete_children.exists() is False

View File

@ -0,0 +1,165 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from core.models.history import History
class OrganizationHistoryPermissions(TestCase):
item_model = Organization
model = History
model_name = 'history'
app_label = 'core'
namespace = ''
name_view = '_history'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. create an organization that is different to item
3. Create a device
4. Add history device history entry as item
5. create a user
6. create user in different organization (with the required permission)
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.organization
self.history_model_name = self.item._meta.model_name
self.history = self.model.objects.get(
item_pk = self.item.id,
item_class = self.item._meta.model_name,
action = self.model.Actions.ADD,
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
def test_auth_view_history_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_auth_view_history_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_auth_view_history_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_auth_view_history_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200

View File

@ -0,0 +1,21 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelDisplay, ModelIndex
class OrganizationViews(
TestCase,
ModelDisplay,
ModelIndex
):
display_module = 'access.views.organization'
display_view = 'View'
index_module = display_module
index_view = 'IndexView'

View File

@ -0,0 +1,70 @@
import pytest
import unittest
from django.test import TestCase, Client
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.models import TenancyModel
class TeamModel(
TestCase,
TenancyModel
):
model = Team
@classmethod
def setUpTestData(self):
""" Setup Test
"""
self.parent_item = Organization.objects.create(name='test_org')
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=self.parent_item,
name = 'teamone'
)
def test_model_has_property_parent_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert hasattr(self.model, 'parent_object')
def test_model_property_parent_object_returns_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert self.item.parent_object is self.parent_item
@pytest.mark.skip(reason="to be written")
def test_function_save_attributes():
""" Ensure save Attributes function match django default
the save method is overridden. the function attributes must match default django method
"""
pass
@pytest.mark.skip(reason="uses Django group manager")
def test_attribute_is_type_objects(self):
pass
@pytest.mark.skip(reason="uses Django group manager")
def test_model_class_tenancy_manager_function_get_queryset_called(self):
pass

View File

@ -0,0 +1,313 @@
import pytest
import unittest
import requests
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.relations import Hyperlink
from access.models import Organization, Team, TeamUsers, Permission
# from api.tests.abstract.api_permissions import APIPermissions
class TeamAPI(TestCase):
model = Team
app_namespace = 'API'
url_name = '_api_team'
# url_list = '_api_organization_teams'
# change_data = {'name': 'device'}
# delete_data = {'device': 'device'}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a team
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=organization,
team_name = 'teamone',
model_notes = 'random note'
)
self.url_kwargs = {'organization_id': self.organization.id}
self.url_view_kwargs = {'organization_id': self.organization.id, 'group_ptr_id': self.item.id}
self.add_data = {'team_name': 'team_post'}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
# view_team = Team.objects.create(
# team_name = 'view_team',
# organization = organization,
# )
self.item.permissions.set([view_permissions])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = self.item,
user = self.view_user
)
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
self.api_data = response.data
def test_api_field_exists_id(self):
""" Test for existance of API Field
id field must exist
"""
assert 'id' in self.api_data
def test_api_field_type_id(self):
""" Test for type for API Field
id field must be int
"""
assert type(self.api_data['id']) is int
def test_api_field_exists_team_name(self):
""" Test for existance of API Field
team_name field must exist
"""
assert 'team_name' in self.api_data
def test_api_field_type_name(self):
""" Test for type for API Field
team_name field must be str
"""
assert type(self.api_data['team_name']) is str
def test_api_field_exists_model_notes(self):
""" Test for existance of API Field
model_notes field must exist
"""
assert 'model_notes' in self.api_data
def test_api_field_type_model_notes(self):
""" Test for type for API Field
model_notes field must be str
"""
assert type(self.api_data['model_notes']) is str
def test_api_field_exists_url(self):
""" Test for existance of API Field
url field must exist
"""
assert 'url' in self.api_data
def test_api_field_type_url(self):
""" Test for type for API Field
url field must be str
"""
assert type(self.api_data['url']) is str
def test_api_field_exists_permissions(self):
""" Test for existance of API Field
permissions field must exist
"""
assert 'permissions' in self.api_data
def test_api_field_type_permissions(self):
""" Test for type for API Field
url field must be list
"""
assert type(self.api_data['permissions']) is list
def test_api_field_exists_permissions_id(self):
""" Test for existance of API Field
permissions.id field must exist
"""
assert 'id' in self.api_data['permissions'][0]
def test_api_field_type_permissions_id(self):
""" Test for type for API Field
permissions.id field must be int
"""
assert type(self.api_data['permissions'][0]['id']) is int
def test_api_field_exists_permissions_name(self):
""" Test for existance of API Field
permissions.name field must exist
"""
assert 'name' in self.api_data['permissions'][0]
def test_api_field_type_permissions_name(self):
""" Test for type for API Field
permissions.name field must be str
"""
assert type(self.api_data['permissions'][0]['name']) is str
def test_api_field_exists_permissions_codename(self):
""" Test for existance of API Field
permissions.codename field must exist
"""
assert 'codename' in self.api_data['permissions'][0]
def test_api_field_type_permissions_codename(self):
""" Test for type for API Field
permissions.codename field must be str
"""
assert type(self.api_data['permissions'][0]['codename']) is str
def test_api_field_exists_permissions_content_type(self):
""" Test for existance of API Field
permissions.content_type field must exist
"""
assert 'content_type' in self.api_data['permissions'][0]
def test_api_field_type_permissions_content_type(self):
""" Test for type for API Field
permissions.content_type field must be dict
"""
assert type(self.api_data['permissions'][0]['content_type']) is dict
def test_api_field_exists_permissions_content_type_id(self):
""" Test for existance of API Field
permissions.content_type.id field must exist
"""
assert 'id' in self.api_data['permissions'][0]['content_type']
def test_api_field_type_permissions_content_type_id(self):
""" Test for type for API Field
permissions.content_type.id field must be int
"""
assert type(self.api_data['permissions'][0]['content_type']['id']) is int
def test_api_field_exists_permissions_content_type_app_label(self):
""" Test for existance of API Field
permissions.content_type.app_label field must exist
"""
assert 'app_label' in self.api_data['permissions'][0]['content_type']
def test_api_field_type_permissions_content_type_app_label(self):
""" Test for type for API Field
permissions.content_type.app_label field must be str
"""
assert type(self.api_data['permissions'][0]['content_type']['app_label']) is str
def test_api_field_exists_permissions_content_type_model(self):
""" Test for existance of API Field
permissions.content_type.model field must exist
"""
assert 'model' in self.api_data['permissions'][0]['content_type']
def test_api_field_type_permissions_content_type_model(self):
""" Test for type for API Field
permissions.content_type.model field must be str
"""
assert type(self.api_data['permissions'][0]['content_type']['model']) is str

View File

@ -0,0 +1,79 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
from access.models import Team
from django.contrib.auth.models import Group
class TeamHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
model = Team
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_parent = organization
self.item_create = self.model.objects.create(
team_name = 'test_item_' + self.model._meta.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.team_name = 'test_item_' + self.model._meta.model_name + '_changed'
self.item_change.save()
self.field_after_expected_value = '{"name": "test_org_' + self.item_change.team_name + '", "team_name": "' + self.item_change.team_name + '"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
debug = Group.objects.all()
self.item_delete = self.model.objects.create(
team_name = 'test_item_delete_' + self.model._meta.model_name,
organization = self.organization
)
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
self.history_delete = History.objects.get(
action = History.Actions.DELETE[0],
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -0,0 +1,168 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from core.models.history import History
class TeamHistoryPermissions(TestCase):
item_model = Team
model = History
model_name = 'history'
app_label = 'core'
namespace = ''
name_view = '_history'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. create an organization that is different to item
3. Create a device
4. Add history device history entry as item
5. create a user
6. create user in different organization (with the required permission)
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.item_model.objects.create(
organization=organization,
name = 'deviceone'
)
self.history_model_name = self.item._meta.model_name
self.history = self.model.objects.get(
item_pk = self.item.id,
item_class = self.item._meta.model_name,
action = self.model.Actions.ADD,
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
def test_auth_view_history_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_auth_view_history_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_auth_view_history_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_auth_view_history_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200

View File

@ -0,0 +1,210 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissions
from app.tests.abstract.model_permissions import ModelPermissions
class TeamPermissions(
TestCase,
ModelPermissions,
OrganizationManagerModelPermissions,
):
model = Team
app_namespace = 'Access'
url_name_view = '_team_view'
url_name_add = '_team_add'
url_name_change = '_team_view'
url_name_delete = '_team_delete'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a team
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.different_organization = different_organization
self.item = self.model.objects.create(
organization=organization,
name = 'teamone'
)
self.url_view_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.url_add_kwargs = {'pk': self.organization.id}
self.add_data = {'team': 'team'}
self.url_change_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.change_data = {'team': 'team'}
self.url_delete_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.delete_data = {'team': 'team'}
self.url_delete_response = reverse('Access:_organization_view', kwargs={'pk': self.organization.id})
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
self.user_is_organization_manager = User.objects.create_user(
username="test_org_manager",
password="password"
)
self.organization.manager = self.user_is_organization_manager
self.organization.save()
self.different_organization_is_manager = User.objects.create_user(
username="test_org_manager_different_org",
password="password"
)
self.different_organization.manager = self.different_organization_is_manager
self.different_organization.save()

View File

@ -0,0 +1,175 @@
import pytest
import unittest
import requests
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_permissions import APIPermissions
class TeamPermissionsAPI(TestCase, APIPermissions):
model = Team
app_namespace = 'API'
url_name = '_api_team'
url_list = '_api_organization_teams'
change_data = {'name': 'device'}
delete_data = {'device': 'device'}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a team
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=organization,
name = 'teamone'
)
self.url_kwargs = {'organization_id': self.organization.id}
self.url_view_kwargs = {'organization_id': self.organization.id, 'group_ptr_id': self.item.id}
self.add_data = {'team_name': 'team_post'}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)

View File

@ -0,0 +1,29 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import ModelAdd, ModelDelete, ModelDisplay
class TeamViews(
TestCase,
ModelAdd,
ModelDelete,
ModelDisplay,
):
add_module = 'access.views.team'
add_view = 'Add'
# change_module = add_module
# change_view = 'Change'
delete_module = add_module
delete_view = 'Delete'
display_module = add_module
display_view = 'View'

View File

@ -0,0 +1,56 @@
import pytest
import unittest
from django.test import TestCase, Client
from django.contrib.auth.models import User
from access.models import Organization, Team, TeamUsers, Permission
class TeamUsersModel(TestCase):
model = TeamUsers
@classmethod
def setUpTestData(self):
""" Setup Test
"""
organization = Organization.objects.create(name='test_org')
different_organization = Organization.objects.create(name='test_different_organization')
self.parent_item = Team.objects.create(
team_name = 'test_team',
organization = organization,
)
team_user = User.objects.create_user(username="test_self.team_user", password="password")
self.item = self.model.objects.create(
team = self.parent_item,
user = team_user
)
def test_model_has_property_parent_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert hasattr(self.model, 'parent_object')
def test_model_property_parent_object_returns_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert self.item.parent_object == self.parent_item

View File

@ -0,0 +1,92 @@
import pytest
import unittest
import requests
from django.contrib.auth.models import User
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
from access.models import Team, TeamUsers
class TeamUsersHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
model = TeamUsers
model_name = 'teamusers'
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_parent = Team.objects.create(
team_name = 'test_item_' + self.model._meta.model_name,
organization = self.organization
)
self.user = User.objects.create(
username = 'test_item_' + self.model._meta.model_name,
password = 'a random password'
)
self.item_create = self.model.objects.create(
user = self.user,
team = self.item_parent
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.manager = True
self.item_change.save()
self.field_after_expected_value = '{"manager": true}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
self.user_delete = User.objects.create(
username = 'test_item_delete' + self.model._meta.model_name,
password = 'a random password'
)
self.item_delete = self.model.objects.create(
user = self.user_delete,
team = self.item_parent
)
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
self.history_delete = History.objects.get(
action = History.Actions.DELETE[0],
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -0,0 +1,48 @@
from django.test import TestCase
import pytest
import requests
import unittest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from access.models import Organization, Team
# @pytest.fixture
# def organization() -> Organization:
# return Organization.objects.create(
# name='Test org',
# )
# @pytest.fixture
# def team() -> Team:
# return Team.objects.create(
# name='Team one',
# organization = Organization.objects.create(
# name='Test org',
# ),
# )
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_add_team_manager(user):
"""Ensure user can be added when user is team manager
user requires permissions team view and user add
"""
pass
@pytest.mark.skip(reason="to be written")
def test_authorization_user_permission_delete_team_manager(user):
"""Ensure user can be deleted when user is team manager
user requires permissions team view and user delete
"""
pass
# is_superuser to be able to view, add, change, delete for all objects

View File

@ -0,0 +1,325 @@
# from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissionAdd, OrganizationManagerModelPermissionDelete
from app.tests.abstract.model_permissions import ModelPermissionsAdd, ModelPermissionsChange, ModelPermissionsDelete
class TeamUserPermissions(
TestCase,
ModelPermissionsAdd,
ModelPermissionsDelete,
OrganizationManagerModelPermissionAdd,
OrganizationManagerModelPermissionDelete
):
model = TeamUsers
app_namespace = 'Access'
url_name_view = '_team_user_view'
url_name_add = '_team_user_add'
url_name_change = '_team_user_view'
url_name_delete = '_team_user_delete'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.different_organization = different_organization
self.test_team = Team.objects.create(
team_name = 'test_team',
organization = organization,
)
self.team_user = User.objects.create_user(username="test_self.team_user", password="password")
self.item = self.model.objects.create(
team = self.test_team,
user = self.team_user
)
self.url_view_kwargs = {'pk': self.item.id}
self.url_add_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.add_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
self.url_change_kwargs = {'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id}
self.change_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
self.url_delete_kwargs = {'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id}
self.delete_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
self.url_delete_response = reverse('Access:_team_view',
kwargs={
'organization_id': self.organization.id,
'pk': self.test_team.id
}
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
self.user_is_organization_manager = User.objects.create_user(
username="test_org_manager",
password="password"
)
self.organization.manager = self.user_is_organization_manager
self.organization.save()
self.different_organization_is_manager = User.objects.create_user(
username="test_org_manager_different_org",
password="password"
)
self.different_organization.manager = self.different_organization_is_manager
self.different_organization.save()
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 200

View File

@ -0,0 +1,30 @@
import pytest
import unittest
import requests
from django.test import TestCase
from app.tests.abstract.models import AddView, DeleteView
class TeamUserViews(
TestCase,
AddView,
DeleteView
):
add_module = 'access.views.user'
add_view = 'Add'
# change_module = add_module
# change_view = 'GroupView'
delete_module = add_module
delete_view = 'Delete'
# display_module = add_module
# display_view = 'GroupView'
# index_module = add_module
# index_view = 'GroupIndexView'

View File

@ -0,0 +1,93 @@
import pytest
import unittest
from django.test import TestCase
from access.models import TenancyObject, TenancyManager
from core.mixin.history_save import SaveHistory
from unittest.mock import patch
class TenancyManagerTests(TestCase):
item = TenancyManager
def test_has_attribute_get_queryset(self):
""" Field organization exists """
assert hasattr(self.item, 'get_queryset')
def test_is_function_get_queryset(self):
""" Attribute 'get_organization' is a function """
assert callable(self.item.get_queryset)
class TenancyObjectTests(TestCase):
item = TenancyObject
def test_class_inherits_save_history(self):
""" Confirm class inheritence
TenancyObject must inherit SaveHistory
"""
assert issubclass(TenancyObject, SaveHistory)
def test_has_attribute_organization(self):
""" Field organization exists """
assert hasattr(self.item, 'organization')
def test_has_attribute_is_global(self):
""" Field organization exists """
assert hasattr(self.item, 'is_global')
def test_has_attribute_model_notes(self):
""" Field organization exists """
assert hasattr(self.item, 'model_notes')
def test_has_attribute_get_organization(self):
""" Function 'get_organization' Exists """
assert hasattr(self.item, 'get_organization')
def test_is_function_get_organization(self):
""" Attribute 'get_organization' is a function """
assert callable(self.item.get_organization)
@pytest.mark.skip(reason="figure out how to test abstract class")
def test_has_attribute_objects(self):
""" Attribute Check
attribute `objects` must be set to `access.models.TenancyManager()`
"""
assert 'objects' in self.item
@pytest.mark.skip(reason="figure out how to test abstract class")
def test_attribute_not_none_objects(self):
""" Attribute Check
attribute `objects` must be set to `access.models.TenancyManager()`
"""
assert self.item.objects is not None

16
app/access/urls.py Normal file
View File

@ -0,0 +1,16 @@
from django.urls import path
from . import views
from .views import team, organization, user
app_name = "Access"
urlpatterns = [
path("", organization.IndexView.as_view(), name="Organizations"),
path("<int:pk>/", organization.View.as_view(), name="_organization_view"),
# path("<int:pk>/edit", organization.Change.as_view(), name="_organization_change"),
path("<int:organization_id>/team/<int:pk>/", team.View.as_view(), name="_team_view"),
path("<int:pk>/team/add", team.Add.as_view(), name="_team_add"),
path("<int:organization_id>/team/<int:pk>/delete", team.Delete.as_view(), name="_team_delete"),
path("<int:organization_id>/team/<int:pk>/user/add", user.Add.as_view(), name="_team_user_add"),
path("<int:organization_id>/team/<int:team_id>/user/<int:pk>/delete", user.Delete.as_view(), name="_team_user_delete"),
]

View File

@ -0,0 +1,123 @@
from django.contrib.auth import decorators as auth_decorator
from django.db.models import Q
from django.utils.decorators import method_decorator
from django.views import generic
from access.mixin import *
from access.models import *
from access.forms.organization import OrganizationForm
from core.views.common import ChangeView, IndexView
class IndexView(IndexView):
model = Organization
permission_required = [
'access.view_organization'
]
template_name = 'access/index.html.j2'
context_object_name = "organization_list"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Organizations'
return context
def get_queryset(self):
if self.request.user.is_superuser:
return Organization.objects.filter()
else:
return Organization.objects.filter(
Q(pk__in=self.user_organizations())
|
Q(manager=self.request.user.id)
)
class View(ChangeView):
context_object_name = "organization"
form_class = OrganizationForm
model = Organization
template_name = "access/organization.html.j2"
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.view_organization' ]):
raise PermissionDenied('You are not part of this organization')
return super().get(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['pk']}/"
def get_queryset(self):
return Organization.objects.filter(pk=self.kwargs['pk'])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name + '/'
context['teams'] = Team.objects.filter(organization=self.kwargs['pk'])
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['content_title'] = 'Organization - ' + context[self.context_object_name].name
return context
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.change_organization' ]):
raise PermissionDenied('You are not part of this organization')
return super().post(request, *args, **kwargs)
class Change(OrganizationPermission, generic.DetailView):
pass
class Delete(OrganizationPermission, generic.DetailView):
pass

153
app/access/views/team.py Normal file
View File

@ -0,0 +1,153 @@
from django.contrib.auth import decorators as auth_decorator
from django.contrib.auth.models import Permission
from django.utils.decorators import method_decorator
from django.urls import reverse
from access.forms.team import TeamForm, TeamFormAdd
from access.models import Team, TeamUsers, Organization
from access.mixin import *
from core.views.common import AddView, ChangeView, DeleteView
class View(ChangeView):
context_object_name = "team"
form_class = TeamForm
model = Team
permission_required = [
'access.view_team',
'access.change_team',
]
template_name = 'access/team.html.j2'
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.view_team' ]):
raise PermissionDenied('You are not part of this organization')
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name + '/'
organization = Organization.objects.get(pk=self.kwargs['organization_id'])
context['organization'] = organization
team = Team.objects.get(pk=self.kwargs['pk'])
teamusers = TeamUsers.objects.filter(team=self.kwargs['pk'])
context['teamusers'] = teamusers
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
return context
def get_success_url(self, **kwargs):
return reverse('Access:_team_view', args=(self.kwargs['organization_id'], self.kwargs['pk'],))
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.change_team' ]):
raise PermissionDenied('You are not part of this organization')
return super().post(request, *args, **kwargs)
class Add(AddView):
form_class = TeamFormAdd
model = Team
parent_model = Organization
permission_required = [
'access.add_team',
]
template_name = 'form.html.j2'
def form_valid(self, form):
form.instance.organization = Organization.objects.get(pk=self.kwargs['pk'])
return super().form_valid(form)
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['pk']}/"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Team'
return context
class Delete(DeleteView):
model = Team
permission_required = [
'access.delete_team'
]
template_name = 'form.html.j2'
fields = [
'team_name',
'permissions',
'organization'
]
def get_success_url(self, **kwargs):
return f"/organization/{self.kwargs['organization_id']}/"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
context['content_title'] = 'Delete Team'
return context

75
app/access/views/user.py Normal file
View File

@ -0,0 +1,75 @@
from django.contrib.auth import decorators as auth_decorator
from django.urls import reverse
from access.forms.team_users import TeamUsersForm
from access.models import Team, TeamUsers
from core.views.common import AddView, DeleteView
class Add(AddView):
context_object_name = "teamuser"
form_class = TeamUsersForm
model = TeamUsers
parent_model = Team
permission_required = [
'access.add_teamusers'
]
template_name = 'form.html.j2'
def form_valid(self, form):
team = Team.objects.get(pk=self.kwargs['pk'])
form.instance.team = team
return super().form_valid(form)
def get_success_url(self, **kwargs):
return reverse('Access:_team_view',
kwargs={
'organization_id': self.kwargs['organization_id'],
'pk': self.kwargs['pk']
}
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Team User'
return context
class Delete(DeleteView):
model = TeamUsers
permission_required = [
'access.delete_teamusers'
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse('Access:_team_view',
kwargs={
'organization_id': self.kwargs['organization_id'],
'pk': self.kwargs['team_id']
}
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete Team User'
return context

0
app/api/__init__.py Normal file
View File

6
app/api/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

79
app/api/auth.py Normal file
View File

@ -0,0 +1,79 @@
import datetime
from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from api.models.tokens import AuthToken
class TokenAuthentication(BaseAuthentication):
""" API Token Authentication
Provides the ability to use the API by using a token to authenticate.
"""
def authenticate_header(self, request):
return 'Token'
def authenticate(self, request):
""" Authentication the API session using the supplied token
Args:
request (object): API Request Object
Raises:
exceptions.AuthenticationFailed: 'Token header invalid' - Authorization Header Value is not in format `Token <auth-token>`
exceptions.AuthenticationFailed: 'Token header invalid. Possibly incorrectly formatted' - Authentication header value has >1 space
exceptions.AuthenticationFailed: 'Invalid token header. Token string should not contain invalid characters.' - Authorization header contains non-unicode chars
Returns:
None (None): User not authenticated
tuple(user,token): User authenticated
"""
auth = get_authorization_header(request).split()
if not auth:
return None
if len(auth) == 1:
raise exceptions.AuthenticationFailed('Token header invalid')
elif len(auth) > 2:
raise exceptions.AuthenticationFailed('Token header invalid. Possibly incorrectly formatted')
elif len(auth) == 2:
try:
decoded_token: str = auth[1].decode("utf-8")
for token in AuthToken.objects.filter():
provided_token: str = token.token_hash(decoded_token)
if token.token == provided_token:
if datetime.datetime.strptime(str(token.expires),'%Y-%m-%d %H:%M:%S%z') > datetime.datetime.now(datetime.timezone.utc):
user = token.user
return (user, provided_token)
else:
expired_token = AuthToken.objects.get(id=token.id)
expired_token.delete()
except UnicodeError:
raise exceptions.AuthenticationFailed('Invalid token header. Token string should not contain invalid characters.')
return None

View File

@ -0,0 +1,49 @@
import datetime
from django import forms
from api.models.tokens import AuthToken
from app import settings
from core.forms.common import CommonModelForm
class AuthTokenForm(CommonModelForm):
prefix = 'user_token'
class Meta:
fields = [
'note',
'expires',
]
model = AuthToken
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['expires'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
self.fields['expires'].input_formats = settings.DATETIME_FORMAT
self.fields['expires'].format="%Y-%m-%dT%H:%M"
self.fields['expires'].initial= datetime.datetime.now() + datetime.timedelta(days=90)
if self.prefix + '-gen_token' not in self.data:
generated_token = self.instance.generate()
else:
generated_token = self.data[self.prefix + '-gen_token']
self.fields['gen_token'] = forms.CharField(
label="Generated Token",
initial=generated_token,
empty_value= generated_token,
required=False,
help_text = 'Ensure you save this token somewhere as you will never be able to obtain it again',
)
self.fields['gen_token'].widget.attrs['readonly'] = True

View File

@ -0,0 +1,31 @@
# Generated by Django 5.0.7 on 2024-07-12 03:54
import access.fields
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AuthToken',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('note', models.CharField(blank=True, default=None, max_length=50, null=True)),
('token', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='Auth Token')),
('expires', models.DateTimeField(verbose_name='Expiry Date')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

View File

109
app/api/models/tokens.py Normal file
View File

@ -0,0 +1,109 @@
import hashlib
import random
import string
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Field
from django.forms import ValidationError
from access.fields import *
from access.models import TenancyObject
class AuthToken(models.Model):
def validate_note_no_token(self, note, token):
""" Ensure plaintext token cant be saved to notes field.
called from app.settings.views.user_settings.TokenAdd.form_valid()
Args:
note (Field): _Note field_
token (Field): _Token field_
Raises:
ValidationError: _Validation failed_
"""
validation: bool = True
if str(note) == str(token):
validation = False
if str(token)[:9] in str(note): # Allow user to use up to 8 chars so they can reference it.
validation = False
if not validation:
raise ValidationError('Token can not be placed in the notes field.')
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
note = models.CharField(
blank = True,
max_length = 50,
default = None,
null= True,
)
token = models.CharField(
verbose_name = 'Auth Token',
db_index=True,
max_length = 64,
null = False,
blank = False,
unique = True,
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
expires = models.DateTimeField(
verbose_name = 'Expiry Date',
null = False,
blank = False
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def generate(self) -> str:
return str(hashlib.sha256(str(self.randomword()).encode('utf-8')).hexdigest())
def token_hash(self, token:str) -> str:
salt = settings.SECRET_KEY
return str(hashlib.sha256(str(token + salt).encode('utf-8')).hexdigest())
def randomword(self) -> str:
return ''.join(random.choice(string.ascii_letters) for i in range(120))
def __str__(self):
return self.token

View File

View File

@ -0,0 +1,139 @@
from rest_framework import serializers, request
from rest_framework.reverse import reverse
from access.models import Organization, Team
from django.contrib.auth.models import Permission
class TeamSerializerBase(serializers.ModelSerializer):
url = serializers.SerializerMethodField('get_url')
class Meta:
model = Team
fields = (
'team_name',
'model_notes',
'permissions',
'url',
)
def get_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_team", args=[obj.organization.id,obj.pk]))
class TeamPermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
depth = 1
fields = '__all__'
class TeamSerializer(TeamSerializerBase):
permissions_url = serializers.SerializerMethodField('get_url')
def get_url(self, obj):
request = self.context.get('request')
team = Team.objects.get(pk=obj.id)
return request.build_absolute_uri(reverse('API:_api_team_permission', args=[team.organization_id,team.id]))
def validate(self, data):
"""
Check that start is before finish.
"""
data['organization_id'] = self._context['view'].kwargs['organization_id']
return data
url = serializers.SerializerMethodField('team_url')
def team_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse('API:_api_team', args=[obj.organization_id,obj.id]))
class Meta:
model = Team
depth = 2
fields = (
"id",
"team_name",
'model_notes',
'permissions',
'permissions_url',
'url',
)
read_only_fields = [
'id',
'organization',
'permissions_url',
'url'
]
class OrganizationListSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_organization", format="html"
)
class Meta:
model = Organization
fields = (
"id",
"name",
'url',
)
class OrganizationSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_organization", format="html"
)
team_url = serializers.SerializerMethodField('get_url')
def get_url(self, obj):
request = self.context.get('request')
team = Team.objects.filter(pk=obj.id)
return request.build_absolute_uri(reverse('API:_api_organization_teams', args=[obj.id]))
teams = TeamSerializer(source='team_set', many=True, read_only=False)
view_name="API:_api_organization"
class Meta:
model = Organization
fields = (
"id",
"name",
'teams',
'url',
'team_url',
)

View File

@ -0,0 +1,86 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
from config_management.models.groups import ConfigGroups
class ParentGroupSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField('get_url')
class Meta:
model = ConfigGroups
fields = [
'id',
'name',
'url',
]
read_only_fields = [
'id',
'name',
'url',
]
def get_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
class ConfigGroupsSerializerBase(serializers.ModelSerializer):
parent = ParentGroupSerializer(read_only=True)
url = serializers.SerializerMethodField('get_url')
class Meta:
model = ConfigGroups
fields = [
'id',
'parent',
'name',
'config',
'url',
]
read_only_fields = [
'id',
'name',
'config',
'url',
]
def get_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
class ConfigGroupsSerializer(ConfigGroupsSerializerBase):
class Meta:
model = ConfigGroups
depth = 1
fields = [
'id',
'parent',
'name',
'config',
'url',
]
read_only_fields = [
'id',
'parent',
'name',
'config',
'url',
]

View File

@ -0,0 +1,168 @@
from django.core.exceptions import ValidationError
from django.utils.html import escape
class Inventory:
""" Inventory Object
Pass in an Inventory dict that a device has provided and sanitize ready for use.
Raises:
ValidationError: Malformed inventory data.
"""
class Details:
_name: str
_serial_number: str
_uuid: str
def __init__(self, details: dict):
self._name = escape(details['name'])
self._serial_number = escape(details['serial_number'])
self._uuid = escape(details['uuid'])
@property
def name(self) -> str:
return str(self._name)
@property
def serial_number(self) -> str:
return str(self._serial_number)
@property
def uuid(self) -> str:
return str(self._uuid)
class OperatingSystem:
_name: str
_version_major: str
_version: str
def __init__(self, operating_system: dict):
self._name = escape(operating_system['name'])
self._version_major = escape(operating_system['version_major'])
self._version = escape(operating_system['version'])
@property
def name(self) -> str:
return str(self._name)
@property
def version_major(self) -> str:
return str(self._version_major)
@property
def version(self) -> str:
return str(self._version)
class Software:
_name: str
_category: str
_version: str
def __init__(self, software: dict):
self._name = escape(software['name'])
self._category = escape(software['category'])
self._version = escape(software['version'])
@property
def name(self) -> str:
return str(self._name)
@property
def category(self) -> str:
return str(self._category)
@property
def version(self) -> str:
return str(self._version)
_details: Details = None
_operating_system: OperatingSystem = None
_software: list[Software] = []
def __init__(self, inventory: dict):
if (
type(inventory['details']) is dict and
type(inventory['os']) is dict and
type(inventory['software']) is list
):
self._details = self.Details(inventory['details'])
self._operating_system = self.OperatingSystem(inventory['os'])
for software in inventory['software']:
self._software += [ self.Software(software) ]
else:
raise ValidationError('Inventory File is invalid')
@property
def details(self) -> Details:
return self._details
@property
def operating_system(self) -> OperatingSystem:
return self._operating_system
@property
def software(self) -> list[Software]:
return list(self._software)

View File

@ -0,0 +1,81 @@
from django.urls import reverse
from rest_framework import serializers
from api.serializers.config import ParentGroupSerializer
from config_management.models.groups import ConfigGroupHosts
from itam.models.device import Device
class DeviceConfigGroupsSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='group.name', read_only=True)
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_config_group", format="html"
)
class Meta:
model = ConfigGroupHosts
fields = [
'id',
'name',
'url',
]
read_only_fields = [
'id',
'name',
'url',
]
class DeviceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="API:device-detail", format="html"
)
config = serializers.SerializerMethodField('get_device_config')
groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True)
def get_device_config(self, device):
request = self.context.get('request')
return request.build_absolute_uri(reverse('API:_api_device_config', args=[device.slug]))
class Meta:
model = Device
depth = 1
fields = [
'id',
'is_global',
'name',
'config',
'serial_number',
'uuid',
'inventorydate',
'created',
'modified',
'groups',
'organization',
'url',
]
read_only_fields = [
'id',
'config',
'inventorydate',
'created',
'modified',
'groups',
'url',
]

View File

@ -0,0 +1,73 @@
from django.urls import reverse
from itam.models.device import Device
from rest_framework import serializers
class InventorySerializer(serializers.Serializer):
""" Serializer for Inventory Upload """
class DetailsSerializer(serializers.Serializer):
name = serializers.CharField(
help_text = 'Host name',
required = True
)
serial_number = serializers.CharField(
help_text = 'Devices serial number',
required = True
)
uuid = serializers.CharField(
help_text = 'Device system UUID',
required = True
)
class OperatingSystemSerializer(serializers.Serializer):
name = serializers.CharField(
help_text='Name of the operating system installed on the device',
required = True,
)
version_major = serializers.IntegerField(
help_text='Major semver version number of the OS version',
required = True,
)
version = serializers.CharField(
help_text='semver version number of the OS',
required = True
)
class SoftwareSerializer(serializers.Serializer):
name = serializers.CharField(
help_text='Name of the software',
required = True
)
category = serializers.CharField(
help_text='Category of the software',
default = None,
required = False
)
version = serializers.CharField(
default = None,
help_text='semver version number of the software',
required = False
)
details = DetailsSerializer()
os = OperatingSystemSerializer()
software = SoftwareSerializer(many = True)

View File

@ -0,0 +1,19 @@
from rest_framework import serializers
from itam.models.device import Software
class SoftwareSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="API:software-detail", format="html"
)
class Meta:
model = Software
fields = '__all__'
read_only_fields = [
'slug',
]

446
app/api/tasks.py Normal file
View File

@ -0,0 +1,446 @@
import json
import re
from django.utils import timezone
from celery import shared_task, current_task
from celery.utils.log import get_task_logger
from celery import states
from access.models import Organization
from api.serializers.inventory import Inventory
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
from settings.models.app_settings import AppSettings
logger = get_task_logger(__name__)
@shared_task(bind=True)
def process_inventory(self, data, organization: int):
device = None
device_operating_system = None
operating_system = None
operating_system_version = None
try:
logger.info('Begin Processing Inventory')
data = json.loads(data)
data = Inventory(data)
organization = Organization.objects.get(id=organization)
app_settings = AppSettings.objects.get(owner_organization = None)
device_serial_number = None
device_uuid = None
if data.details.serial_number and str(data.details.serial_number).lower() != 'na':
device_serial_number = str(data.details.serial_number)
if data.details.uuid and str(data.details.uuid).lower() != 'na':
device_uuid = str(data.details.uuid)
if device_serial_number: # Search for device by serial number.
device = Device.objects.filter(
serial_number__iexact=device_serial_number
)
if device.exists():
device = Device.objects.get(
serial_number__iexact=device_serial_number
)
else:
device = None
if device_uuid and not device: # Search for device by UUID.
device = Device.objects.filter(
uuid__iexact=device_uuid
)
if device.exists():
device = Device.objects.get(
uuid__iexact=device_uuid
)
else:
device = None
if not device: # Search for device by Name.
device = Device.objects.filter(
name__iexact=str(data.details.name).lower()
)
if device.exists():
device = Device.objects.get(
name__iexact=str(data.details.name).lower()
)
else:
device = None
if not device: # Create the device
device = Device.objects.create(
name = data.details.name,
device_type = None,
serial_number = device_serial_number,
uuid = device_uuid,
organization = organization,
)
if device:
logger.info(f"Device: {device.name}, Serial: {device.serial_number}, UUID: {device.uuid}")
device_edited = False
if not device.uuid and device_uuid:
device.uuid = device_uuid
device_edited = True
if not device.serial_number and device_serial_number:
device.serial_number = data.details.serial_number
device_edited = True
if str(device.name).lower() != str(data.details.name).lower(): # Update device Name
device.name = data.details.name
device_edited = True
if device_edited:
device.save()
operating_system = OperatingSystem.objects.filter(
name=data.operating_system.name,
is_global = True
)
if operating_system.exists():
operating_system = OperatingSystem.objects.get(
name=data.operating_system.name,
is_global = True
)
else:
operating_system = None
if not operating_system:
operating_system = OperatingSystem.objects.filter(
name=data.operating_system.name,
organization = organization
)
if operating_system.exists():
operating_system = OperatingSystem.objects.get(
name=data.operating_system.name,
organization = organization
)
else:
operating_system = None
if not operating_system:
operating_system = OperatingSystem.objects.create(
name = data.operating_system.name,
organization = organization,
is_global = True
)
operating_system_version = OperatingSystemVersion.objects.filter(
name=data.operating_system.version_major,
operating_system=operating_system
)
if operating_system_version.exists():
operating_system_version = OperatingSystemVersion.objects.get(
name=data.operating_system.version_major,
operating_system=operating_system
)
else:
operating_system_version = None
if not operating_system_version:
operating_system_version = OperatingSystemVersion.objects.create(
organization = organization,
is_global = True,
name = data.operating_system.version_major,
operating_system = operating_system,
)
device_operating_system = DeviceOperatingSystem.objects.filter(
device=device,
)
if device_operating_system.exists():
device_operating_system = DeviceOperatingSystem.objects.get(
device=device,
)
else:
device_operating_system = None
if not device_operating_system:
device_operating_system = DeviceOperatingSystem.objects.create(
organization = organization,
device=device,
version = data.operating_system.version,
operating_system_version = operating_system_version,
installdate = timezone.now()
)
if not device_operating_system.installdate: # Only update install date if empty
device_operating_system.installdate = timezone.now()
device_operating_system.save()
if device_operating_system.operating_system_version != operating_system_version:
device_operating_system.operating_system_version = operating_system_version
device_operating_system.save()
if device_operating_system.version != data.operating_system.version:
device_operating_system.version = data.operating_system.version
device_operating_system.save()
if app_settings.software_is_global:
software_organization = app_settings.global_organization
else:
software_organization = device.organization
if app_settings.software_categories_is_global:
software_category_organization = app_settings.global_organization
else:
software_category_organization = device.organization
inventoried_software: list = []
for inventory in list(data.software):
software = None
software_category = None
software_version = None
device_software = None
software_category = SoftwareCategory.objects.filter( name = inventory.category )
if software_category.exists():
software_category = SoftwareCategory.objects.get(
name = inventory.category
)
else: # Create Software Category
software_category = SoftwareCategory.objects.create(
organization = software_category_organization,
is_global = True,
name = inventory.category,
)
if software_category.name == inventory.category:
if Software.objects.filter( name = inventory.name ).exists():
software = Software.objects.get(
name = inventory.name
)
if not software.category:
software.category = software_category
software.save()
else: # Create Software
software = Software.objects.create(
organization = software_organization,
is_global = True,
name = inventory.name,
category = software_category,
)
if software.name == inventory.name:
pattern = r"^(\d+:)?(?P<semver>\d+\.\d+(\.\d+)?)"
semver = re.search(pattern, str(inventory.version), re.DOTALL)
if semver:
semver = semver['semver']
else:
semver = inventory.version
if SoftwareVersion.objects.filter( name = semver, software = software ).exists():
software_version = SoftwareVersion.objects.get(
name = semver,
software = software,
)
else: # Create Software Category
software_version = SoftwareVersion.objects.create(
organization = organization,
is_global = True,
name = semver,
software = software,
)
if software_version.name == semver:
if DeviceSoftware.objects.filter( software = software, device=device ).exists():
device_software = DeviceSoftware.objects.get(
device = device,
software = software
)
logger.debug(f"Select Existing Device Software: {device_software.software.name}")
else: # Create Software
device_software = DeviceSoftware.objects.create(
organization = organization,
is_global = True,
installedversion = software_version,
software = software,
device = device,
action=None
)
logger.debug(f"Create Device Software: {device_software.software.name}")
if device_software: # Update the Inventoried software
inventoried_software += [ device_software.id ]
if not device_software.installed: # Only update install date if blank
device_software.installed = timezone.now()
device_software.save()
logger.debug(f"Update Device Software (installed): {device_software.software.name}")
if device_software.installedversion.name != software_version.name:
device_software.installedversion = software_version
device_software.save()
logger.debug(f"Update Device Software (installedversion): {device_software.software.name}")
for not_installed in DeviceSoftware.objects.filter( device=device ):
if not_installed.id not in inventoried_software:
not_installed.delete()
logger.debug(f"Remove Device Software: {not_installed.software.name}")
if device and operating_system and operating_system_version and device_operating_system:
device.inventorydate = timezone.now()
device.save()
logger.info('Finish Processing Inventory')
return str('finished...')
except Exception as e:
logger.critical('Exception')
raise Exception(e)
return str(f'Exception Occured: {e}')

View File

View File

View File

@ -0,0 +1,470 @@
import pytest
import unittest
from django.shortcuts import reverse
from django.test import TestCase, Client
class APIPermissionView:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
def test_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
response = client.get(url)
assert response.status_code == 401
def test_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
class APIPermissionAdd:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_list: str
""" URL view name of the item list page """
url_kwargs: dict = None
""" URL view kwargs for the item list page """
add_data: dict = None
def test_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
response = client.put(url, data=self.add_data)
assert response.status_code == 401
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.no_permissions_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.different_organization_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.view_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.add_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 201
class APIPermissionChange:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
change_data: dict = None
def test_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 401
def test_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.add_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.change_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 200
class APIPermissionDelete:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
delete_data: dict = None
def test_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 401
def test_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.add_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.change_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.delete_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 204
class APIPermissions(
APIPermissionAdd,
APIPermissionChange,
APIPermissionDelete,
APIPermissionView
):
""" Abstract class containing all API Permission test cases """
model: object
""" Item Model to test """

View File

View File

@ -0,0 +1,989 @@
import datetime
import json
import pytest
import unittest
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
from django.test.utils import override_settings
from unittest.mock import patch
from access.models import Organization, Team, TeamUsers, Permission
from api.views.mixin import OrganizationPermissionAPI
from api.serializers.inventory import Inventory
from api.tasks import process_inventory
from itam.models.device import Device, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
from settings.models.user_settings import UserSettings
class InventoryAPI(TestCase):
model = Device
model_name = 'device'
app_label = 'itam'
inventory = {
"details": {
"name": "device_name",
"serial_number": "a serial number",
"uuid": "string"
},
"os": {
"name": "os_name",
"version_major": "12",
"version": "12.1"
},
"software": [
{
"name": "software_name",
"category": "category_name",
"version": "1.2.3"
},
{
"name": "software_name_not_semver",
"category": "category_name",
"version": "2024.4"
},
{
"name": "software_name_semver_contained",
"category": "category_name",
"version": "1.2.3-rc1"
},
]
}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
2. Create a team for user with correct permissions
3. add user to the teeam
4. upload the inventory
5. conduct queries for tests
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
self.add_user = User.objects.create_user(username="test_user_add", password="password")
add_user_settings = UserSettings.objects.get(user=self.add_user)
add_user_settings.default_organization = organization
add_user_settings.save()
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
# upload the inventory
process_inventory(json.dumps(self.inventory), organization.id)
self.device = Device.objects.get(name=self.inventory['details']['name'])
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
self.software_version = SoftwareVersion.objects.get(
name = self.inventory['software'][0]['version'],
software = self.software,
)
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
self.software_version_not_semver = SoftwareVersion.objects.get(
name = self.inventory['software'][1]['version'],
software = self.software_not_semver
)
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
self.software_version_is_semver = SoftwareVersion.objects.get(
software = self.software_is_semver
)
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(OrganizationPermissionAPI, 'permission_check')
def test_inventory_function_called_permission_check(self, permission_check):
""" Inventory Upload checks permissions
Function 'permission_check' is the function that checks permissions
As the non-established way of authentication an API permission is being done
confimation that the permissions are still checked is required.
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert permission_check.called
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory, '__init__')
def test_inventory_serializer_inventory_called(self, serializer):
""" Inventory Upload checks permissions
Function 'permission_check' is the function that checks permissions
As the non-established way of authentication an API permission is being done
confimation that the permissions are still checked is required.
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert serializer.called
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.Details, '__init__')
def test_inventory_serializer_inventory_details_called(self, serializer):
""" Inventory Upload uses Inventory serializer
Details Serializer is called for inventory details dict.
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert serializer.called
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.OperatingSystem, '__init__')
def test_inventory_serializer_inventory_operating_system_called(self, serializer):
""" Inventory Upload uses Inventory serializer
Operating System Serializer is called for inventory Operating system dict.
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert serializer.called
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
@patch.object(Inventory.Software, '__init__')
def test_inventory_serializer_inventory_software_called(self, serializer):
""" Inventory Upload uses Inventory serializer
Software Serializer is called for inventory software list.
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert serializer.called
def test_api_inventory_device_added(self):
""" Device is created """
assert self.device.name == self.inventory['details']['name']
def test_api_inventory_device_uuid_match(self):
""" Device uuid match """
assert self.device.uuid == self.inventory['details']['uuid']
def test_api_inventory_device_serial_number_match(self):
""" Device SN match """
assert self.device.serial_number == self.inventory['details']['serial_number']
def test_api_inventory_operating_system_added(self):
""" Operating System is created """
assert self.operating_system.name == self.inventory['os']['name']
def test_api_inventory_operating_system_version_added(self):
""" Operating System version is created """
assert self.operating_system_version.name == self.inventory['os']['version_major']
def test_api_inventory_device_has_operating_system_added(self):
""" Operating System version linked to device """
assert self.device_operating_system.version == self.inventory['os']['version']
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_operating_system_version_is_semver(self):
""" Operating System version is full semver
Operating system versions name is the major version number of semver.
The device version is to be full semver
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_no_version_cleaned(self):
""" Check softare cleaned up
As part of the inventory upload the software versions of software found on the device is set to null
and before the processing is completed, the version=null software is supposed to be cleaned up.
"""
pass
def test_api_inventory_software_category_added(self):
""" Software category exists """
assert self.software_category.name == self.inventory['software'][0]['category']
def test_api_inventory_software_added(self):
""" Test software exists """
assert self.software.name == self.inventory['software'][0]['name']
def test_api_inventory_software_category_linked_to_software(self):
""" Software category linked to software """
assert self.software.category == self.software_category
def test_api_inventory_software_version_added(self):
""" Test software version exists """
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_software_version_returns_semver(self):
""" Software Version from inventory returns semver if within version string """
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
def test_api_inventory_software_version_returns_original_version(self):
""" Software Version from inventory returns inventoried version if no semver found """
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
def test_api_inventory_software_version_linked_to_software(self):
""" Test software version linked to software it belongs too """
assert self.software_version.software == self.software
def test_api_inventory_device_has_software_version(self):
""" Inventoried software is linked to device and it's the corret one"""
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_device_software_has_installed_date(self):
""" Inventoried software version has install date """
assert self.device_software.installed is not None
def test_api_inventory_device_software_installed_date_type(self):
""" Inventoried software version has install date """
assert type(self.device_software.installed) is datetime.datetime
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
""" A blank installed date of software is updated if the software was already attached to the device """
pass
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_api_inventory_valid_status_ok_existing_device(self):
""" Successful inventory upload returns 200 for existing device"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 200
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_api_inventory_invalid_status_bad_request(self):
""" Incorrectly formated inventory upload returns 400 """
client = Client()
url = reverse('API:_api_device_inventory')
mod_inventory = self.inventory.copy()
mod_inventory.update({
'details': {
'name': 'test_api_inventory_invalid_status_bad_request'
},
'software': {
'not_within_a': 'list'
}
})
client.force_login(self.add_user)
response = client.post(url, data=mod_inventory, content_type='application/json')
assert response.status_code == 400
@pytest.mark.skip(reason="to be written")
def test_api_inventory_exeception_status_sever_error(self):
""" if the method throws an exception 500 must be returned.
idea to test: add a random key to the report that is not documented
and perform some action against it that will cause a python exception.
"""
pass
class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
""" Test inventory upload with different name
should match by serial number
"""
model = Device
model_name = 'device'
app_label = 'itam'
inventory = {
"details": {
"name": "device_name",
"serial_number": "serial_number_123",
"uuid": "string"
},
"os": {
"name": "os_name",
"version_major": "12",
"version": "12.1"
},
"software": [
{
"name": "software_name",
"category": "category_name",
"version": "1.2.3"
},
{
"name": "software_name_not_semver",
"category": "category_name",
"version": "2024.4"
},
{
"name": "software_name_semver_contained",
"category": "category_name",
"version": "1.2.3-rc1"
},
]
}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
2. Create a team for user with correct permissions
3. add user to the teeam
4. upload the inventory
5. conduct queries for tests
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
Device.objects.create(
name='random device name',
serial_number='serial_number_123'
)
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
self.add_user = User.objects.create_user(username="test_user_add", password="password")
add_user_settings = UserSettings.objects.get(user=self.add_user)
add_user_settings.default_organization = organization
add_user_settings.save()
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
# upload the inventory
process_inventory(json.dumps(self.inventory), organization.id)
self.device = Device.objects.get(name=self.inventory['details']['name'])
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
self.software_version = SoftwareVersion.objects.get(
name = self.inventory['software'][0]['version'],
software = self.software,
)
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
self.software_version_not_semver = SoftwareVersion.objects.get(
name = self.inventory['software'][1]['version'],
software = self.software_not_semver
)
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
self.software_version_is_semver = SoftwareVersion.objects.get(
software = self.software_is_semver
)
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
def test_api_inventory_device_added(self):
""" Device is created """
assert self.device.name == self.inventory['details']['name']
def test_api_inventory_device_uuid_match(self):
""" Device uuid match """
assert self.device.uuid == self.inventory['details']['uuid']
def test_api_inventory_device_serial_number_match(self):
""" Device SN match """
assert self.device.serial_number == self.inventory['details']['serial_number']
def test_api_inventory_operating_system_added(self):
""" Operating System is created """
assert self.operating_system.name == self.inventory['os']['name']
def test_api_inventory_operating_system_version_added(self):
""" Operating System version is created """
assert self.operating_system_version.name == self.inventory['os']['version_major']
def test_api_inventory_device_has_operating_system_added(self):
""" Operating System version linked to device """
assert self.device_operating_system.version == self.inventory['os']['version']
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_operating_system_version_is_semver(self):
""" Operating System version is full semver
Operating system versions name is the major version number of semver.
The device version is to be full semver
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_no_version_cleaned(self):
""" Check softare cleaned up
As part of the inventory upload the software versions of software found on the device is set to null
and before the processing is completed, the version=null software is supposed to be cleaned up.
"""
pass
def test_api_inventory_software_category_added(self):
""" Software category exists """
assert self.software_category.name == self.inventory['software'][0]['category']
def test_api_inventory_software_added(self):
""" Test software exists """
assert self.software.name == self.inventory['software'][0]['name']
def test_api_inventory_software_category_linked_to_software(self):
""" Software category linked to software """
assert self.software.category == self.software_category
def test_api_inventory_software_version_added(self):
""" Test software version exists """
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_software_version_returns_semver(self):
""" Software Version from inventory returns semver if within version string """
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
def test_api_inventory_software_version_returns_original_version(self):
""" Software Version from inventory returns inventoried version if no semver found """
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
def test_api_inventory_software_version_linked_to_software(self):
""" Test software version linked to software it belongs too """
assert self.software_version.software == self.software
def test_api_inventory_device_has_software_version(self):
""" Inventoried software is linked to device and it's the corret one"""
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_device_software_has_installed_date(self):
""" Inventoried software version has install date """
assert self.device_software.installed is not None
def test_api_inventory_device_software_installed_date_type(self):
""" Inventoried software version has install date """
assert type(self.device_software.installed) is datetime.datetime
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
""" A blank installed date of software is updated if the software was already attached to the device """
pass
class InventoryAPIDifferentNameUUIDMatch(TestCase):
""" Test inventory upload with different name
should match by uuid
"""
model = Device
model_name = 'device'
app_label = 'itam'
inventory = {
"details": {
"name": "device_name",
"serial_number": "serial_number_123",
"uuid": "123-456-789"
},
"os": {
"name": "os_name",
"version_major": "12",
"version": "12.1"
},
"software": [
{
"name": "software_name",
"category": "category_name",
"version": "1.2.3"
},
{
"name": "software_name_not_semver",
"category": "category_name",
"version": "2024.4"
},
{
"name": "software_name_semver_contained",
"category": "category_name",
"version": "1.2.3-rc1"
},
]
}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
2. Create a team for user with correct permissions
3. add user to the teeam
4. upload the inventory
5. conduct queries for tests
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
Device.objects.create(
name='random device name',
uuid='123-456-789'
)
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
self.add_user = User.objects.create_user(username="test_user_add", password="password")
add_user_settings = UserSettings.objects.get(user=self.add_user)
add_user_settings.default_organization = organization
add_user_settings.save()
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
# upload the inventory
process_inventory(json.dumps(self.inventory), organization.id)
self.device = Device.objects.get(name=self.inventory['details']['name'])
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
self.software_version = SoftwareVersion.objects.get(
name = self.inventory['software'][0]['version'],
software = self.software,
)
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
self.software_version_not_semver = SoftwareVersion.objects.get(
name = self.inventory['software'][1]['version'],
software = self.software_not_semver
)
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
self.software_version_is_semver = SoftwareVersion.objects.get(
software = self.software_is_semver
)
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
def test_api_inventory_device_added(self):
""" Device is created """
assert self.device.name == self.inventory['details']['name']
def test_api_inventory_device_uuid_match(self):
""" Device uuid match """
assert self.device.uuid == self.inventory['details']['uuid']
def test_api_inventory_device_serial_number_match(self):
""" Device SN match """
assert self.device.serial_number == self.inventory['details']['serial_number']
def test_api_inventory_operating_system_added(self):
""" Operating System is created """
assert self.operating_system.name == self.inventory['os']['name']
def test_api_inventory_operating_system_version_added(self):
""" Operating System version is created """
assert self.operating_system_version.name == self.inventory['os']['version_major']
def test_api_inventory_device_has_operating_system_added(self):
""" Operating System version linked to device """
assert self.device_operating_system.version == self.inventory['os']['version']
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_operating_system_version_is_semver(self):
""" Operating System version is full semver
Operating system versions name is the major version number of semver.
The device version is to be full semver
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_inventory_software_no_version_cleaned(self):
""" Check softare cleaned up
As part of the inventory upload the software versions of software found on the device is set to null
and before the processing is completed, the version=null software is supposed to be cleaned up.
"""
pass
def test_api_inventory_software_category_added(self):
""" Software category exists """
assert self.software_category.name == self.inventory['software'][0]['category']
def test_api_inventory_software_added(self):
""" Test software exists """
assert self.software.name == self.inventory['software'][0]['name']
def test_api_inventory_software_category_linked_to_software(self):
""" Software category linked to software """
assert self.software.category == self.software_category
def test_api_inventory_software_version_added(self):
""" Test software version exists """
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_software_version_returns_semver(self):
""" Software Version from inventory returns semver if within version string """
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
def test_api_inventory_software_version_returns_original_version(self):
""" Software Version from inventory returns inventoried version if no semver found """
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
def test_api_inventory_software_version_linked_to_software(self):
""" Test software version linked to software it belongs too """
assert self.software_version.software == self.software
def test_api_inventory_device_has_software_version(self):
""" Inventoried software is linked to device and it's the corret one"""
assert self.software_version.name == self.inventory['software'][0]['version']
def test_api_inventory_device_software_has_installed_date(self):
""" Inventoried software version has install date """
assert self.device_software.installed is not None
def test_api_inventory_device_software_installed_date_type(self):
""" Inventoried software version has install date """
assert type(self.device_software.installed) is datetime.datetime
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
""" A blank installed date of software is updated if the software was already attached to the device """
pass

View File

@ -0,0 +1,283 @@
import celery
import pytest
import unittest
import requests
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import TestCase, Client
from django.test.utils import override_settings
from unittest.mock import patch
from access.models import Organization, Team, TeamUsers, Permission
from itam.models.device import Device
from settings.models.user_settings import UserSettings
class InventoryPermissionsAPI(TestCase):
model = Device
model_name = 'device'
app_label = 'itam'
inventory = {
"details": {
"name": "device_name",
"serial_number": "a serial number",
"uuid": "string"
},
"os": {
"name": "os_name",
"version_major": "12",
"version": "12.1"
},
"software": [
{
"name": "software_name",
"category": "category_name",
"version": "1.2.3"
}
]
}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a device
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
# self.item = self.model.objects.create(
# organization=organization,
# name = 'deviceone'
# )
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
add_user_settings = UserSettings.objects.get(user=self.add_user)
add_user_settings.default_organization = organization
add_user_settings.save()
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('API:_api_device_inventory')
response = client.put(url, data=self.inventory, content_type='application/json')
assert response.status_code == 401
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.no_permissions_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.different_organization_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.view_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 403
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPOGATES=True)
def test_device_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('API:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
assert response.status_code == 200

View File

@ -0,0 +1,35 @@
from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
@pytest.mark.skip(reason="to be written")
def test_api_access_auth_required(user):
"""Ensure that no api access has been granted
"""
pass
@pytest.mark.skip(reason="to be written")
def test_api_access_home(user):
"""Ensure api home view visible once logged in
"""
pass

View File

@ -0,0 +1,326 @@
import hashlib
import json
import pytest
import requests
import unittest
from datetime import datetime, timedelta
from django.contrib.auth.models import AnonymousUser, User
from django.shortcuts import reverse
from django.test import TestCase, Client
from access.models import Organization, Team, TeamUsers, Permission
from api.models.tokens import AuthToken
from settings.models.user_settings import UserSettings
class APIAuthToken(TestCase):
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
3. create user
4. create user settings
5. create API key (valid)
6. generate an API key that does not exist
5. create API key (expired)
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.add_user = User.objects.create_user(username="test_user_add", password="password")
add_user_settings = UserSettings.objects.get(user=self.add_user)
add_user_settings.default_organization = organization
add_user_settings.save()
expires = datetime.utcnow() + timedelta(days = 10)
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
token = AuthToken.objects.create(
user = self.add_user,
expires=expires
)
self.api_token_valid = token.generate()
self.hashed_token = token.token_hash(self.api_token_valid)
token.token = self.hashed_token
token.save()
self.api_token_does_not_exist = hashlib.sha256(str('a random string').encode('utf-8')).hexdigest()
expires = datetime.utcnow() + timedelta(days = -10)
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
self.api_token_expired = token.generate()
self.hashed_token_expired = token.token_hash(self.api_token_expired)
token = AuthToken.objects.create(
user = self.add_user,
expires=expires,
token = self.hashed_token_expired
)
def test_token_create_own(self):
""" Check correct permission for add
User can only create token for self.
"""
client = Client()
client.force_login(self.add_user)
url = reverse('_user_auth_token_add', kwargs={'user_id': self.add_user.id})
response = client.post(url, kwargs={'user_id': self.add_user.id})
assert response.status_code == 200
def test_token_create_other_user(self):
""" Check correct permission for add
User can not create token for another user.
"""
client = Client()
client.force_login(self.add_user)
url = reverse('_user_auth_token_add', kwargs={'user_id': 999})
response = client.post(url, kwargs={'user_id': 999})
assert response.status_code == 403
def test_token_delete_own(self):
""" Check correct permission for delete
User can only delete token for self.
"""
client = Client()
client.force_login(self.add_user)
url = reverse('_user_auth_token_delete', kwargs={'user_id': self.add_user.id, 'pk': 1})
response = client.post(url, kwargs={'user_id': self.add_user.id, 'pk': 1})
assert response.status_code == 302 and response.url == '/account/settings/1'
def test_token_delete_other_user(self):
""" Check correct permission for delete
User can not delete another users token.
"""
client = Client()
client.force_login(self.add_user)
url = reverse('_user_auth_token_delete', kwargs={'user_id': 999, 'pk': 1})
response = client.post(url, data={'id': 1}, kwargs={'user_id': 999, 'pk': 1})
assert response.status_code == 403
def test_auth_invalid_token(self):
""" Check token authentication
Invalid token does not allow login
"""
client = Client()
url = reverse('home') + 'api/'
response = client.get(
url,
content_type='application/json',
headers = {
'Accept': 'application/json',
'Authorization': 'Token ' + self.api_token_does_not_exist,
}
)
assert response.status_code == 401
def test_auth_no_token(self):
""" Check token authentication
providing no token does not allow login
"""
client = Client()
url = reverse('home') + 'api/'
response = client.get(
url,
content_type='application/json',
headers = {
'Accept': 'application/json'
}
)
assert response.status_code == 401
def test_auth_expired_token(self):
""" Check token authentication
expired token does not allow login
"""
client = Client()
url = reverse('home') + 'api/'
response = client.get(
url,
content_type='application/json',
headers = {
'Accept': 'application/json',
'Authorization': 'Token ' + self.api_token_expired,
}
)
assert response.status_code == 401
def test_auth_valid_token(self):
""" Check token authentication
Valid token allows login
"""
client = Client()
url = reverse('home') + 'api/'
response = client.get(
url,
content_type='application/json',
headers = {
'Accept': 'application/json',
'Authorization': 'Token ' + self.api_token_valid,
}
)
assert response.status_code == 200
def test_feat_expired_token_is_removed(self):
""" token feature confirmation
expired token is deleted
"""
client = Client()
url = reverse('home') + 'api/'
response = client.get(
url,
content_type='application/json',
headers = {
'Accept': 'application/json',
'Authorization': 'Token ' + self.api_token_expired,
}
)
db_query = AuthToken.objects.filter(
token = self.hashed_token_expired
)
assert not db_query.exists()
def test_token_not_saved_to_db(self):
""" confirm generated token not saved to the database """
db_query = AuthToken.objects.filter(
token = self.api_token_valid
)
assert not db_query.exists()
def test_header_format_invalid_token(self):
""" token header format check
header missing 'Token' prefix reports invalid
"""
client = Client()
url = reverse('home') + 'api/'
response = client.get(
url,
content_type='application/json',
headers = {
'Accept': 'application/json',
'Authorization': '' + self.api_token_valid,
}
)
content: dict = json.loads(response.content.decode('utf-8'))
assert response.status_code == 401 and content['detail'] == 'Token header invalid'
def test_header_format_invalid_token_spaces(self):
""" token header format check
auth header with extra spaces reports invalid
"""
client = Client()
url = reverse('home') + 'api/'
response = client.get(
url,
content_type='application/json',
headers = {
'Accept': 'application/json',
'Authorization': 'Token A space ' + self.api_token_valid,
}
)
content: dict = json.loads(response.content.decode('utf-8'))
assert response.status_code == 401 and content['detail'] == 'Token header invalid. Possibly incorrectly formatted'

43
app/api/urls.py Normal file
View File

@ -0,0 +1,43 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from rest_framework.urlpatterns import format_suffix_patterns
from .views import access, config, index
from .views.itam import software, config as itam_config
from .views.itam.device import DeviceViewSet
from .views.itam import inventory
app_name = "API"
router = DefaultRouter()
router.register('', index.Index, basename='_api_home')
router.register('device', DeviceViewSet, basename='device')
router.register('software', software.SoftwareViewSet, basename='software')
urlpatterns = [
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),
path("configuration/<int:pk>", config.ConfigGroupsDetail.as_view(), name='_api_config_group'),
path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"),
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),
path("organization/<int:pk>/", access.OrganizationDetail.as_view(), name='_api_organization'),
path("organization/<int:organization_id>/team", access.TeamList.as_view(), name='_api_organization_teams'),
path("organization/<int:organization_id>/team/<int:group_ptr_id>/", access.TeamDetail.as_view(), name='_api_team'),
path("organization/<int:organization_id>/team/<int:group_ptr_id>/permissions", access.TeamPermissionDetail.as_view(), name='_api_team_permission'),
path("organization/team/", access.TeamList.as_view(), name='_api_teams'),
]
urlpatterns = format_suffix_patterns(urlpatterns)
urlpatterns += router.urls

317
app/api/views/access.py Normal file
View File

@ -0,0 +1,317 @@
from django.contrib.auth.models import Permission
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from rest_framework import generics, routers, serializers, views
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.response import Response
from access.mixin import OrganizationMixin
from access.models import Organization, Team
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer, TeamPermissionSerializer
from api.views.mixin import OrganizationPermissionAPI
@extend_schema_view(
get=extend_schema(
summary = "Fetch Organizations",
description="Returns a list of organizations."
),
)
class OrganizationList(generics.ListAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = Organization.objects.all()
lookup_field = 'pk'
serializer_class = OrganizationListSerializer
def get_view_name(self):
return "Organizations"
@extend_schema_view(
get=extend_schema(
summary = "Get An Organization",
),
patch=extend_schema(
summary = "Update an organization",
),
put=extend_schema(
summary = "Update an organization",
),
)
class OrganizationDetail(generics.RetrieveUpdateAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = Organization.objects.all()
lookup_field = 'pk'
serializer_class = OrganizationSerializer
def get_view_name(self):
return "Organization"
@extend_schema_view(
post=extend_schema(
summary = "Create a Team",
description = """Create a team within the defined organization.""",
tags = ['team',],
request = TeamSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
create=extend_schema(exclude=True),
)
class TeamList(generics.ListCreateAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = Team.objects.all()
serializer_class = TeamSerializer
def get_queryset(self):
self.queryset = Team.objects.filter(organization=self.kwargs['organization_id'])
return self.queryset
def get_view_name(self):
return "Organization Teams"
@extend_schema_view(
get=extend_schema(
summary = "Fetch a Team",
description = """Fetch a team within the defined organization.
""",
methods=["GET"],
tags = ['team',],
request = TeamSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
patch=extend_schema(
summary = "Update a Team",
description = """Update a team within the defined organization.
""",
methods=["Patch"],
tags = ['team',],
request = TeamSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
put = extend_schema(
summary = "Amend a team",
tags = ['team',],
),
delete=extend_schema(
summary = "Delete a Team",
tags = ['team',],
),
post = extend_schema(
exclude = True,
)
)
class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = Team.objects.all()
serializer_class = TeamSerializer
lookup_field = 'group_ptr_id'
@extend_schema_view(
get=extend_schema(
summary = "Fetch a teams permissions",
tags = ['team',],
),
post=extend_schema(
summary = "Replace team Permissions",
description = """Replace the teams permissions with the permissions supplied.
Teams Permissions will be replaced with the permissions supplied. **ALL** existing permissions will be
removed.
permissions are required to be in format `<module name>_<permission>_<table name>`
""",
methods=["POST"],
tags = ['team',],
request = TeamPermissionSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
delete=extend_schema(
summary = "Delete permissions",
tags = ['team',],
),
patch = extend_schema(
summary = "Amend team Permissions",
description = """Amend the teams permissions with the permissions supplied.
Teams permissions will include the existing permissions along with the ones supplied.
permissions are required to be in format `<module name>_<permission>_<table name>`
""",
methods=["PATCH"],
parameters = None,
tags = ['team',],
request = TeamPermissionSerializer,
responses = {
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
}
),
put = extend_schema(
summary = "Amend team Permissions",
tags = ['team',],
)
)
class TeamPermissionDetail(views.APIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = Team.objects.all()
serializer_class = TeamPermissionSerializer
def get(self, request, *args, **kwargs):
return Response(data=Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()[0])
def get_view_name(self):
return "Team Permissions"
def delete(self, request, *args, **kwargs):
vals = self.process_request()
remove = vals['remove']
new_permission = Team.objects.get(pk=self.kwargs['group_ptr_id'])
for remove_permission in remove:
new_permission.permissions.remove(remove_permission)
new_permission.save()
return Response(data=Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()[0])
def patch(self, request, *args, **kwargs):
vals = self.process_request()
add = vals['add']
new_permission = Team.objects.get(pk=self.kwargs['group_ptr_id'])
for add_permission in add:
new_permission.permissions.add(add_permission)
new_permission.save()
return Response(data=Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()[0])
def post(self, request, *args, **kwargs):
vals = self.process_request()
add = vals['add']
remove = vals['remove']
exists = vals['exists']
new_permission = Team.objects.get(pk=self.kwargs['group_ptr_id'])
for add_permission in add:
new_permission.permissions.add(add_permission)
new_permission.save()
for remove_permission in remove:
new_permission.permissions.remove(remove_permission)
new_permission.save()
return Response(data=Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()[0])
def process_request(self) -> dict({
"add": list,
"remove": list,
"exists": list
}):
initial_values = Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()
add = []
remove = []
exists = []
for request_permission in self.request.data:
fields = request_permission.split('.')
try:
permission = Permission.objects.get(codename=str(fields[1]), content_type__app_label=str(fields[0]))
exists += [ permission.id ]
if permission and request_permission not in initial_values[0]:
add += [ permission.id ]
except:
raise serializers.ValidationError(f'Value was invalid: {request_permission}')
for existing_permission in initial_values[1].all():
if existing_permission.id not in add and existing_permission.id not in exists:
remove += [ existing_permission.id ]
return {
"add": add,
"remove": remove,
"exists": exists
}

54
app/api/views/config.py Normal file
View File

@ -0,0 +1,54 @@
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics
from api.serializers.config import ConfigGroupsSerializer
from api.views.mixin import OrganizationPermissionAPI
from config_management.models.groups import ConfigGroups
@extend_schema_view(
get=extend_schema(
summary = "Fetch Config groups",
description="Returns a list of Config Groups."
),
)
class ConfigGroupsList(generics.ListAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ConfigGroups.objects.all()
lookup_field = 'pk'
serializer_class = ConfigGroupsSerializer
def get_view_name(self):
return "Config Groups"
@extend_schema_view(
get=extend_schema(
summary = "Get A Config Group",
# responses = {}
),
)
class ConfigGroupsDetail(generics.RetrieveAPIView):
permission_classes = [
OrganizationPermissionAPI
]
queryset = ConfigGroups.objects.all()
lookup_field = 'pk'
serializer_class = ConfigGroupsSerializer
def get_view_name(self):
return "Config Group"

34
app/api/views/index.py Normal file
View File

@ -0,0 +1,34 @@
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from django.contrib.auth.models import User
from django.utils.safestring import mark_safe
from rest_framework import generics, permissions, routers, viewsets
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
class Index(viewsets.ViewSet):
# permission_required = 'access.view_organization'
def get_view_name(self):
return "API Index"
def get_view_description(self, html=False) -> str:
text = "My REST API"
if html:
return mark_safe(f"<p>{text}</p>")
else:
return text
def list(self, request, pk=None):
return Response(
{
# "teams": reverse("_api_teams", request=request),
"devices": reverse("API:device-list", request=request),
"config_groups": reverse("API:_api_config_groups", request=request),
"organizations": reverse("API:_api_orgs", request=request),
"software": reverse("API:software-list", request=request),
}
)

View File

View File

@ -0,0 +1,19 @@
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from itam.models.device import Device
from rest_framework import views
from rest_framework.response import Response
class View(views.APIView):
def get(self, request, slug):
device = Device.objects.get(slug=slug)
return Response(device.get_configuration(device.id))
def get_view_name(self):
return "Device Config"

View File

@ -0,0 +1,55 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema
from rest_framework import generics, viewsets
from access.mixin import OrganizationMixin
from api.serializers.itam.device import DeviceSerializer
from api.views.mixin import OrganizationPermissionAPI
from itam.models.device import Device
class DeviceViewSet(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
]
queryset = Device.objects.all()
serializer_class = DeviceSerializer
@extend_schema( description='Fetch devices that are from the users assigned organization(s)', methods=["GET"])
def list(self, request):
return super().list(request)
@extend_schema( description='Fetch the selected device', methods=["GET"])
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
def get_queryset(self):
if self.request.user.is_superuser:
return self.queryset.filter().order_by('name')
else:
return self.queryset.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
def get_view_name(self):
if self.detail:
return "Device"
return 'Devices'

View File

@ -0,0 +1,123 @@
import json
import re
from django.core.exceptions import ValidationError, PermissionDenied
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import generics, views
from rest_framework.response import Response
from api.views.mixin import OrganizationPermissionAPI
from api.serializers.itam.inventory import InventorySerializer
from api.serializers.inventory import Inventory
from core.http.common import Http
from itam.models.device import Device
from settings.models.user_settings import UserSettings
from api.tasks import process_inventory
class InventoryPermissions(OrganizationPermissionAPI):
def permission_check(self, request, view, obj=None) -> bool:
data = view.request.data
self.obj = Device.objects.get(slug=str(data.details.name).lower())
return super().permission_check(request, view, obj=None)
class Collect(OrganizationPermissionAPI, views.APIView):
queryset = Device.objects.all()
@extend_schema(
summary = "Upload a device's inventory",
description = """After inventorying a device, it's inventory file, `.json` is uploaded to this endpoint.
If the device does not exist, it will be created. If the device does exist the existing
device will be updated with the information within the inventory.
matching for an existing device is by slug which is the hostname converted to lower case
letters. This conversion is automagic.
**NOTE:** _for device creation, the API user must have user setting 'Default Organization'. Without
this setting populated, no device will be created and the endpoint will return HTTP/403_
## Permissions
- `itam.add_device` Required to upload inventory
""",
methods=["POST"],
parameters = None,
tags = ['device', 'inventory',],
request = InventorySerializer,
responses = {
200: OpenApiResponse(description='Inventory upload successful'),
401: OpenApiResponse(description='User Not logged in'),
403: OpenApiResponse(description='User is missing permission or in different organization'),
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
}
)
def post(self, request, *args, **kwargs):
status = Http.Status.OK
response_data = 'OK'
try:
data = json.loads(request.body)
data = Inventory(data)
device = None
self.default_organization = UserSettings.objects.get(user=request.user).default_organization
if Device.objects.filter(slug=str(data.details.name).lower()).exists():
self.obj = Device.objects.get(slug=str(data.details.name).lower())
device = self.obj
if not self.permission_check(request=request, view=self, obj=device):
raise Http404
task = process_inventory.delay(request.body, self.default_organization.id)
response_data: dict = {"task_id": f"{task.id}"}
except PermissionDenied as e:
status = Http.Status.FORBIDDEN
response_data = ''
except ValidationError as e:
status = Http.Status.BAD_REQUEST
response_data = e.message
except Exception as e:
print(f'An error occured{e}')
status = Http.Status.SERVER_ERROR
response_data = 'Unknown Server Error occured'
return Response(data=response_data,status=status)
def get_view_name(self):
return "Inventory"

View File

@ -0,0 +1,43 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404
from rest_framework import generics, viewsets
from access.mixin import OrganizationMixin
from api.serializers.itam.software import SoftwareSerializer
from api.views.mixin import OrganizationPermissionAPI
from itam.models.software import Software
class SoftwareViewSet(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [
OrganizationPermissionAPI
]
queryset = Software.objects.all()
serializer_class = SoftwareSerializer
def get_object(self, queryset=None, **kwargs):
item = self.kwargs.get('pk')
return get_object_or_404(Software, pk=item)
def get_queryset(self):
if self.request.user.is_superuser:
return self.queryset.filter().order_by('name')
else:
return self.queryset.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
def get_view_name(self):
return "Software"

146
app/api/views/mixin.py Normal file
View File

@ -0,0 +1,146 @@
from django.core.exceptions import PermissionDenied
from django.forms import ValidationError
from rest_framework import exceptions
from rest_framework.permissions import DjangoObjectPermissions
from access.mixin import OrganizationMixin
class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
"""checking organization membership"""
def has_permission(self, request, view):
return self.permission_check(request, view)
def has_object_permission(self, request, view, obj):
return self.permission_check(request, view, obj)
def permission_check(self, request, view, obj=None) -> bool:
if request.user.is_anonymous:
return False
self.request = request
method = self.request._request.method.lower()
if method.upper() not in view.allowed_methods:
view.http_method_not_allowed(request._request)
if hasattr(view, 'queryset'):
if view.queryset.model._meta:
self.obj = view.queryset.model
object_organization = None
if method == 'get':
action = 'view'
elif method == 'post':
action = 'add'
if 'organization' in request.data:
if not request.data['organization']:
raise ValidationError('you must provide an organization')
object_organization = int(request.data['organization'])
elif method == 'patch':
action = 'change'
elif method == 'put':
action = 'change'
elif method == 'delete':
action = 'delete'
else:
action = 'view'
permission = self.obj._meta.app_label + '.' + action + '_' + self.obj._meta.model_name
self.permission_required = [ permission ]
if view:
if 'organization_id' in view.kwargs:
if view.kwargs['organization_id']:
object_organization = view.kwargs['organization_id']
if object_organization is None and 'pk' in view.kwargs:
self.obj = view.queryset.get(pk=view.kwargs['pk'])
if obj:
if obj.get_organization():
object_organization = obj.get_organization().id
if hasattr(self.obj, 'is_global'):
if obj.is_global:
object_organization = 0
if 'pk' in view.kwargs:
if object_organization is None and view.queryset.model._meta.model_name == 'organization' and view.kwargs['pk']:
object_organization = view.kwargs['pk']
if object_organization is None:
self.obj = view.queryset.get()
if hasattr(self, 'obj') and object_organization is None and 'pk' in view.kwargs:
if self.obj.get_organization():
object_organization = self.obj.get_organization().id
if hasattr(self.obj, 'is_global'):
if self.obj.is_global:
object_organization = 0
# ToDo: implement proper checking of listview as this if allows ALL.
if 'pk' not in view.kwargs and method == 'get' and object_organization is None:
return True
if hasattr(self, 'default_organization'):
object_organization = self.default_organization
if method == 'post' and hasattr(self, 'default_organization'):
if self.default_organization:
object_organization = self.default_organization.id
if not self.has_organization_permission(object_organization) and not request.user.is_superuser:
raise PermissionDenied('You are not part of this organization')
return True

Some files were not shown because too many files have changed in this diff Show More