Skip to content

Commit b05c160

Browse files
authored
Merge pull request carbonblack#81 from carbonblack/user-management-fixes
Add user operations example, Team model object. Fixes for User model object.
2 parents fa1f2c5 + 1b32b5c commit b05c160

File tree

8 files changed

+284
-5
lines changed

8 files changed

+284
-5
lines changed

docs/response-examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ Let's create a user on our Cb Response server::
402402
>>> user.password = "cbisawesome"
403403
>>> user.first_name = "Jason"
404404
>>> user.last_name = "Garman"
405+
>>> user.email = "jgarman@carbonblack.com"
405406
>>> user.teams = []
406407
>>> user.global_admin = False
407408
Creating a new User object

examples/response/user_operations.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python
2+
#
3+
4+
import sys
5+
from cbapi.response.models import User, Team, SensorGroup
6+
from cbapi.example_helpers import build_cli_parser, get_cb_response_object, get_object_by_name_or_id
7+
from cbapi.errors import ServerError
8+
import logging
9+
import getpass
10+
from cbapi.response.rest_api import get_api_token
11+
12+
13+
log = logging.getLogger(__name__)
14+
15+
16+
def list_users(cb, parser, args):
17+
format_string = "{:16s} {:30s} {:30s} {:40s}"
18+
print(format_string.format("Username", "Name", "Email address", "Teams"))
19+
print(format_string.format("-"*16, "-"*30, "-"*30, "-"*40))
20+
21+
for u in cb.select(User):
22+
print(format_string.format(u.username, " ".join([u.first_name, u.last_name]), u.email,
23+
", ".join([t['name'] for t in u.teams])))
24+
25+
26+
def list_teams(cb, parser, args):
27+
for t in cb.select(Team):
28+
print("Team {0} (id {1}):".format(t.name, t.id))
29+
for ga in t.group_access:
30+
print(" {0} on Sensor Group \"{1}\"".format(ga["access_category"], ga["group_name"]))
31+
32+
33+
def add_team(cb, parser, args):
34+
t = cb.create(Team)
35+
t.name = args.name
36+
37+
for sg in args.administrator or []:
38+
if isinstance(t, int):
39+
sg = cb.select(SensorGroup, sg)
40+
else:
41+
sg = cb.select(SensorGroup).where("name:{0}".format(sg)).first()
42+
43+
t.add_administrator_access(sg)
44+
45+
for sg in args.viewer or []:
46+
if isinstance(t, int):
47+
sg = cb.select(SensorGroup, sg)
48+
else:
49+
sg = cb.select(SensorGroup).where("name:{0}".format(sg)).first()
50+
51+
t.add_viewer_access(sg)
52+
53+
try:
54+
t.save()
55+
except ServerError as se:
56+
print("Could not add team: {0:s}".format(str(se)))
57+
except Exception as e:
58+
print("Could not add team: {0:s}".format(str(e)))
59+
else:
60+
log.debug("team data: {0:s}".format(str(t)))
61+
print("Added team {0}.".format(t.name))
62+
63+
64+
def add_user(cb, parser, args):
65+
u = cb.create(User)
66+
u.username = args.username
67+
u.first_name = args.first_name
68+
u.last_name = args.last_name
69+
u.email = args.email
70+
u.teams = []
71+
u.global_admin = args.global_admin
72+
73+
log.debug("Adding user: {0:s}".format(u.username))
74+
75+
if not args.password:
76+
passwords_dont_match = True
77+
while passwords_dont_match:
78+
pw1 = getpass.getpass("New password for {0}: ".format(u.username))
79+
pw2 = getpass.getpass("Re-enter password: ")
80+
if pw1 == pw2:
81+
passwords_dont_match = False
82+
else:
83+
print("Passwords don't match; try again")
84+
85+
u.password = pw1
86+
else:
87+
u.password = args.password
88+
89+
for t in args.team or []:
90+
if isinstance(t, int):
91+
t = cb.select(Team, t)
92+
else:
93+
t = cb.select(Team).where("name:{0}".format(t)).first()
94+
95+
u.add_team(t)
96+
97+
try:
98+
u.save()
99+
except ServerError as se:
100+
print("Could not add user: {0:s}".format(str(se)))
101+
except Exception as e:
102+
print("Could not add user: {0:s}".format(str(e)))
103+
else:
104+
log.debug("user data: {0:s}".format(str(u)))
105+
print("Added user {0}.".format(u.username))
106+
107+
108+
def get_api_key(cb, parser, args):
109+
if not args.password:
110+
password = getpass.getpass("Password for {0}: ".format(args.username))
111+
else:
112+
password = args.password
113+
114+
print("API token for user {0}: {1}".format(args.username, get_api_token(cb.credentials.url,
115+
args.username, password,
116+
verify=cb.credentials.ssl_verify)))
117+
118+
119+
def delete_user(cb, parser, args):
120+
user = cb.select(User, args.username)
121+
try:
122+
user.delete()
123+
except Exception as e:
124+
print("Could not delete user {0:s}: {1:s}".format(args.username, str(e)))
125+
else:
126+
print("Deleted user {0:s}".format(args.username))
127+
128+
129+
def main():
130+
parser = build_cli_parser()
131+
commands = parser.add_subparsers(help="User commands", dest="command_name")
132+
133+
list_command = commands.add_parser("list", help="List all configured users")
134+
list_teams_command = commands.add_parser("list-teams", help="List all configured user teams")
135+
136+
add_command = commands.add_parser("add", help="Add new user")
137+
add_command.add_argument("-u", "--username", help="New user's username", required=True)
138+
add_command.add_argument("-f", "--first-name", help="First name", required=True)
139+
add_command.add_argument("-l", "--last-name", help="Last name", required=True)
140+
add_command.add_argument("-p", "--password", help="Password - if not specified, prompt at runtime", required=False)
141+
add_command.add_argument("-e", "--email", help="Email address", required=True)
142+
add_command.add_argument("-A", "--global-admin", help="Make new user global admin", default=False,
143+
action="store_true")
144+
add_command.add_argument("-t", "--team", help="Add new user to this team (can specify multiple teams)",
145+
action="append", metavar="TEAM-NAME")
146+
147+
add_team_command = commands.add_parser("add-team", help="Add new team")
148+
add_team_command.add_argument("-N", "--name", help="Name of the new team")
149+
add_team_command.add_argument("-A", "--administrator", help="Add administrator rights to the given sensor group",
150+
metavar="SENSOR-GROUP", action="append")
151+
add_team_command.add_argument("-V", "--viewer", help="Add viewer rights to the given sensor group",
152+
metavar="SENSOR-GROUP", action="append")
153+
154+
get_api_key_command = commands.add_parser("get-api-key", help="Get API key for user")
155+
get_api_key_command.add_argument("-u", "--username", help="Username", required=True)
156+
get_api_key_command.add_argument("-p", "--password", help="Password - if not specified, prompt at runtime",
157+
required=False)
158+
159+
del_command = commands.add_parser("delete", help="Delete user")
160+
del_user_specifier = del_command.add_mutually_exclusive_group(required=True)
161+
del_user_specifier.add_argument("-u", "--username", help="Name of user to delete.")
162+
163+
args = parser.parse_args()
164+
cb = get_cb_response_object(args)
165+
166+
if args.command_name == "list":
167+
return list_users(cb, parser, args)
168+
elif args.command_name == "list-teams":
169+
return list_teams(cb, parser, args)
170+
elif args.command_name == "get-api-key":
171+
return get_api_key(cb, parser, args)
172+
elif args.command_name == "add":
173+
return add_user(cb, parser, args)
174+
elif args.command_name == "add-team":
175+
return add_team(cb, parser, args)
176+
elif args.command_name == "delete":
177+
return delete_user(cb, parser, args)
178+
179+
180+
if __name__ == "__main__":
181+
sys.exit(main())

