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

Bitwarden access manager #10

Merged
merged 42 commits into from
Apr 4, 2023
Merged

Bitwarden access manager #10

merged 42 commits into from
Apr 4, 2023

Conversation

yannickhilber
Copy link
Contributor

Description

Add script to retrieve Bitwarden configuration using API and to compare with a configuration file.

The script reuse the logic of github_access_manager script and some parts can be share between the two script. This will be perform in another PR.

Note for the reviewer

  • member attribute access_all is set to true only when a member has the role type owner
  • member attribute type refers to the member role: 0 = user, 1 = owner, 2 = admin, 3 = manager, 4 = custom (when writing this, I realize that I forgot to add a logic to make it readable in the toml config. I will add it later)
  • group attribute access_all is set to true only when Grant access to all current and future collections box is tick.
    image
  • GroupCollectionAccess read_only attribute is set to true when the permission Can view or Can view, except password are set. This is set to false when the permission Can edit or Can edit, except password are set. The API doesn't do the distinction Can ... and Can ..., except password permissions.
  • The API doesn't allow to retrieve the collection name, we need to set external ID in the UI to retrieve his name
    image
Example output
The following collections on Bitwarden need to be changed to match organization.toml:

  [[collection]]
  collection_id = 8e69ce49-85ae-4e09-a52c-afaf00a90a3f
  external_id = ""
  member_access = [
    { member_id = 2564c11f-fc1b-4ec7-aa0b-afaf00a9e4a4, member_name = "yan", role = "c6a13b93-edc1-4c3b-9fc5-afaf00a8d33f" },
  ]
  group_access = [
-   { group_id = c6a13b93-edc1-4c3b-9fc5-afaf00a8d33f, group_name = "group1", readOnly = "True" },
+   { group_id = c6a13b93-edc1-4c3b-9fc5-afaf00a8d33f, group_name = "group2", readOnly = "True" },
  ]

The following members on Bitwarden need to be changed to match organization.toml:

  [[member]]
  member_id = 2564c11f-fc1b-4ec7-aa0b-afaf00a9e4a4
  member_name = yan
- email = [email protected]
- type = 3
+ email = [email protected]
+ type = 2
  access_all = False

The following groups on Bitwarden need to be changed to match organization.toml:

  [[group]]
  group_id = c6a13b93-edc1-4c3b-9fc5-afaf00a8d33f
  group_name = group1
- access_all = False
+ access_all = True

The following members of group 'group1' are specified in organization.toml, but are not present on Bitwarden:

  yunkel

Issue chorus-infrastructure #3941

Copy link
Contributor

@ruuda ruuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the docstring 😅 Will continue the review next week!

bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
Copy link
Contributor

@ruuda ruuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finally reviewed the full thing now. Overall it looks really nice! I left many comments, but most of them are fairly superficial Python things.

I mostly looked superficially at the implementation now that I did not try to make sense of the diffing logic on a high level, but that is very similar to what we already have for GitHub, so if the data structures are good for Bitwarden, it should work. I’ll do a more high-level review later then, we can focus on the Python things first.

Unrelated to the content of the PR, but you ran into both mypy and black and I realize I didn’t document this at all, it would be nice to mention these in the readme or in a CONTRIBUTING.md.

bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Show resolved Hide resolved
email: str
type: str
access_all: Optional[bool]
# groups: Tuple[str, ...]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Note to self, we need to make a decision on this before merging.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the docstring, the group should go in the member, right? Then this should be uncommented, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the docstring, the group should go in the member, right? Then this should be uncommented, no?

Copy link
Contributor Author

@yannickhilber yannickhilber Mar 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the group should go there in order to keep track of member assigned group in the toml file.

As we go with this approach a member not part of a group will be reported in the user diff and we don't need a specific group_diff to check if a member is whether part of a group of not.

I implemented the change in b0e8a2f

bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Show resolved Hide resolved
bitwarden_access_manager.py Show resolved Hide resolved
@ruuda ruuda self-requested a review March 6, 2023 12:23
f"member_id = {self.id}",
f"member_name = {self.name}",
f"email = {self.email}",
f"type = {str(self.type)}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still unresolved.

bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
@yannickhilber
Copy link
Contributor Author

This is still unresolved.

What is unresolved here ?

@ruuda
Copy link
Contributor

ruuda commented Mar 17, 2023

What is unresolved here ?

This: https://github.com/ChorusOne/github-access-manager/pull/10/files#r1128160013. If you go to the “files changed” tab you can see the full context, from the “conversation” tab sometimes things are missing.

