github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/google/resource_google_project_iam_policy.go (about) 1 package google 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "sort" 8 9 "github.com/hashicorp/terraform/helper/schema" 10 "google.golang.org/api/cloudresourcemanager/v1" 11 ) 12 13 func resourceGoogleProjectIamPolicy() *schema.Resource { 14 return &schema.Resource{ 15 Create: resourceGoogleProjectIamPolicyCreate, 16 Read: resourceGoogleProjectIamPolicyRead, 17 Update: resourceGoogleProjectIamPolicyUpdate, 18 Delete: resourceGoogleProjectIamPolicyDelete, 19 20 Schema: map[string]*schema.Schema{ 21 "project": &schema.Schema{ 22 Type: schema.TypeString, 23 Required: true, 24 ForceNew: true, 25 }, 26 "policy_data": &schema.Schema{ 27 Type: schema.TypeString, 28 Required: true, 29 DiffSuppressFunc: jsonPolicyDiffSuppress, 30 }, 31 "authoritative": &schema.Schema{ 32 Type: schema.TypeBool, 33 Optional: true, 34 }, 35 "etag": &schema.Schema{ 36 Type: schema.TypeString, 37 Computed: true, 38 }, 39 "restore_policy": &schema.Schema{ 40 Type: schema.TypeString, 41 Computed: true, 42 }, 43 "disable_project": &schema.Schema{ 44 Type: schema.TypeBool, 45 Optional: true, 46 }, 47 }, 48 } 49 } 50 51 func resourceGoogleProjectIamPolicyCreate(d *schema.ResourceData, meta interface{}) error { 52 config := meta.(*Config) 53 pid := d.Get("project").(string) 54 // Get the policy in the template 55 p, err := getResourceIamPolicy(d) 56 if err != nil { 57 return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err) 58 } 59 60 // An authoritative policy is applied without regard for any existing IAM 61 // policy. 62 if v, ok := d.GetOk("authoritative"); ok && v.(bool) { 63 log.Printf("[DEBUG] Setting authoritative IAM policy for project %q", pid) 64 err := setProjectIamPolicy(p, config, pid) 65 if err != nil { 66 return err 67 } 68 } else { 69 log.Printf("[DEBUG] Setting non-authoritative IAM policy for project %q", pid) 70 // This is a non-authoritative policy, meaning it should be merged with 71 // any existing policy 72 ep, err := getProjectIamPolicy(pid, config) 73 if err != nil { 74 return err 75 } 76 77 // First, subtract the policy defined in the template from the 78 // current policy in the project, and save the result. This will 79 // allow us to restore the original policy at some point (which 80 // assumes that Terraform owns any common policy that exists in 81 // the template and project at create time. 82 rp := subtractIamPolicy(ep, p) 83 rps, err := json.Marshal(rp) 84 if err != nil { 85 return fmt.Errorf("Error marshaling restorable IAM policy: %v", err) 86 } 87 d.Set("restore_policy", string(rps)) 88 89 // Merge the policies together 90 mb := mergeBindings(append(p.Bindings, rp.Bindings...)) 91 ep.Bindings = mb 92 if err = setProjectIamPolicy(ep, config, pid); err != nil { 93 return fmt.Errorf("Error applying IAM policy to project: %v", err) 94 } 95 } 96 d.SetId(pid) 97 return resourceGoogleProjectIamPolicyRead(d, meta) 98 } 99 100 func resourceGoogleProjectIamPolicyRead(d *schema.ResourceData, meta interface{}) error { 101 log.Printf("[DEBUG]: Reading google_project_iam_policy") 102 config := meta.(*Config) 103 pid := d.Get("project").(string) 104 105 p, err := getProjectIamPolicy(pid, config) 106 if err != nil { 107 return err 108 } 109 110 var bindings []*cloudresourcemanager.Binding 111 if v, ok := d.GetOk("restore_policy"); ok { 112 var restored cloudresourcemanager.Policy 113 // if there's a restore policy, subtract it from the policy_data 114 err := json.Unmarshal([]byte(v.(string)), &restored) 115 if err != nil { 116 return fmt.Errorf("Error unmarshaling restorable IAM policy: %v", err) 117 } 118 subtracted := subtractIamPolicy(p, &restored) 119 bindings = subtracted.Bindings 120 } else { 121 bindings = p.Bindings 122 } 123 // we only marshal the bindings, because only the bindings get set in the config 124 pBytes, err := json.Marshal(&cloudresourcemanager.Policy{Bindings: bindings}) 125 if err != nil { 126 return fmt.Errorf("Error marshaling IAM policy: %v", err) 127 } 128 log.Printf("[DEBUG]: Setting etag=%s", p.Etag) 129 d.Set("etag", p.Etag) 130 d.Set("policy_data", string(pBytes)) 131 return nil 132 } 133 134 func resourceGoogleProjectIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error { 135 log.Printf("[DEBUG]: Updating google_project_iam_policy") 136 config := meta.(*Config) 137 pid := d.Get("project").(string) 138 139 // Get the policy in the template 140 p, err := getResourceIamPolicy(d) 141 if err != nil { 142 return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err) 143 } 144 pBytes, _ := json.Marshal(p) 145 log.Printf("[DEBUG] Got policy from config: %s", string(pBytes)) 146 147 // An authoritative policy is applied without regard for any existing IAM 148 // policy. 149 if v, ok := d.GetOk("authoritative"); ok && v.(bool) { 150 log.Printf("[DEBUG] Updating authoritative IAM policy for project %q", pid) 151 err := setProjectIamPolicy(p, config, pid) 152 if err != nil { 153 return fmt.Errorf("Error setting project IAM policy: %v", err) 154 } 155 d.Set("restore_policy", "") 156 } else { 157 log.Printf("[DEBUG] Updating non-authoritative IAM policy for project %q", pid) 158 // Get the previous policy from state 159 pp, err := getPrevResourceIamPolicy(d) 160 if err != nil { 161 return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err) 162 } 163 ppBytes, _ := json.Marshal(pp) 164 log.Printf("[DEBUG] Got previous version of changed project IAM policy: %s", string(ppBytes)) 165 166 // Get the existing IAM policy from the API 167 ep, err := getProjectIamPolicy(pid, config) 168 if err != nil { 169 return fmt.Errorf("Error retrieving IAM policy from project API: %v", err) 170 } 171 epBytes, _ := json.Marshal(ep) 172 log.Printf("[DEBUG] Got existing version of changed IAM policy from project API: %s", string(epBytes)) 173 174 // Subtract the previous and current policies from the policy retrieved from the API 175 rp := subtractIamPolicy(ep, pp) 176 rpBytes, _ := json.Marshal(rp) 177 log.Printf("[DEBUG] After subtracting the previous policy from the existing policy, remaining policies: %s", string(rpBytes)) 178 rp = subtractIamPolicy(rp, p) 179 rpBytes, _ = json.Marshal(rp) 180 log.Printf("[DEBUG] After subtracting the remaining policies from the config policy, remaining policies: %s", string(rpBytes)) 181 rps, err := json.Marshal(rp) 182 if err != nil { 183 return fmt.Errorf("Error marhsaling restorable IAM policy: %v", err) 184 } 185 d.Set("restore_policy", string(rps)) 186 187 // Merge the policies together 188 mb := mergeBindings(append(p.Bindings, rp.Bindings...)) 189 ep.Bindings = mb 190 if err = setProjectIamPolicy(ep, config, pid); err != nil { 191 return fmt.Errorf("Error applying IAM policy to project: %v", err) 192 } 193 } 194 195 return resourceGoogleProjectIamPolicyRead(d, meta) 196 } 197 198 func resourceGoogleProjectIamPolicyDelete(d *schema.ResourceData, meta interface{}) error { 199 log.Printf("[DEBUG]: Deleting google_project_iam_policy") 200 config := meta.(*Config) 201 pid := d.Get("project").(string) 202 203 // Get the existing IAM policy from the API 204 ep, err := getProjectIamPolicy(pid, config) 205 if err != nil { 206 return fmt.Errorf("Error retrieving IAM policy from project API: %v", err) 207 } 208 // Deleting an authoritative policy will leave the project with no policy, 209 // and unaccessible by anyone without org-level privs. For this reason, the 210 // "disable_project" property must be set to true, forcing the user to ack 211 // this outcome 212 if v, ok := d.GetOk("authoritative"); ok && v.(bool) { 213 if v, ok := d.GetOk("disable_project"); !ok || !v.(bool) { 214 return fmt.Errorf("You must set 'disable_project' to true before deleting an authoritative IAM policy") 215 } 216 ep.Bindings = make([]*cloudresourcemanager.Binding, 0) 217 218 } else { 219 // A non-authoritative policy should set the policy to the value of "restore_policy" in state 220 // Get the previous policy from state 221 rp, err := getRestoreIamPolicy(d) 222 if err != nil { 223 return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err) 224 } 225 ep.Bindings = rp.Bindings 226 } 227 if err = setProjectIamPolicy(ep, config, pid); err != nil { 228 return fmt.Errorf("Error applying IAM policy to project: %v", err) 229 } 230 d.SetId("") 231 return nil 232 } 233 234 // Subtract all bindings in policy b from policy a, and return the result 235 func subtractIamPolicy(a, b *cloudresourcemanager.Policy) *cloudresourcemanager.Policy { 236 am := rolesToMembersMap(a.Bindings) 237 238 for _, b := range b.Bindings { 239 if _, ok := am[b.Role]; ok { 240 for _, m := range b.Members { 241 delete(am[b.Role], m) 242 } 243 if len(am[b.Role]) == 0 { 244 delete(am, b.Role) 245 } 246 } 247 } 248 a.Bindings = rolesToMembersBinding(am) 249 return a 250 } 251 252 func setProjectIamPolicy(policy *cloudresourcemanager.Policy, config *Config, pid string) error { 253 // Apply the policy 254 pbytes, _ := json.Marshal(policy) 255 log.Printf("[DEBUG] Setting policy %#v for project: %s", string(pbytes), pid) 256 _, err := config.clientResourceManager.Projects.SetIamPolicy(pid, 257 &cloudresourcemanager.SetIamPolicyRequest{Policy: policy}).Do() 258 259 if err != nil { 260 return fmt.Errorf("Error applying IAM policy for project %q. Policy is %#v, error is %s", pid, policy, err) 261 } 262 return nil 263 } 264 265 // Get a cloudresourcemanager.Policy from a schema.ResourceData 266 func getResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) { 267 ps := d.Get("policy_data").(string) 268 // The policy string is just a marshaled cloudresourcemanager.Policy. 269 policy := &cloudresourcemanager.Policy{} 270 if err := json.Unmarshal([]byte(ps), policy); err != nil { 271 return nil, fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err) 272 } 273 return policy, nil 274 } 275 276 // Get the previous cloudresourcemanager.Policy from a schema.ResourceData if the 277 // resource has changed 278 func getPrevResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) { 279 var policy *cloudresourcemanager.Policy = &cloudresourcemanager.Policy{} 280 if d.HasChange("policy_data") { 281 v, _ := d.GetChange("policy_data") 282 if err := json.Unmarshal([]byte(v.(string)), policy); err != nil { 283 return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err) 284 } 285 } 286 return policy, nil 287 } 288 289 // Get the restore_policy that can be used to restore a project's IAM policy to its 290 // state before it was adopted into Terraform 291 func getRestoreIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) { 292 if v, ok := d.GetOk("restore_policy"); ok { 293 policy := &cloudresourcemanager.Policy{} 294 if err := json.Unmarshal([]byte(v.(string)), policy); err != nil { 295 return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err) 296 } 297 return policy, nil 298 } 299 return nil, fmt.Errorf("Resource does not have a 'restore_policy' attribute defined.") 300 } 301 302 // Retrieve the existing IAM Policy for a Project 303 func getProjectIamPolicy(project string, config *Config) (*cloudresourcemanager.Policy, error) { 304 p, err := config.clientResourceManager.Projects.GetIamPolicy(project, 305 &cloudresourcemanager.GetIamPolicyRequest{}).Do() 306 307 if err != nil { 308 return nil, fmt.Errorf("Error retrieving IAM policy for project %q: %s", project, err) 309 } 310 return p, nil 311 } 312 313 // Convert a map of roles->members to a list of Binding 314 func rolesToMembersBinding(m map[string]map[string]bool) []*cloudresourcemanager.Binding { 315 bindings := make([]*cloudresourcemanager.Binding, 0) 316 for role, members := range m { 317 b := cloudresourcemanager.Binding{ 318 Role: role, 319 Members: make([]string, 0), 320 } 321 for m, _ := range members { 322 b.Members = append(b.Members, m) 323 } 324 bindings = append(bindings, &b) 325 } 326 return bindings 327 } 328 329 // Map a role to a map of members, allowing easy merging of multiple bindings. 330 func rolesToMembersMap(bindings []*cloudresourcemanager.Binding) map[string]map[string]bool { 331 bm := make(map[string]map[string]bool) 332 // Get each binding 333 for _, b := range bindings { 334 // Initialize members map 335 if _, ok := bm[b.Role]; !ok { 336 bm[b.Role] = make(map[string]bool) 337 } 338 // Get each member (user/principal) for the binding 339 for _, m := range b.Members { 340 // Add the member 341 bm[b.Role][m] = true 342 } 343 } 344 return bm 345 } 346 347 // Merge multiple Bindings such that Bindings with the same Role result in 348 // a single Binding with combined Members 349 func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding { 350 bm := rolesToMembersMap(bindings) 351 rb := make([]*cloudresourcemanager.Binding, 0) 352 353 for role, members := range bm { 354 var b cloudresourcemanager.Binding 355 b.Role = role 356 b.Members = make([]string, 0) 357 for m, _ := range members { 358 b.Members = append(b.Members, m) 359 } 360 rb = append(rb, &b) 361 } 362 363 return rb 364 } 365 366 func jsonPolicyDiffSuppress(k, old, new string, d *schema.ResourceData) bool { 367 var oldPolicy, newPolicy cloudresourcemanager.Policy 368 if err := json.Unmarshal([]byte(old), &oldPolicy); err != nil { 369 log.Printf("[ERROR] Could not unmarshal old policy %s: %v", old, err) 370 return false 371 } 372 if err := json.Unmarshal([]byte(new), &newPolicy); err != nil { 373 log.Printf("[ERROR] Could not unmarshal new policy %s: %v", new, err) 374 return false 375 } 376 if newPolicy.Etag != oldPolicy.Etag { 377 return false 378 } 379 if newPolicy.Version != oldPolicy.Version { 380 return false 381 } 382 if len(newPolicy.Bindings) != len(oldPolicy.Bindings) { 383 return false 384 } 385 sort.Sort(sortableBindings(newPolicy.Bindings)) 386 sort.Sort(sortableBindings(oldPolicy.Bindings)) 387 for pos, newBinding := range newPolicy.Bindings { 388 oldBinding := oldPolicy.Bindings[pos] 389 if oldBinding.Role != newBinding.Role { 390 return false 391 } 392 if len(oldBinding.Members) != len(newBinding.Members) { 393 return false 394 } 395 sort.Strings(oldBinding.Members) 396 sort.Strings(newBinding.Members) 397 for i, newMember := range newBinding.Members { 398 oldMember := oldBinding.Members[i] 399 if newMember != oldMember { 400 return false 401 } 402 } 403 } 404 return true 405 } 406 407 type sortableBindings []*cloudresourcemanager.Binding 408 409 func (b sortableBindings) Len() int { 410 return len(b) 411 } 412 func (b sortableBindings) Swap(i, j int) { 413 b[i], b[j] = b[j], b[i] 414 } 415 func (b sortableBindings) Less(i, j int) bool { 416 return b[i].Role < b[j].Role 417 }