src/cbapi/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def _join(self, join_cls, field_name):
312312
class MutableBaseModel(NewBaseModel):
313313
_new_object_http_method = "POST"
314314
_change_object_http_method = "PUT"
315+
_new_object_needs_primary_key = False
315316

316317
def __setattr__(self, attrname, val):
317318
# allow subclasses to define their own property setters
@@ -362,7 +363,8 @@ def _update_object(self):
362363
if self.__class__.primary_key in self._dirty_attributes.keys() or self._model_unique_id is None:
363364
new_object_info = deepcopy(self._info)
364365
try:
365-
del(new_object_info[self.__class__.primary_key])
366+
if not self._new_object_needs_primary_key:
367+
del(new_object_info[self.__class__.primary_key])
366368
except Exception:
367369
pass
368370
log.debug("Creating a new {0:s} object".format(self.__class__.__name__))

src/cbapi/query.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __iter__(self):
5353

5454

5555
class SimpleQuery(BaseQuery):
56-
def __init__(self, cls, cb, urlobject=None):
56+
def __init__(self, cls, cb, urlobject=None, returns_fulldoc=True):
5757
super(SimpleQuery, self).__init__()
5858

5959
self._doc_class = cls
@@ -66,6 +66,7 @@ def __init__(self, cls, cb, urlobject=None):
6666
self._results = []
6767
self._query = {}
6868
self._sort_by = None
69+
self._returns_full_doc = returns_fulldoc
6970

7071
def _clone(self):
7172
nq = self.__class__(self._doc_class, self._cb)
@@ -99,7 +100,7 @@ def results(self):
99100
if not self._full_init:
100101
self._results = []
101102
for item in self._cb.get_object(self._urlobject, default=[]):
102-
t = self._doc_class.new_object(self._cb, item, full_doc=True)
103+
t = self._doc_class.new_object(self._cb, item, full_doc=self._returns_full_doc)
103104
if self._match_query(t):
104105
self._results.append(t)
105106
self._results = self._sort(self._results)

