Skip to content

Commit 20fdca6

Browse files
committed
Merge commit
GitOrigin-RevId: 0905d6d383e9c5c4672b97827c7d11cc0d70d344
1 parent 51478ac commit 20fdca6

File tree

5 files changed

+243
-1
lines changed

5 files changed

+243
-1
lines changed

example/okta-sync/matchers.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
groups:
3+
-
4+
name: db/mongo
5+
resources:
6+
- type:mongo name:don*
7+
- type:ssh name:dev*
8+
-
9+
name: app/web
10+
resources:
11+
- type:ssh name:dev-web*

example/okta-sync/okta-sync.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import yaml
2+
import os
3+
import strongdm
4+
from okta.framework.ApiClient import ApiClient
5+
from okta.framework.Utils import Utils
6+
from okta.models.user.User import User
7+
from okta.models.usergroup.UserGroup import UserGroup
8+
9+
def load_matchers():
10+
f = open('matchers.yml')
11+
data = yaml.load(f, Loader = yaml.Loader)
12+
return data
13+
14+
class OktaUser:
15+
def __init__(self, login, first_name, last_name, groups):
16+
self.login = login
17+
self.first_name = first_name
18+
self.last_name = last_name
19+
self.groups = groups
20+
def __repr__(self):
21+
return "%s %s"%(self.login,self.groups)
22+
23+
def load_okta_users():
24+
ret = []
25+
apiClient = ApiClient(pathname='/api/v1/users', base_url=os.getenv('OKTA_CLIENT_ORGURL'), api_token=os.getenv('OKTA_CLIENT_TOKEN'))
26+
params = {
27+
'search': "profile.department eq \"Engineering\" and (status eq \"ACTIVE\")"
28+
}
29+
response = ApiClient.get_path(apiClient, '/', params=params)
30+
users = Utils.deserialize(response.text, User)
31+
for u in users:
32+
response = ApiClient.get_path(apiClient, '/{}/groups'.format(u.id))
33+
userGroups = Utils.deserialize(response.text, UserGroup)
34+
groups = []
35+
for ug in userGroups:
36+
groups.append(ug.profile.name)
37+
oktaUser = OktaUser(u.profile.login, u.profile.firstName, u.profile.lastName, groups)
38+
ret.append(oktaUser)
39+
return ret
40+
41+
def main():
42+
try:
43+
okta_sync()
44+
except Exception as ex:
45+
print("okta sync failed:"+str(ex))
46+
47+
def okta_sync():
48+
SDM_API_ACCESS_KEY = os.getenv('SDM_API_ACCESS_KEY')
49+
SDM_API_SECRET_KEY = os.getenv('SDM_API_SECRET_KEY')
50+
OKTA_CLIENT_TOKEN = os.getenv('OKTA_CLIENT_TOKEN')
51+
OKTA_CLIENT_ORGURL = os.getenv('OKTA_CLIENT_ORGURL')
52+
53+
if SDM_API_ACCESS_KEY is None or SDM_API_SECRET_KEY is None \
54+
or OKTA_CLIENT_TOKEN is None or OKTA_CLIENT_ORGURL is None:
55+
print("SDM_API_ACCESS_KEY, SDM_API_SECRET_KEY, OKTA_CLIENT_TOKEN, and OKTA_CLIENT_ORGURL must be set")
56+
return
57+
58+
matchers = load_matchers()
59+
okta_users = load_okta_users()
60+
61+
client = strongdm.Client(SDM_API_ACCESS_KEY, SDM_API_SECRET_KEY)
62+
63+
accounts = {o.email:o.id for o in client.accounts.list("")}
64+
permissions = [v for v in client.account_grants.list("")]
65+
66+
# define current state
67+
current = {}
68+
for p in permissions:
69+
if p.account_id not in current:
70+
current[p.account_id] = set()
71+
current[p.account_id].add((p.resource_id,p.id))
72+
73+
# define desired state
74+
desired = {}
75+
overlapping = 0
76+
for group in matchers["groups"]:
77+
for resourceQuery in group["resources"]:
78+
for res in client.resources.list(resourceQuery):
79+
for u in okta_users:
80+
if group["name"] in u.groups:
81+
if u.login not in accounts:
82+
continue
83+
overlapping+=1
84+
aid = accounts[u.login]
85+
if aid not in desired:
86+
desired[aid] = set()
87+
desired[aid].add(res.id)
88+
89+
# revoke things
90+
revocations = 0
91+
for aid,curRes in current.items():
92+
desRes = desired.get(aid,set())
93+
for rid in curRes:
94+
if rid[0] not in desRes:
95+
revocations+=1
96+
client.account_grants.delete(rid[1])
97+
98+
# grant things
99+
grants = 0
100+
for aid,desRes in desired.items():
101+
curRes = current.get(aid,set())
102+
for rid in desRes:
103+
for cr in curRes:
104+
if rid != cr[0]:
105+
grants+=1
106+
client.account_grants.create(strongdm.AccountGrant(resource_id=rid, account_id=aid))
107+
108+
print("{} Okta users, {} strongDM users, {} overlapping users, {} grants, {} revocations".format(\
109+
len(okta_users),len(accounts), overlapping, grants, revocations))
110+
111+
if __name__ == '__main__':
112+
main()

