diff --git a/lib/entitlements/backend/github_org.rb b/lib/entitlements/backend/github_org.rb index dc73152..1787cd9 100644 --- a/lib/entitlements/backend/github_org.rb +++ b/lib/entitlements/backend/github_org.rb @@ -12,7 +12,8 @@ class GitHubOrg ORGANIZATION_ROLES = { "admin" => "ADMIN", # `billing-manager` is currently not supported - "member" => "MEMBER" + "member" => "MEMBER", + "security_manager" => "SECURITY-MANAGER" } # Error classes diff --git a/lib/entitlements/backend/github_org/provider.rb b/lib/entitlements/backend/github_org/provider.rb index 7f5de3b..f07db1f 100644 --- a/lib/entitlements/backend/github_org/provider.rb +++ b/lib/entitlements/backend/github_org/provider.rb @@ -104,7 +104,15 @@ def role_name(role_identifier) # Returns an Entitlements::Models::Group object. Contract String => Entitlements::Models::Group def role_to_group(role) - members = github.org_members.keys.select { |username| github.org_members[username] == role } + # The security_manager role is a special case because it is not a role that is + # part of the org membership API. Instead, it is a role that is assigned to users via + # the org role API. + if role == "security_manager" + members = github.users_with_role(role) + else + members = github.org_members.keys.select { |username| github.org_members[username] == role } + end + Entitlements::Models::Group.new( dn: role_dn(role), members: Set.new(members), diff --git a/lib/entitlements/backend/github_org/service.rb b/lib/entitlements/backend/github_org/service.rb index 0e7e760..8ad77ae 100644 --- a/lib/entitlements/backend/github_org/service.rb +++ b/lib/entitlements/backend/github_org/service.rb @@ -35,6 +35,23 @@ def sync(implementation, role) private + Contract String, String => C::HashOf[Symbol, C::Any] + def add_user_to_role(user, role) + if role == "security_manager" + octokit.add_role_to_user(user, role) + + # This is a hack to get around the fact that the GitHub API + # has two different concepts of organization roles, + # and the one we want to use is not present in organization memberships. + # + # If we get here, we know that the user is already member of the organization, + # and we know that the user has been successfully granted the role. + { user:, role:, state: "active" } + else + octokit.update_organization_membership(org, user:, role:) + end + end + # Upsert a user with a role to the organization. # # user: A String with the (GitHub) username of the person to add or modify. @@ -46,10 +63,21 @@ def add_user_to_organization(user, role) Entitlements.logger.debug "#{identifier} add_user_to_organization(user=#{user}, org=#{org}, role=#{role})" begin - new_membership = octokit.update_organization_membership(org, user:, role:) + new_membership = add_role_to_user(user, role) rescue Octokit::NotFound => e raise e unless ignore_not_found + Entitlements.logger.warn "User #{user} not found in GitHub instance #{identifier}, ignoring." + return false + rescue Octokit::UnprocessableEntity => e + # Two conditions can cause this: + # - If the role is not enabled, we'll get a 422. + # - If the user is not a member of the organization, we'll get a 422. + + # We'll loop this under ignore_not_found + # since this affects the case where we want to add a user to security_manager role + raise e unless ignore_not_found + Entitlements.logger.warn "User #{user} not found in GitHub instance #{identifier}, ignoring." return false end diff --git a/lib/entitlements/service/github.rb b/lib/entitlements/service/github.rb index 1bfd5a1..c9e52b8 100644 --- a/lib/entitlements/service/github.rb +++ b/lib/entitlements/service/github.rb @@ -394,6 +394,32 @@ def max_graphql_results MAX_GRAPHQL_RESULTS end # :nocov: + + Contract C::None => C::ArrayOf[Hash] + def org_roles + Entitlements.cache[:github_org_roles][org_signature] ||= begin + octokit.get("/orgs/#{@org}/organization-roles") + end + end + + Contract String => C::Maybe[Hash] + def org_role(role) + org_roles.find { |r| r[:name] == role } + end + + Contract String, String => C::Any + def add_role_to_user(user, role) + role_id = org_role(role)[:id] + octokit.put("/orgs/#{org_name}/organization-roles/users/#{user}/#{role_id}") + end + + Contract String => C::ArrayOf[Hash] + def users_with_role(role) + role_id = org_role(role)[:id] + Entitlements.cache[:github_org_role_users][org_signature][role] ||= begin + octokit.get("/orgs/#{org_name}/organization-roles/#{role_id}/users") + end + end end end end