src/cbapi/response/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
from .models import (
66
BannedHash, Site, ThrottleRule, Alert, Feed, Sensor, User, Watchlist, Investigation, ThreatReport, Binary, Process,
7-
SensorGroup, FeedAction, WatchlistAction, TaggedEvent, IngressFilter, StoragePartition
7+
SensorGroup, FeedAction, WatchlistAction, TaggedEvent, IngressFilter, StoragePartition, Team
88
)
99
from .rest_api import CbEnterpriseResponseAPI, CbResponseAPI

src/cbapi/response/models.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from contextlib import closing
1414
import struct
1515
from cbapi.six.moves import urllib
16+
from copy import deepcopy
1617
import cbapi.six as six
1718
import logging
1819
import time
@@ -844,10 +845,38 @@ def results(self):
844845
return self._results
845846

846847

848+
class Team(MutableBaseModel, CreatableModelMixin):
849+
swagger_meta_file = "response/models/team.yaml"
850+
urlobject = "/api/team"
851+
852+
@classmethod
853+
def _query_implementation(cls, cb):
854+
return SimpleQuery(cls, cb, urlobject="/api/teams", returns_fulldoc=False)
855+
856+
def _add_access(self, sg, access_type):
857+
if isinstance(sg, int):
858+
sg = self._cb.select(SensorGroup, sg)
859+
860+
new_access = [ga for ga in self.group_access if ga.get("group_id") != sg.id]
861+
new_access.append({
862+
"group_id": sg.id,
863+
"access_category": access_type,
864+
"group_name": sg.name
865+
})
866+
self.group_access = new_access
867+
868+
def add_viewer_access(self, sg):
869+
return self._add_access(sg, "Viewer")
870+
871+
def add_administrator_access(self, sg):
872+
return self._add_access(sg, "Administrator")
873+
874+
847875
class User(MutableBaseModel, CreatableModelMixin):
848876
swagger_meta_file = "response/models/user.yaml"
849877
urlobject = "/api/user"
850878
primary_key = "username"
879+
_new_object_needs_primary_key = True
851880

852881
@classmethod
853882
def _query_implementation(cls, cb):
@@ -862,6 +891,45 @@ def _retrieve_cb_info(self):
862891
info["id"] = self._model_unique_id
863892
return info
864893

894+
def _update_object(self):
895+
if self._cb.cb_server_version < LooseVersion("6.1.0"):
896+
# only include IDs of the teams and not the entire dictionary
897+
if self.__class__.primary_key in self._dirty_attributes.keys() or self._model_unique_id is None:
898+
new_object_info = deepcopy(self._info)
899+
try:
900+
del(new_object_info["id"])
901+
except KeyError:
902+
pass
903+
new_teams = [t.get("id") for t in new_object_info["teams"]]
904+
new_teams = [t for t in new_teams if t]
905+
new_object_info["teams"] = new_teams
906+
907+
try:
908+
if not self._new_object_needs_primary_key:
909+
del (new_object_info[self.__class__.primary_key])
910+
except Exception:
911+
pass
912+
log.debug("Creating a new {0:s} object".format(self.__class__.__name__))
913+
ret = self._cb.api_json_request(self.__class__._new_object_http_method, self.urlobject,
914+
data=new_object_info)
915+
else:
916+
log.debug(
917+
"Updating {0:s} with unique ID {1:s}".format(self.__class__.__name__, str(self._model_unique_id)))
918+
ret = self._cb.api_json_request(self.__class__._change_object_http_method,
919+
self._build_api_request_uri(), data=self._info)
920+
921+
return self._refresh_if_needed(ret)
922+
else:
923+
return super(User, self)._update_object()
924+
925+
def add_team(self, t):
926+
if isinstance(t, int):
927+
t = self._cb.select(Team, t)
928+
929+
new_teams = [team for team in self.teams if team.get("id") != t.id]
930+
new_teams.append({"id": t.id, "name": t.name})
931+
self.teams = new_teams
932+
865933

866934
class Watchlist(MutableBaseModel, CreatableModelMixin):
867935
swagger_meta_file = "response/models/watchlist-new.yaml"

src/cbapi/response/models/team.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
type: object
2+
required:
3+
- name
4+
- group_access
5+
properties:
6+
id:
7+
type: integer
8+
description: 'User team id'
9+
name:
10+
type: string
11+
description: 'User team name'
12+
group_access:
13+
type: array
14+
description: "Sensor group permissions for this team"
15+
items:
16+
title: teamPermission
17+
type: object
18+
properties:
19+
group_id:
20+
type: integer
21+
description: ''
22+
group_name:
23+
type: string
24+
description: ''
25+
access_category:
26+
type: string
27+
description: ''

src/cbapi/response/models/user.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ required:
33
- username
44
- last_name
55
- first_name
6-
- password
76
- email
87
- teams
98
- global_admin

0 commit comments

Comments
 (0)