11import base64
22import hashlib
3+ import logging
34import os
45import subprocess
56import time
67
78import requests
89from pyotp import TOTP
910
11+ logger = logging .getLogger ("sync.bitwarden" )
1012
1113BITWARDEN_URL = os .environ .get ("BITWARDEN_URL" )
1214
@@ -31,24 +33,27 @@ def sync(teams, users):
3133 totp = TOTP (BITWARDEN_2FA_TOKEN )
3234
3335 # Configure bitwarden CLI
36+ logger .info ("Configuring Bitwarden CLI..." )
3437 subprocess .run (["bw" , "config" , "server" , BITWARDEN_URL ])
38+ logger .info ("Logging in to Bitwarden..." )
3539 subprocess .run (
3640 ["bw" , "login" , BITWARDEN_EMAIL , BITWARDEN_PASSWORD , "--code" , totp .now ()]
3741 )
3842 res = subprocess .run (
3943 ["bw" , "unlock" , BITWARDEN_PASSWORD , "--raw" ], capture_output = True
4044 )
4145 os .environ ["BW_SESSION" ] = res .stdout .decode ("utf-8" )
42- # Add newline to logs
43- print ()
4446
4547 # Log into admin interface, invite users, and check for 2FA
48+ logger .info ("Logging in to admin interface..." )
4649 session = requests .Session ()
4750 session .post (buildURL ("/admin/" ), {"token" : BITWARDEN_ADMIN_TOKEN })
4851 raw_users = session .get (buildURL ("/admin/users/" ))
4952
5053 # Generate Map of email to 2FA status
5154 twofac = {user ["Email" ]: user ["TwoFactorEnabled" ] for user in raw_users .json ()}
55+ twofac_enabled = sum (1 for v in twofac .values () if v )
56+ logger .info ("Found %d existing user(s), %d with 2FA enabled" , len (twofac ), twofac_enabled )
5257
5358 # Log into web vault
5459 res = session .post (
@@ -82,33 +87,37 @@ def sync(teams, users):
8287 body = res .json ()
8388 if "access_token" not in body :
8489 # Authentication failed. Most likely due to TOTP
85- print ( "Bitwarden: TOTP code invalid" )
90+ logger . error ( "Authentication failed - TOTP code invalid" )
8691 # Sleep to ensure next job uses a different TOTP code
8792 time .sleep (45 )
8893 exit (1 )
94+ logger .info ("Successfully authenticated to web vault" )
8995 session .headers .update ({"Authorization" : f"Bearer { body ['access_token' ]} " })
9096
9197 # Generate mapping of organization to uuid
9298 res = session .get (buildURL ("/api/sync?excludeDomains=true" ))
9399 body = res .json ()
94100 raw_orgs = body ["Profile" ]["Organizations" ]
95101 organizations = {org ["Name" ]: org ["Id" ] for org in raw_orgs }
102+ logger .info ("Found %d organization(s): %s" , len (organizations ), ", " .join (organizations .keys ()))
96103
97104 # Grant team leads access to bitwarden organizations
98105 for team in teams ["leads" ]:
99106 name = team .name [:- 6 ] # Strip Leads
100- invite_members (organizations [name ], team , users , session )
101- confirm_access (organizations [name ], twofac , session )
107+ logger .info ("Syncing leads team '%s' -> org '%s'" , team .name , name )
108+ invite_members (organizations [name ], name , team , users , session )
109+ confirm_access (organizations [name ], name , twofac , session )
102110
103111 # Grant directors access to director organization
104112 for team in teams ["directors" ]:
105113 name = team .name
106- invite_members (organizations [name ], team , users , session )
107- confirm_access (organizations [name ], twofac , session )
114+ logger .info ("Syncing directors team '%s' -> org '%s'" , team .name , name )
115+ invite_members (organizations [name ], name , team , users , session )
116+ confirm_access (organizations [name ], name , twofac , session )
108117 return
109118
110119
111- def invite_members (organization_uuid , team , users , session ):
120+ def invite_members (organization_uuid , org_name , team , users , session ):
112121 """
113122 Given a bitwarden organization and a GitHub team, invite all members of that team
114123 into the organization
@@ -119,6 +128,7 @@ def invite_members(organization_uuid, team, users, session):
119128 gh_id = member .login .lower ()
120129 if gh_id in users :
121130 email = users [gh_id ]["email" ]
131+ logger .info ("Inviting %s (%s) to org '%s'" , gh_id , email , org_name )
122132 # Make individual requests because bw errors out if an email is already invited
123133 data = {
124134 "emails" : [email ],
@@ -139,24 +149,32 @@ def invite_members(organization_uuid, team, users, session):
139149 "manageUsers" : False ,
140150 },
141151 }
142- session .post (
152+ res = session .post (
143153 buildURL (f"/api/organizations/{ organization_uuid } /users/invite" ),
144154 json = data ,
145155 )
156+ if res .ok :
157+ logger .info (" -> Invite sent successfully" )
158+ else :
159+ logger .warning (" -> Invite returned status %d (may already be invited)" , res .status_code )
160+ else :
161+ logger .warning ("Skipping %s (not found in roster)" , gh_id )
146162
147163
148- def confirm_access (organization_uuid , twofac , session ):
164+ def confirm_access (organization_uuid , org_name , twofac , session ):
149165 """
150166 Confirm all invited users to an organization if they have enabled 2Fa
151167 """
152168 res = session .get (buildURL (f"/api/organizations/{ organization_uuid } /users" ))
153169 body = res .json ()
170+ logger .info ("Checking %d user(s) in org '%s' for confirmation" , len (body ["Data" ]), org_name )
154171 for user in body ["Data" ]:
155172 # If user has 2FA enabled, confirm access
156173 email = user ["Email" ]
157174 id_ = user ["Id" ]
158175 accepted = user ["Status" ] == 1
159176 if accepted and email in twofac and twofac [email ]:
177+ logger .info ("Confirming access for %s in org '%s' (2FA enabled, invite accepted)" , email , org_name )
160178 # I can't figure out how to reverse engineer the confirm route after a user accepts
161179 # an invite. I know that we encrypt an "org key" using the invited user's public key
162180 # however, I'm not sure what that "org key" is. Looking at the official CLI, I think
@@ -172,6 +190,8 @@ def confirm_access(organization_uuid, twofac, session):
172190 organization_uuid ,
173191 ]
174192 )
193+ elif accepted and email in twofac and not twofac [email ]:
194+ logger .warning ("Skipping confirmation for %s in org '%s' (2FA not enabled)" , email , org_name )
175195
176196
177197# Crypto copied from
0 commit comments