example/okta-sync/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
strongdm
2+
okta

example/panicButton.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright 2020 StrongDM Inc
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
import sys
16+
import os.path
17+
import pprint
18+
import time
19+
import json
20+
import os
21+
import strongdm
22+
23+
# panicButton.py suspends all users except for one admin,
24+
# in the fake use case of a critical break in or something
25+
# usage:
26+
# python3 panicButton.py [email protected]
27+
# to revert back to pre-panic state:
28+
# python3 panicButton.py revert
29+
def main():
30+
access_key = os.getenv("SDM_API_ACCESS_KEY")
31+
secret_key = os.getenv("SDM_API_SECRET_KEY")
32+
if access_key is None or secret_key is None:
33+
print("SDM_API_ACCESS_KEY and SDM_API_SECRET_KEY must be provided")
34+
return 1
35+
36+
client = strongdm.Client(access_key, secret_key)
37+
38+
if len(sys.argv) == 2 and sys.argv[1] == "revert":
39+
with open('state.json', 'r') as infile:
40+
state = json.load(infile)
41+
42+
reinstated_count = 0
43+
44+
users = client.accounts.list('')
45+
for user in users:
46+
if not user.suspended:
47+
continue
48+
reinstated_count += 1
49+
user.suspended = False
50+
client.accounts.update(user)
51+
for attachment in state['attachments']:
52+
try:
53+
client.account_attachments.create(strongdm.AccountAttachment(account_id=attachment["account_id"],role_id=attachment["role_id"]))
54+
except strongdm.errors.AlreadyExistsError:
55+
pass
56+
except Exception as ex:
57+
print("skipping creation of attachment due to error: ", str(ex))
58+
for grant in state['grants']:
59+
try:
60+
client.account_grants.create(strongdm.AccountGrant(account_id=grant["account_id"],resource_id=grant["resource_id"]))
61+
except strongdm.errors.AlreadyExistsError:
62+
pass
63+
except Exception as ex:
64+
print("skipping creation of grant due to error: ", str(ex))
65+
print("reinstated " + str(reinstated_count) + " users")
66+
print("recreated " + str(len(state['attachments'])) + " account attachments")
67+
print("recreated " + str(len(state['grants'])) + " account grants")
68+
return
69+
70+
admin_email = ""
71+
if len(sys.argv) == 2:
72+
admin_email = sys.argv[1]
73+
else:
74+
print("please provide an admin email to preserve")
75+
return 1
76+
77+
admin_user_id = ""
78+
users = client.accounts.list('email:?', admin_email)
79+
for user in users:
80+
admin_user_id = user.id
81+
82+
account_attachments = client.account_attachments.list('')
83+
account_grants = client.account_grants.list('')
84+
85+
state = {
86+
'attachments': [{"account_id":x.account_id,"role_id":x.role_id} for x in account_attachments if x.account_id != admin_user_id],
87+
'grants': [{"account_id":x.account_id,"resource_id":x.resource_id} for x in account_grants if x.account_id != admin_user_id and x.valid_until is None],
88+
}
89+
90+
print("storing " + str(len(state['attachments'])) + " account attachments in state")
91+
print("storing " + str(len(state['grants'])) + " account grants in state")
92+
93+
with open('state.json', 'w') as outfile:
94+
json.dump(state, outfile)
95+
96+
97+
suspended_count = 0
98+
users = client.accounts.list('')
99+
for user in users:
100+
if isinstance(user, strongdm.User) and user.email == admin_email:
101+
continue
102+
user.suspended = True
103+
try:
104+
client.accounts.update(user)
105+
suspended_count += 1
106+
except Exception as ex:
107+
print("skipping user " + user.id + " on account of error: " + str(ex))
108+
109+
110+
print("suspended " + str(suspended_count) + " users ")
111+
112+
113+
if __name__ == "__main__":
114+
main()

strongdm/plumbing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ def quote_filter_args(filter, *args):
5151

5252

5353
def timestamp_to_porcelain(t):
54-
return t.ToDatetime().replace(tzinfo=datetime.timezone.utc)
54+
ts = t.ToDatetime().replace(tzinfo=datetime.timezone.utc)
55+
if ts == datetime.datetime(1970,1,1,0,0,0,0,datetime.timezone.utc):
56+
return None
57+
return ts
5558

5659

5760
def timestamp_to_plumbing(t):

0 commit comments

Comments
 (0)