@yannickhilber
Copy link
Contributor Author

This: https://github.com/ChorusOne/github-access-manager/pull/10/files#r1128160013. If you go to the “files changed” tab you can see the full context, from the “conversation” tab sometimes things are missing.

Thanks, I will check with the full context next time !

Copy link
Contributor

@ruuda ruuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s almost good! I have a few more superficial comments about the Python, but the structure of the program looks good.

bitwarden_access_manager.py Outdated Show resolved Hide resolved
email: str
type: str
access_all: Optional[bool]
# groups: Tuple[str, ...]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the docstring, the group should go in the member, right? Then this should be uncommented, no?

email: str
type: str
access_all: Optional[bool]
# groups: Tuple[str, ...]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the docstring, the group should go in the member, right? Then this should be uncommented, no?

bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
def get_group_members(self, id: str, name: str) -> Iterable[GroupMember]:
members = json.load(self._http_get(f"/public/groups/{id}/member-ids"))

for member in members:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we will request every member once for every group that they are part of. It’s not a huge problem, but we could reduce the number of API calls by keeping a dict of member id to member, and only requesting the member if we haven’t requested it before.

But this PR is already open for a long time, let’s not do that right now, we can do it as a follow-up.

bitwarden_access_manager.py Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
When a member is not part of a group, it will be reflected in Member
diff. Therefore group diff is not necessary and will result in
duplicated information. group_diff is removed.
Copy link
Contributor

@ruuda ruuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me! I have a few more small Python comments, but at this point it’s only nitpicks, I think the structure of the program looks good, and the access checks all look correct too!

I’ll try and run this on our organization and report back if I run into any issues.

bitwarden_access_manager.py Outdated Show resolved Hide resolved
Comment on lines 162 to 167
groups = self.groups or ()
if len(groups) > 0:
groups_str = ", ".join(f'"{g}"' for g in sorted(groups))
result = result + "groups = [ " + groups_str + " ]"
else:
result = result + "groups = []"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
groups = self.groups or ()
if len(groups) > 0:
groups_str = ", ".join(f'"{g}"' for g in sorted(groups))
result = result + "groups = [ " + groups_str + " ]"
else:
result = result + "groups = []"
result = result + "groups = [" + ", ".join(json.dumps(g) for g in sorted(groups)) + "]"

groups is no longer Optional, so we don’t need the or (). Also, I don’t see a reason to have the spaces after [ and before ] if the list is non-empty, if we omit those, then the case distinction goes away.

Copy link
Contributor Author

@yannickhilber yannickhilber Mar 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to convert g into a json string with json.dumps ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just surround g by quotes, then if the string g contains a " or a \, it will produce invalid toml. If we use json.dumps to format a json string literal, then it will escape " and \ to \" and \\. This works because toml has the same escaping rules as json.

bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Show resolved Hide resolved
bitwarden_access_manager.py Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
bitwarden_access_manager.py Outdated Show resolved Hide resolved
@ruuda
Copy link
Contributor

ruuda commented Mar 30, 2023

Ah small thing, can you chmod +x bitwarden_access_manager.py?


@staticmethod
def new(client_id: str, client_secret: str) -> BitwardenClient:
response = requests.post(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed now that I try to run this ... this is using the third-party library requests. We don’t need it to make requests, we already import http.client from the standard library, and we even construct the client already. You can see here how we do this in the GitHub version:

self.connection.request(
method="GET",
url=url,
headers={
"Authorization": f"token {self.github_token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "Github Access Manager",
},
)
# TODO: Respect these response headers
# X-RateLimit-Limit: 5000
# X-RateLimit-Remaining: 4994
# X-RateLimit-Reset: 1653495296
# X-RateLimit-Used: 6
# X-RateLimit-Resource: core
return self.connection.getresponse()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can push a commit to fix this, I’m looking into it anyway.

data={
"grant_type": "client_credentials",
"scope": "api.organization",
"Accept": "application/json",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the Accept really go here? This is an http header, shouldn’t it go in headers?

* The access_all key for members and groups is optional, default is false.
* The member_access key for a collection only list members with direct access
to collection. It omits direct access for members with the role
owners or admins because they have implicit access to all collections.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out, this is not true, I thought it was because I could access everything in our organization, but it was because access_all was true for me (which I discovered using this tool :D). So could we remove this special case?

@yannickhilber yannickhilber merged commit fbda58b into master Apr 4, 2023
@yannickhilber yannickhilber deleted the bitwarden_access_manager branch April 4, 2023 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants