Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api docs #142

Merged
merged 120 commits into from
Feb 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
eb7c65e
Rewrite inventory to use queries for all views except ApplicationList
sheagcraig Sep 5, 2017
248e550
Update included datatableview
sheagcraig Sep 6, 2017
cc9060c
Update to new datatableview API, backpedal on URL changes
sheagcraig Sep 6, 2017
7987434
Add SalSettings for inventory filtering
sheagcraig Sep 6, 2017
da06718
Add django User session handling to custom permission class
sheagcraig Sep 7, 2017
a402a39
Add `rest_framework` to installed apps, and use global settings
sheagcraig Sep 7, 2017
04c92f4
Add API doc URLs, and clean up base `urls.py` and `api.urls.py`
sheagcraig Sep 7, 2017
3ed49d6
Remove view-class-level perm and auth overrides. Reorder imports
sheagcraig Sep 7, 2017
f1ffbdc
Use new django-rest-framework explicit field declarations
sheagcraig Sep 7, 2017
9eafbd8
Update django-rest-framework, add markdown and pygments for docs
sheagcraig Sep 7, 2017
9305c2a
Remove some boilerplate and the now-unused json_response module.
sheagcraig Sep 7, 2017
b4ca543
Use `request.user.userprofile.level` for session auth to API
sheagcraig Sep 7, 2017
ef0309e
Begin working on API docs.
sheagcraig Sep 7, 2017
2148cb7
Start migrating obvious models to ModelViewSets to simplify
sheagcraig Sep 8, 2017
df330df
Add custom queryfields mixin along with merging Machines endpoints.
sheagcraig Sep 21, 2017
a598394
Formatting
sheagcraig Sep 21, 2017
b7bd090
Update machine_detail
bochoven Sep 22, 2017
fd14976
Merge pull request #143 from bochoven/patch-1
grahamgilbert Sep 22, 2017
134430f
Reports can be unzipped if they re small enough
grahamgilbert Oct 2, 2017
f6af08f
Merge pull request #144 from salopensource/nonb2zip
grahamgilbert Oct 3, 2017
f872a7b
Don't tie ourselves to System Profiler's format.
grahamgilbert Oct 3, 2017
35f66fe
Merge pull request #145 from salopensource/hwinfo
grahamgilbert Oct 3, 2017
90d8345
Support memory to be shipped in KB
grahamgilbert Oct 3, 2017
a657773
!= searches could sneak through and not be negated
grahamgilbert Oct 5, 2017
f0c3ff2
Because why not
grahamgilbert Oct 5, 2017
ee2d7ce
Merge pull request #146 from salopensource/searchinvestigations
grahamgilbert Oct 5, 2017
5431604
fix multiple dictionary in array conditions.
erikng Oct 12, 2017
4f60573
Merge pull request #147 from erikng/multidicts
grahamgilbert Oct 12, 2017
1af7026
Merge pull request #141 from sheagcraig/inventory_app_path_fix
grahamgilbert Oct 12, 2017
4cfa341
Merge branch 'master' into gosal_compatability
bdemetris Oct 13, 2017
b7c05bf
Merge branch 'master' into gosal_compatability
bdemetris Oct 13, 2017
7a597fc
Golang struct support
bdemetris Oct 13, 2017
abbb84e
Use variables instead of repeating code
bdemetris Oct 13, 2017
3f85ccf
Merge pull request #148 from bdemetris/gosal_compatability
grahamgilbert Oct 13, 2017
3634ff1
Initial commit of 3rd party client docs
grahamgilbert Oct 17, 2017
e9048a6
keep default behavior for macos, fill osvers twice
bdemetris Oct 17, 2017
44ab805
re nest if statement for version length
bdemetris Oct 17, 2017
a48afb4
Merge pull request #150 from bdemetris/osverspatch
grahamgilbert Oct 17, 2017
ad66838
Switches line 40 and 41 to resolve blow-up issue
adoering-te Oct 19, 2017
79a2cc4
removed .count() from filter_machines in uptime.py
Nov 6, 2017
f11361a
Merge pull request #153 from w0de/master
grahamgilbert Nov 6, 2017
fe54c1d
Add documentation for filtering machine endpoint.
sheagcraig Nov 8, 2017
17f28cb
Implement DRF-style Fact and Condition endpoints.
sheagcraig Nov 8, 2017
fbabb88
fix: Enable plugins by name
clburlison Nov 8, 2017
be932c1
Merge pull request #151 from delize/fix-cryptstatus.py
grahamgilbert Nov 8, 2017
d49dd48
fix: Update Machine Details Plugin to enable vi UI
clburlison Nov 8, 2017
ec2e9eb
Merge pull request #149 from salopensource/3rdpartyclientdocs
grahamgilbert Nov 8, 2017
eac42e0
Merge pull request #154 from clburlison/fix-enable-plugin
grahamgilbert Nov 8, 2017
e244724
Use has_permission instead of has_object_permission in API auth
sheagcraig Nov 8, 2017
0e50580
Merge pull request #155 from sheagcraig/auth_fix
grahamgilbert Nov 8, 2017
69440ff
Bump build number
grahamgilbert Nov 8, 2017
3989a57
Factor out machine filter by serial as a mixin
sheagcraig Nov 8, 2017
a67c1f7
Move Inventory to new DRF style.
sheagcraig Nov 8, 2017
2dd6541
Add django-filters to installed apps and start applying to API views
sheagcraig Nov 9, 2017
71ccdf7
Add machine__hostname to inventory filtering.
sheagcraig Nov 9, 2017
9d20788
Move pending_updates and pending_apple updates.
sheagcraig Nov 9, 2017
91a31c2
Reorder views into alpha order.
sheagcraig Nov 9, 2017
2078e9d
Finish moving routes and use DefaultRouter
sheagcraig Nov 9, 2017
cc63f5b
Add documentation for search terms and finish adding search and filte…
sheagcraig Nov 9, 2017
90b882c
Might as well update this while we're at it.
sheagcraig Nov 10, 2017
e886181
Remove now unused Mixin for serial filtration.
sheagcraig Nov 10, 2017
801b33f
Restore the QueryFieldsMixin that had been mistakenly removed.
sheagcraig Nov 10, 2017
d27e687
Add `/api/saved_searches/<id>/execute` endpoint.
sheagcraig Nov 10, 2017
91e196b
fix: Proper fix for when Crypt server is not set
clburlison Nov 15, 2017
34eb383
Merge pull request #156 from clburlison/proper-crypt-fix
grahamgilbert Nov 15, 2017
1494e65
fix: Update undeployed to work
clburlison Nov 15, 2017
badbbd3
Merge pull request #157 from clburlison/fix-undeployed
grahamgilbert Nov 15, 2017
48f4fba
work around race conditions in startup by forcing retries
Nov 21, 2017
f459e71
Fix: illegal filter reference in InventoryItem
octomike Nov 29, 2017
de3e28a
Fix: missing import for redirect in license views
octomike Nov 29, 2017
4feef99
Merge pull request #159 from MPIB/fix_serverview_redirect
grahamgilbert Nov 29, 2017
aedb8d1
Merge pull request #158 from MPIB/fix_license_list
grahamgilbert Nov 29, 2017
7551169
Sal 3.2.7
grahamgilbert Dec 4, 2017
fc5e3f3
Merge branch 'master' of https://github.com/salopensource/sal
grahamgilbert Dec 4, 2017
bfb727a
fix: Correct puppet status alignment
clburlison Dec 8, 2017
6b017e8
Merge pull request #164 from clburlison/puppet-status-align-fix
grahamgilbert Dec 8, 2017
6bd79a7
correcting reference to Slack channel
discentem Dec 12, 2017
68db95c
Merge pull request #165 from discentem/master
grahamgilbert Dec 12, 2017
b9dc5b4
Start to be able to search on facts in the basic search
grahamgilbert Jan 5, 2018
4d5ba85
Handle output from GoSal
grahamgilbert Jan 5, 2018
62e9948
Bump version
grahamgilbert Jan 5, 2018
cb499ca
Merge pull request #167 from salopensource/factsfromwindows
grahamgilbert Jan 5, 2018
388c438
Ability to add facts and conditions to basic search
grahamgilbert Jan 5, 2018
13aff57
Merge pull request #168 from salopensource/search_facts
grahamgilbert Jan 5, 2018
4cd0a06
I am not sure why we cannot search on this
grahamgilbert Jan 10, 2018
c049602
Merge branch 'master' of git://github.com/salopensource/sal
Jan 11, 2018
7c034dc
return only distinct results for global search
Jan 11, 2018
816ea2f
removed race condition work around for PR
Jan 11, 2018
9e2c4dc
Added deploy 'Status' field
Jan 12, 2018
96aca72
Merge pull request #171 from ChefAustin/patch-1
grahamgilbert Jan 12, 2018
31eca68
Merge pull request #170 from w0de/master
grahamgilbert Jan 12, 2018
db89255
Update search_maint.sh for FreeBSD
Zolotkey Jan 17, 2018
cd580a7
Merge pull request #172 from Zolotkey/patch-1
grahamgilbert Jan 17, 2018
4734902
making search_maint.sh path independent
epackorigan Jan 18, 2018
ae0eb3a
missing sal
epackorigan Jan 18, 2018
3309ca2
Broken client reporting
grahamgilbert Jan 19, 2018
2880648
Merge pull request #175 from salopensource/broken_client
grahamgilbert Jan 19, 2018
7abc812
Bump version
grahamgilbert Jan 19, 2018
fdf69c0
Merge pull request #174 from epackorigan/epackorigan-search_maint-fix
grahamgilbert Jan 19, 2018
6cacae2
Merge branch 'api_docs' of github.com:sheagcraig/sal
sheagcraig Jan 19, 2018
5bf422a
Fix typo in requirements file.
sheagcraig Jan 19, 2018
38ef833
Remove unused imports.
sheagcraig Jan 22, 2018
9cb3e4c
Version the API, restoring the original API code to v1.
sheagcraig Jan 22, 2018
3e2617e
The DRF update means we have to explicitly state fields on v1 api.
sheagcraig Jan 22, 2018
76dcb1b
Use args to restrict API docs site to v2.
sheagcraig Jan 22, 2018
524600b
Fix incorrect field querystring name, remove activity from short results
sheagcraig Jan 22, 2018
b17923b
Start testing the API.
sheagcraig Jan 22, 2018
86d78e7
More correctly handle saved search full vs machine full params in API.
sheagcraig Jan 22, 2018
5e39faf
Start abstracting common test tasks out.
sheagcraig Jan 23, 2018
3f0c50a
Add fixtures for more involved testing data usage.
sheagcraig Jan 23, 2018
8c746b8
Add stdout suppressing decorator for test case methods.
sheagcraig Jan 23, 2018
52c64cb
Factor out repetitive auth and get procedures.
sheagcraig Jan 23, 2018
b1c7ef3
Add SavedSearch tests
sheagcraig Jan 23, 2018
567f78c
Lint.
sheagcraig Jan 23, 2018
df48970
Fix machine result based on failed tests, test full param
sheagcraig Jan 23, 2018
c6d83c1
Test API Machine list for full param correctness.
sheagcraig Jan 23, 2018
0250a67
Lint and fix typo in test.
sheagcraig Jan 23, 2018
54e991e
Finish SavedSearch testing, lint, nest search serialization
sheagcraig Jan 23, 2018
d858beb
Restructure testing into separate files. Add API general tests.
sheagcraig Jan 23, 2018
e8968c8
Use some magic to automatically generate tests for boring endpoints
sheagcraig Jan 24, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions 3rdpartyclients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
(This will end up in the wiki, this is just here so we can work on it in the meantime)

