418 lines
14 KiB
Go
418 lines
14 KiB
Go
|
package google
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"sort"
|
||
|
|
||
|
"github.com/hashicorp/terraform/helper/schema"
|
||
|
"google.golang.org/api/cloudresourcemanager/v1"
|
||
|
)
|
||
|
|
||
|
func resourceGoogleProjectIamPolicy() *schema.Resource {
|
||
|
return &schema.Resource{
|
||
|
Create: resourceGoogleProjectIamPolicyCreate,
|
||
|
Read: resourceGoogleProjectIamPolicyRead,
|
||
|
Update: resourceGoogleProjectIamPolicyUpdate,
|
||
|
Delete: resourceGoogleProjectIamPolicyDelete,
|
||
|
|
||
|
Schema: map[string]*schema.Schema{
|
||
|
"project": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Required: true,
|
||
|
ForceNew: true,
|
||
|
},
|
||
|
"policy_data": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Required: true,
|
||
|
DiffSuppressFunc: jsonPolicyDiffSuppress,
|
||
|
},
|
||
|
"authoritative": &schema.Schema{
|
||
|
Type: schema.TypeBool,
|
||
|
Optional: true,
|
||
|
},
|
||
|
"etag": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Computed: true,
|
||
|
},
|
||
|
"restore_policy": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Computed: true,
|
||
|
},
|
||
|
"disable_project": &schema.Schema{
|
||
|
Type: schema.TypeBool,
|
||
|
Optional: true,
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func resourceGoogleProjectIamPolicyCreate(d *schema.ResourceData, meta interface{}) error {
|
||
|
config := meta.(*Config)
|
||
|
pid := d.Get("project").(string)
|
||
|
// Get the policy in the template
|
||
|
p, err := getResourceIamPolicy(d)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err)
|
||
|
}
|
||
|
|
||
|
// An authoritative policy is applied without regard for any existing IAM
|
||
|
// policy.
|
||
|
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
|
||
|
log.Printf("[DEBUG] Setting authoritative IAM policy for project %q", pid)
|
||
|
err := setProjectIamPolicy(p, config, pid)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
log.Printf("[DEBUG] Setting non-authoritative IAM policy for project %q", pid)
|
||
|
// This is a non-authoritative policy, meaning it should be merged with
|
||
|
// any existing policy
|
||
|
ep, err := getProjectIamPolicy(pid, config)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// First, subtract the policy defined in the template from the
|
||
|
// current policy in the project, and save the result. This will
|
||
|
// allow us to restore the original policy at some point (which
|
||
|
// assumes that Terraform owns any common policy that exists in
|
||
|
// the template and project at create time.
|
||
|
rp := subtractIamPolicy(ep, p)
|
||
|
rps, err := json.Marshal(rp)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error marshaling restorable IAM policy: %v", err)
|
||
|
}
|
||
|
d.Set("restore_policy", string(rps))
|
||
|
|
||
|
// Merge the policies together
|
||
|
mb := mergeBindings(append(p.Bindings, rp.Bindings...))
|
||
|
ep.Bindings = mb
|
||
|
if err = setProjectIamPolicy(ep, config, pid); err != nil {
|
||
|
return fmt.Errorf("Error applying IAM policy to project: %v", err)
|
||
|
}
|
||
|
}
|
||
|
d.SetId(pid)
|
||
|
return resourceGoogleProjectIamPolicyRead(d, meta)
|
||
|
}
|
||
|
|
||
|
func resourceGoogleProjectIamPolicyRead(d *schema.ResourceData, meta interface{}) error {
|
||
|
log.Printf("[DEBUG]: Reading google_project_iam_policy")
|
||
|
config := meta.(*Config)
|
||
|
pid := d.Get("project").(string)
|
||
|
|
||
|
p, err := getProjectIamPolicy(pid, config)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
var bindings []*cloudresourcemanager.Binding
|
||
|
if v, ok := d.GetOk("restore_policy"); ok {
|
||
|
var restored cloudresourcemanager.Policy
|
||
|
// if there's a restore policy, subtract it from the policy_data
|
||
|
err := json.Unmarshal([]byte(v.(string)), &restored)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error unmarshaling restorable IAM policy: %v", err)
|
||
|
}
|
||
|
subtracted := subtractIamPolicy(p, &restored)
|
||
|
bindings = subtracted.Bindings
|
||
|
} else {
|
||
|
bindings = p.Bindings
|
||
|
}
|
||
|
// we only marshal the bindings, because only the bindings get set in the config
|
||
|
pBytes, err := json.Marshal(&cloudresourcemanager.Policy{Bindings: bindings})
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error marshaling IAM policy: %v", err)
|
||
|
}
|
||
|
log.Printf("[DEBUG]: Setting etag=%s", p.Etag)
|
||
|
d.Set("etag", p.Etag)
|
||
|
d.Set("policy_data", string(pBytes))
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func resourceGoogleProjectIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error {
|
||
|
log.Printf("[DEBUG]: Updating google_project_iam_policy")
|
||
|
config := meta.(*Config)
|
||
|
pid := d.Get("project").(string)
|
||
|
|
||
|
// Get the policy in the template
|
||
|
p, err := getResourceIamPolicy(d)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err)
|
||
|
}
|
||
|
pBytes, _ := json.Marshal(p)
|
||
|
log.Printf("[DEBUG] Got policy from config: %s", string(pBytes))
|
||
|
|
||
|
// An authoritative policy is applied without regard for any existing IAM
|
||
|
// policy.
|
||
|
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
|
||
|
log.Printf("[DEBUG] Updating authoritative IAM policy for project %q", pid)
|
||
|
err := setProjectIamPolicy(p, config, pid)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error setting project IAM policy: %v", err)
|
||
|
}
|
||
|
d.Set("restore_policy", "")
|
||
|
} else {
|
||
|
log.Printf("[DEBUG] Updating non-authoritative IAM policy for project %q", pid)
|
||
|
// Get the previous policy from state
|
||
|
pp, err := getPrevResourceIamPolicy(d)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err)
|
||
|
}
|
||
|
ppBytes, _ := json.Marshal(pp)
|
||
|
log.Printf("[DEBUG] Got previous version of changed project IAM policy: %s", string(ppBytes))
|
||
|
|
||
|
// Get the existing IAM policy from the API
|
||
|
ep, err := getProjectIamPolicy(pid, config)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error retrieving IAM policy from project API: %v", err)
|
||
|
}
|
||
|
epBytes, _ := json.Marshal(ep)
|
||
|
log.Printf("[DEBUG] Got existing version of changed IAM policy from project API: %s", string(epBytes))
|
||
|
|
||
|
// Subtract the previous and current policies from the policy retrieved from the API
|
||
|
rp := subtractIamPolicy(ep, pp)
|
||
|
rpBytes, _ := json.Marshal(rp)
|
||
|
log.Printf("[DEBUG] After subtracting the previous policy from the existing policy, remaining policies: %s", string(rpBytes))
|
||
|
rp = subtractIamPolicy(rp, p)
|
||
|
rpBytes, _ = json.Marshal(rp)
|
||
|
log.Printf("[DEBUG] After subtracting the remaining policies from the config policy, remaining policies: %s", string(rpBytes))
|
||
|
rps, err := json.Marshal(rp)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error marhsaling restorable IAM policy: %v", err)
|
||
|
}
|
||
|
d.Set("restore_policy", string(rps))
|
||
|
|
||
|
// Merge the policies together
|
||
|
mb := mergeBindings(append(p.Bindings, rp.Bindings...))
|
||
|
ep.Bindings = mb
|
||
|
if err = setProjectIamPolicy(ep, config, pid); err != nil {
|
||
|
return fmt.Errorf("Error applying IAM policy to project: %v", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return resourceGoogleProjectIamPolicyRead(d, meta)
|
||
|
}
|
||
|
|
||
|
func resourceGoogleProjectIamPolicyDelete(d *schema.ResourceData, meta interface{}) error {
|
||
|
log.Printf("[DEBUG]: Deleting google_project_iam_policy")
|
||
|
config := meta.(*Config)
|
||
|
pid := d.Get("project").(string)
|
||
|
|
||
|
// Get the existing IAM policy from the API
|
||
|
ep, err := getProjectIamPolicy(pid, config)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error retrieving IAM policy from project API: %v", err)
|
||
|
}
|
||
|
// Deleting an authoritative policy will leave the project with no policy,
|
||
|
// and unaccessible by anyone without org-level privs. For this reason, the
|
||
|
// "disable_project" property must be set to true, forcing the user to ack
|
||
|
// this outcome
|
||
|
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
|
||
|
if v, ok := d.GetOk("disable_project"); !ok || !v.(bool) {
|
||
|
return fmt.Errorf("You must set 'disable_project' to true before deleting an authoritative IAM policy")
|
||
|
}
|
||
|
ep.Bindings = make([]*cloudresourcemanager.Binding, 0)
|
||
|
|
||
|
} else {
|
||
|
// A non-authoritative policy should set the policy to the value of "restore_policy" in state
|
||
|
// Get the previous policy from state
|
||
|
rp, err := getRestoreIamPolicy(d)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err)
|
||
|
}
|
||
|
ep.Bindings = rp.Bindings
|
||
|
}
|
||
|
if err = setProjectIamPolicy(ep, config, pid); err != nil {
|
||
|
return fmt.Errorf("Error applying IAM policy to project: %v", err)
|
||
|
}
|
||
|
d.SetId("")
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Subtract all bindings in policy b from policy a, and return the result
|
||
|
func subtractIamPolicy(a, b *cloudresourcemanager.Policy) *cloudresourcemanager.Policy {
|
||
|
am := rolesToMembersMap(a.Bindings)
|
||
|
|
||
|
for _, b := range b.Bindings {
|
||
|
if _, ok := am[b.Role]; ok {
|
||
|
for _, m := range b.Members {
|
||
|
delete(am[b.Role], m)
|
||
|
}
|
||
|
if len(am[b.Role]) == 0 {
|
||
|
delete(am, b.Role)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
a.Bindings = rolesToMembersBinding(am)
|
||
|
return a
|
||
|
}
|
||
|
|
||
|
func setProjectIamPolicy(policy *cloudresourcemanager.Policy, config *Config, pid string) error {
|
||
|
// Apply the policy
|
||
|
pbytes, _ := json.Marshal(policy)
|
||
|
log.Printf("[DEBUG] Setting policy %#v for project: %s", string(pbytes), pid)
|
||
|
_, err := config.clientResourceManager.Projects.SetIamPolicy(pid,
|
||
|
&cloudresourcemanager.SetIamPolicyRequest{Policy: policy}).Do()
|
||
|
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error applying IAM policy for project %q. Policy is %+s, error is %s", pid, policy, err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Get a cloudresourcemanager.Policy from a schema.ResourceData
|
||
|
func getResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
|
||
|
ps := d.Get("policy_data").(string)
|
||
|
// The policy string is just a marshaled cloudresourcemanager.Policy.
|
||
|
policy := &cloudresourcemanager.Policy{}
|
||
|
if err := json.Unmarshal([]byte(ps), policy); err != nil {
|
||
|
return nil, fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err)
|
||
|
}
|
||
|
return policy, nil
|
||
|
}
|
||
|
|
||
|
// Get the previous cloudresourcemanager.Policy from a schema.ResourceData if the
|
||
|
// resource has changed
|
||
|
func getPrevResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
|
||
|
var policy *cloudresourcemanager.Policy = &cloudresourcemanager.Policy{}
|
||
|
if d.HasChange("policy_data") {
|
||
|
v, _ := d.GetChange("policy_data")
|
||
|
if err := json.Unmarshal([]byte(v.(string)), policy); err != nil {
|
||
|
return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err)
|
||
|
}
|
||
|
}
|
||
|
return policy, nil
|
||
|
}
|
||
|
|
||
|
// Get the restore_policy that can be used to restore a project's IAM policy to its
|
||
|
// state before it was adopted into Terraform
|
||
|
func getRestoreIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
|
||
|
if v, ok := d.GetOk("restore_policy"); ok {
|
||
|
policy := &cloudresourcemanager.Policy{}
|
||
|
if err := json.Unmarshal([]byte(v.(string)), policy); err != nil {
|
||
|
return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err)
|
||
|
}
|
||
|
return policy, nil
|
||
|
}
|
||
|
return nil, fmt.Errorf("Resource does not have a 'restore_policy' attribute defined.")
|
||
|
}
|
||
|
|
||
|
// Retrieve the existing IAM Policy for a Project
|
||
|
func getProjectIamPolicy(project string, config *Config) (*cloudresourcemanager.Policy, error) {
|
||
|
p, err := config.clientResourceManager.Projects.GetIamPolicy(project,
|
||
|
&cloudresourcemanager.GetIamPolicyRequest{}).Do()
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Error retrieving IAM policy for project %q: %s", project, err)
|
||
|
}
|
||
|
return p, nil
|
||
|
}
|
||
|
|
||
|
// Convert a map of roles->members to a list of Binding
|
||
|
func rolesToMembersBinding(m map[string]map[string]bool) []*cloudresourcemanager.Binding {
|
||
|
bindings := make([]*cloudresourcemanager.Binding, 0)
|
||
|
for role, members := range m {
|
||
|
b := cloudresourcemanager.Binding{
|
||
|
Role: role,
|
||
|
Members: make([]string, 0),
|
||
|
}
|
||
|
for m, _ := range members {
|
||
|
b.Members = append(b.Members, m)
|
||
|
}
|
||
|
bindings = append(bindings, &b)
|
||
|
}
|
||
|
return bindings
|
||
|
}
|
||
|
|
||
|
// Map a role to a map of members, allowing easy merging of multiple bindings.
|
||
|
func rolesToMembersMap(bindings []*cloudresourcemanager.Binding) map[string]map[string]bool {
|
||
|
bm := make(map[string]map[string]bool)
|
||
|
// Get each binding
|
||
|
for _, b := range bindings {
|
||
|
// Initialize members map
|
||
|
if _, ok := bm[b.Role]; !ok {
|
||
|
bm[b.Role] = make(map[string]bool)
|
||
|
}
|
||
|
// Get each member (user/principal) for the binding
|
||
|
for _, m := range b.Members {
|
||
|
// Add the member
|
||
|
bm[b.Role][m] = true
|
||
|
}
|
||
|
}
|
||
|
return bm
|
||
|
}
|
||
|
|
||
|
// Merge multiple Bindings such that Bindings with the same Role result in
|
||
|
// a single Binding with combined Members
|
||
|
func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding {
|
||
|
bm := rolesToMembersMap(bindings)
|
||
|
rb := make([]*cloudresourcemanager.Binding, 0)
|
||
|
|
||
|
for role, members := range bm {
|
||
|
var b cloudresourcemanager.Binding
|
||
|
b.Role = role
|
||
|
b.Members = make([]string, 0)
|
||
|
for m, _ := range members {
|
||
|
b.Members = append(b.Members, m)
|
||
|
}
|
||
|
rb = append(rb, &b)
|
||
|
}
|
||
|
|
||
|
return rb
|
||
|
}
|
||
|
|
||
|
func jsonPolicyDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
|
||
|
var oldPolicy, newPolicy cloudresourcemanager.Policy
|
||
|
if err := json.Unmarshal([]byte(old), &oldPolicy); err != nil {
|
||
|
log.Printf("[ERROR] Could not unmarshal old policy %s: %v", old, err)
|
||
|
return false
|
||
|
}
|
||
|
if err := json.Unmarshal([]byte(new), &newPolicy); err != nil {
|
||
|
log.Printf("[ERROR] Could not unmarshal new policy %s: %v", new, err)
|
||
|
return false
|
||
|
}
|
||
|
if newPolicy.Etag != oldPolicy.Etag {
|
||
|
return false
|
||
|
}
|
||
|
if newPolicy.Version != oldPolicy.Version {
|
||
|
return false
|
||
|
}
|
||
|
if len(newPolicy.Bindings) != len(oldPolicy.Bindings) {
|
||
|
return false
|
||
|
}
|
||
|
sort.Sort(sortableBindings(newPolicy.Bindings))
|
||
|
sort.Sort(sortableBindings(oldPolicy.Bindings))
|
||
|
for pos, newBinding := range newPolicy.Bindings {
|
||
|
oldBinding := oldPolicy.Bindings[pos]
|
||
|
if oldBinding.Role != newBinding.Role {
|
||
|
return false
|
||
|
}
|
||
|
if len(oldBinding.Members) != len(newBinding.Members) {
|
||
|
return false
|
||
|
}
|
||
|
sort.Strings(oldBinding.Members)
|
||
|
sort.Strings(newBinding.Members)
|
||
|
for i, newMember := range newBinding.Members {
|
||
|
oldMember := oldBinding.Members[i]
|
||
|
if newMember != oldMember {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
type sortableBindings []*cloudresourcemanager.Binding
|
||
|
|
||
|
func (b sortableBindings) Len() int {
|
||
|
return len(b)
|
||
|
}
|
||
|
func (b sortableBindings) Swap(i, j int) {
|
||
|
b[i], b[j] = b[j], b[i]
|
||
|
}
|
||
|
func (b sortableBindings) Less(i, j int) bool {
|
||
|
return b[i].Role < b[j].Role
|
||
|
}
|