There are existing agents for Sal for both macOS and Windows, but if you wish to build a reporting agent for another platform, the sent data should be in the following format.

If the server has basic authentication enabled (which is the default), you should use the username `sal` and the password will be the client’s key.

The data here is represented as a Python dictionary, you should extrapolate the data structure for the language you are writing the agent in.

```
{
# This needs to be unique for each run. uuidgen on macOS is great for this
'run_uuid': 'e38e3237-50c9-41d7-b0fa-86be834bd766’,
# This is the total disk size in
'disk_size': 976163048,
'name': u'thunderpants',
'key': u'yourveryveryverylongkey',
'sal_version': '2.0.4',
# The serial number must be unique - it is the unique key for a checkin
'serial': u'SOMESERIALNUMBERMUSTBEUNIQUE',
# Choose one of the following
'base64report': 'your base64 encoded report',
'base64bz2report': 'your base64, bzip2 compressed report'
}
```

## The report
The report should start life as a plist. How you generate this is up to you (python, go and javascript have libraries for generating these). The plist should then either be just base 64 encoded, or b2zipped, then base 64 encoded. If your platform supports b2zip, it is suggested that you utilize it.

Sal supports two formats for `MachineInfo` - this can either be in the data structure you get from System Profiler on macOS (under the `SystemProfile` key), or you can use the following fields within a `HardwareInfo` dictionary.

```
'MachineInfo':
{
'os_vers': 'Windows ABC Release l33t',
# Available disk space, in KB (integer)
'AvailableDiskSpace': 1230,
# Total disk size, in KB (integer)
'disk_size': 50000,
# One of 'Darwin', 'Windows', 'Linux'
'os_family': 'Windows',
'HardwareInfo': {
'machine_model': 'The model of your computer',
'cpu_type': 'x86 etc',
'current_processor_speed': '20Ghz',
# This must end with ' KB', ' MB', ' GB' or ' TB' - note the space at the end
'physical_memory': '16 GB',
}
}
```
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ There are variants of Sal that support both [SAML](https://hub.docker.com/r/maca

## Having problems?

You should check out the [troubleshooting](https://github.com/salopensource/sal/wiki/Troubleshooting) page, consider getting in touch via the [Google group](http://groups.google.com/group/sal-discuss), or heading over the the #slack channel on the [macadmins.org Slack](http://macadmins.org).
You should check out the [troubleshooting](https://github.com/salopensource/sal/wiki/Troubleshooting) page, consider getting in touch via the [Google group](http://groups.google.com/group/sal-discuss), or heading over the the #sal channel on the [macadmins.org Slack](http://macadmins.org).

## API

Expand Down
46 changes: 25 additions & 21 deletions api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,44 @@
from rest_framework import authentication
from rest_framework import exceptions
from rest_framework import permissions
from server.models import *

from server.models import ApiKey


class ApiKeyAuthentication(authentication.BaseAuthentication):
"""Use publickey/privatekey HTTP headers for authentication"""

def authenticate(self, request):
public_key = request.META.get('HTTP_PUBLICKEY', False)
private_key = request.META.get('HTTP_PRIVATEKEY', False)
if not public_key or not private_key:
if not any((public_key, private_key)):
return None

try:
api_key = ApiKey.objects.get(private_key=private_key, public_key=public_key)
api_key = ApiKey.objects.get(
private_key=private_key, public_key=public_key)
except ApiKey.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid API Key')

return (api_key, None)


class HasRWPermission(permissions.BasePermission):
"""
Only allows RW API keys to write
"""
"""Only allow Users with 'Global Admin' level or RW API keys."""

def has_object_permission(self, request, view, obj):
public_key = request.META.get('HTTP_PUBLICKEY', False)
private_key = request.META.get('HTTP_PRIVATEKEY', False)
if not public_key or not private_key:
def has_permission(self, request, view):
# Grant Sal 'Global Admin' Users access.
if isinstance(request.user, User):
return request.user.userprofile.level in ('GA',)
# Otherwise, all API tokens that have passed auth can perform
# 'safe' methods.
elif isinstance(request.user, ApiKey):
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to keys that have RW.
return request.user.read_write
else:
# If authentication fails, the user will be of type
# django.contrib.auth.models.AnonymousUser;
# reject anonymous users.
return False

try:
api_key = ApiKey.objects.get(private_key=private_key, public_key=public_key)
except ApiKey.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid API Key')
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to keys that have RW.
return api_key.read_write
18 changes: 18 additions & 0 deletions api/fixtures/business_unit_fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"model": "server.businessunit",
"pk": 1,
"fields": {
"name": "Gandalf",
"users": []
}
},
{
"model": "server.businessunit",
"pk": 2,
"fields": {
"name": "You don't want to know",
"users": []
}
}
]
20 changes: 20 additions & 0 deletions api/fixtures/conditions_fixture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"fields": {
"condition_data": "9",
"condition_name": "os_vers_minor",
"machine": 1
},
"model": "server.condition",
"pk": 1
},
{
"fields": {
"condition_data": "True",
"condition_name": "x86_64_capable",
"machine": 1
},
"model": "server.condition",
"pk": 13105433
}
]
20 changes: 20 additions & 0 deletions api/fixtures/fact_fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"fields": {
"fact_data": "464.79 GiB",
"fact_name": "mountpoints=>/=>size",
"machine": 1
},
"model": "server.fact",
"pk": 1
},
{
"fields": {
"fact_data": "1500",
"fact_name": "networking=>interfaces=>en0=>mtu",
"machine": 1
},
"model": "server.fact",
"pk": 151681726
}
]
30 changes: 30 additions & 0 deletions api/fixtures/inventory_fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"fields": {
"bundleid": "com.sheagcraig.tacofortress",
"bundlename": "TacoFortress",
"name": "TacoFortress"
},
"model": "inventory.application",
"pk": 1
},
{
"fields": {
"datestamp": "2018-01-24T17:32:36.519Z",
"machine": 1,
"sha256hash": "be6d4deb1308ea52ef4c27d2c1b6610c5dae18ddc497360a42e075b5f52697f4"
},
"model": "inventory.inventory",
"pk": 1
},
{
"fields": {
"application": 1,
"machine": 1,
"path": "/Applications/TacoFortress.app",
"version": "201.3.6"
},
"model": "inventory.inventoryitem",
"pk": 1
}
]
78 changes: 78 additions & 0 deletions api/fixtures/machine_fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
[
{
"model": "server.machine",
"pk": 1,
"fields": {
"machine_group": 1,
"serial": "C0DEADBEEF",
"hostname": "mla680",
"operating_system": "10.12",
"memory": "16 GB",
"memory_kb": 16777216,
"munki_version": "2.8.1.2845",
"manifest": "C0DEADBEEF",
"hd_space": "476429672",
"hd_total": "975912960",
"hd_percent": "51",
"console_user": "shcrai",
"machine_model": "MacBookPro11,5",
"machine_model_friendly": "MacBook Pro (Retina, 15-inch, Mid 2015)",
"cpu_type": "Intel Core i7",
"cpu_speed": "2.5 GHz",
"os_family": "Darwin",
"last_checkin": "2016-10-06T19:29:38.376Z",
"first_checkin": "2016-10-06T15:51:21.145Z",
"report": "",
"report_format": "base64bz2",
"errors": 4,
"warnings": 7,
"activity": "",
"puppet_version": "4.7.0",
"sal_version": "1.0.6",
"last_puppet_run": "2016-10-06T19:06:21Z",
"puppet_errors": 0,
"install_log_hash": "ed168dbcf43e8c89ec3bc1a5708a7210cfeeedf0fc4e8cbbda2e0b50af782e72",
"install_log": "",
"deployed": true,
"broken_client": false
}
},
{
"model": "server.machine",
"pk": 2,
"fields": {
"machine_group": 1,
"serial": "C1DEADBEEF",
"hostname": "mla806",
"operating_system": "10.12.5",
"memory": "16 GB",
"memory_kb": 16777216,
"munki_version": "2.8.2.2855",
"manifest": "C1DEADBEEF",
"hd_space": "536276480",
"hd_total": "975831040",
"hd_percent": "45",
"console_user": "shcrai",
"machine_model": "MacBookPro13,3",
"machine_model_friendly": "MacBook Pro (15-inch, 2016)",
"cpu_type": "Intel Core i7",
"cpu_speed": "2.7 GHz",
"os_family": "Darwin",
"last_checkin": "2017-05-16T19:04:51.336Z",
"first_checkin": "2017-05-16T19:04:51.728Z",
"report": "",
"report_format": "base64bz2",
"errors": 0,
"warnings": 3,
"activity": "",
"puppet_version": "4.10.1",
"sal_version": "2.0.3",
"last_puppet_run": "2017-05-16T18:54:56Z",
"puppet_errors": 0,
"install_log_hash": null,
"install_log": null,
"deployed": true,
"broken_client": false
}
}
]
20 changes: 20 additions & 0 deletions api/fixtures/machine_group_fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"model": "server.machinegroup",
"pk": 1,
"fields": {
"business_unit": 1,
"name": "Hai",
"key": "11c3qkyht9b88uja3i7v46rztrzejdgechnl8jw5fqv7z84vdgjynrn9czykctfi4quu4uvbwijbrentnmqepx9jw61avepnl2n8talsk37jnkm36tdvpra55311gi03"
}
},
{
"model": "server.machinegroup",
"pk": 2,
"fields": {
"business_unit": 1,
"name": "tacos",
"key": "tp702yfyw698e7dag786r0ejd99oywwj4ij7egbb4dqm5ghdj9e00zyygppx7okaa4p7vod1h0u7mzeau60yfcji6nnsvbz0y592subwigqa8q3wimebo63cs1d0yj71"
}
}
]
22 changes: 22 additions & 0 deletions api/fixtures/pending_apple_update_fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"fields": {
"display_name": "Safari",
"machine": 1,
"update": "Safari9.1Mavericks",
"update_version": "9.1"
},
"model": "server.pendingappleupdate",
"pk": 1
},
{
"fields": {
"display_name": "Remote Desktop Client Update",
"machine": 1,
"update": "RemoteDesktopClient",
"update_version": "3.8.5"
},
"model": "server.pendingappleupdate",
"pk": 1577299
}
]
22 changes: 22 additions & 0 deletions api/fixtures/pending_update_fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"fields": {
"display_name": "app_store_prefs-1.0.0",
"machine": 1,
"update": "app_store_prefs",
"update_version": "1.0.0"
},
"model": "server.pendingupdate",
"pk": 1
},
{
"fields": {
"display_name": "LyncSetup-1.2",
"machine": 1,
"update": "LyncSetup",
"update_version": "0.1.2"
},
"model": "server.pendingupdate",
"pk": 995286
}
]
25 changes: 25 additions & 0 deletions api/fixtures/plugin_script_fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
{
"fields": {
"historical": false,
"machine": 1,
"plugin": "Tacos",
"recorded": "2016-08-25T15:35:27.328Z"
},
"model": "server.pluginscriptsubmission",
"pk": 1
},
{
"fields": {
"pluginscript_data": "Enabled",
"pluginscript_data_date": null,
"pluginscript_data_int": 0,
"pluginscript_data_string": null,
"pluginscript_name": "Tacos",
"submission": 1,
"submission_and_script_name": "MachineDetailSecurity: Tacos"
},
"model": "server.pluginscriptrow",
"pk": 1
}
]
Loading