github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/builtin/providers/google/resource_google_service_account.go (about) 1 package google 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 8 "github.com/hashicorp/terraform/helper/schema" 9 "google.golang.org/api/googleapi" 10 "google.golang.org/api/iam/v1" 11 ) 12 13 func resourceGoogleServiceAccount() *schema.Resource { 14 return &schema.Resource{ 15 Create: resourceGoogleServiceAccountCreate, 16 Read: resourceGoogleServiceAccountRead, 17 Delete: resourceGoogleServiceAccountDelete, 18 Update: resourceGoogleServiceAccountUpdate, 19 Schema: map[string]*schema.Schema{ 20 "email": &schema.Schema{ 21 Type: schema.TypeString, 22 Computed: true, 23 }, 24 "unique_id": &schema.Schema{ 25 Type: schema.TypeString, 26 Computed: true, 27 }, 28 "name": &schema.Schema{ 29 Type: schema.TypeString, 30 Computed: true, 31 }, 32 "account_id": &schema.Schema{ 33 Type: schema.TypeString, 34 Required: true, 35 ForceNew: true, 36 }, 37 "display_name": &schema.Schema{ 38 Type: schema.TypeString, 39 Optional: true, 40 }, 41 "project": &schema.Schema{ 42 Type: schema.TypeString, 43 Optional: true, 44 ForceNew: true, 45 }, 46 "policy_data": &schema.Schema{ 47 Type: schema.TypeString, 48 Optional: true, 49 }, 50 }, 51 } 52 } 53 54 func resourceGoogleServiceAccountCreate(d *schema.ResourceData, meta interface{}) error { 55 config := meta.(*Config) 56 project, err := getProject(d, config) 57 if err != nil { 58 return err 59 } 60 aid := d.Get("account_id").(string) 61 displayName := d.Get("display_name").(string) 62 63 sa := &iam.ServiceAccount{ 64 DisplayName: displayName, 65 } 66 67 r := &iam.CreateServiceAccountRequest{ 68 AccountId: aid, 69 ServiceAccount: sa, 70 } 71 72 sa, err = config.clientIAM.Projects.ServiceAccounts.Create("projects/"+project, r).Do() 73 if err != nil { 74 return fmt.Errorf("Error creating service account: %s", err) 75 } 76 77 d.SetId(sa.Name) 78 79 // Apply the IAM policy if it is set 80 if pString, ok := d.GetOk("policy_data"); ok { 81 // The policy string is just a marshaled cloudresourcemanager.Policy. 82 // Unmarshal it to a struct. 83 var policy iam.Policy 84 if err = json.Unmarshal([]byte(pString.(string)), &policy); err != nil { 85 return err 86 } 87 88 // Retrieve existing IAM policy from project. This will be merged 89 // with the policy defined here. 90 // TODO(evanbrown): Add an 'authoritative' flag that allows policy 91 // in manifest to overwrite existing policy. 92 p, err := getServiceAccountIamPolicy(sa.Name, config) 93 if err != nil { 94 return fmt.Errorf("Could not find service account %q when applying IAM policy: %s", sa.Name, err) 95 } 96 log.Printf("[DEBUG] Got existing bindings for service account: %#v", p.Bindings) 97 98 // Merge the existing policy bindings with those defined in this manifest. 99 p.Bindings = saMergeBindings(append(p.Bindings, policy.Bindings...)) 100 101 // Apply the merged policy 102 log.Printf("[DEBUG] Setting new policy for service account: %#v", p) 103 _, err = config.clientIAM.Projects.ServiceAccounts.SetIamPolicy(sa.Name, 104 &iam.SetIamPolicyRequest{Policy: p}).Do() 105 106 if err != nil { 107 return fmt.Errorf("Error applying IAM policy for service account %q: %s", sa.Name, err) 108 } 109 } 110 return resourceGoogleServiceAccountRead(d, meta) 111 } 112 113 func resourceGoogleServiceAccountRead(d *schema.ResourceData, meta interface{}) error { 114 config := meta.(*Config) 115 116 // Confirm the service account exists 117 sa, err := config.clientIAM.Projects.ServiceAccounts.Get(d.Id()).Do() 118 if err != nil { 119 if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { 120 log.Printf("[WARN] Removing reference to service account %q because it no longer exists", d.Id()) 121 // The resource doesn't exist anymore 122 d.SetId("") 123 124 return nil 125 } 126 return fmt.Errorf("Error reading service account %q: %q", d.Id(), err) 127 } 128 129 d.Set("email", sa.Email) 130 d.Set("unique_id", sa.UniqueId) 131 d.Set("name", sa.Name) 132 d.Set("display_name", sa.DisplayName) 133 return nil 134 } 135 136 func resourceGoogleServiceAccountDelete(d *schema.ResourceData, meta interface{}) error { 137 config := meta.(*Config) 138 name := d.Id() 139 _, err := config.clientIAM.Projects.ServiceAccounts.Delete(name).Do() 140 if err != nil { 141 return err 142 } 143 d.SetId("") 144 return nil 145 } 146 147 func resourceGoogleServiceAccountUpdate(d *schema.ResourceData, meta interface{}) error { 148 config := meta.(*Config) 149 var err error 150 if ok := d.HasChange("display_name"); ok { 151 sa, err := config.clientIAM.Projects.ServiceAccounts.Get(d.Id()).Do() 152 if err != nil { 153 return fmt.Errorf("Error retrieving service account %q: %s", d.Id(), err) 154 } 155 _, err = config.clientIAM.Projects.ServiceAccounts.Update(d.Id(), 156 &iam.ServiceAccount{ 157 DisplayName: d.Get("display_name").(string), 158 Etag: sa.Etag, 159 }).Do() 160 if err != nil { 161 return fmt.Errorf("Error updating service account %q: %s", d.Id(), err) 162 } 163 } 164 165 if ok := d.HasChange("policy_data"); ok { 166 // The policy string is just a marshaled cloudresourcemanager.Policy. 167 // Unmarshal it to a struct that contains the old and new policies 168 oldP, newP := d.GetChange("policy_data") 169 oldPString := oldP.(string) 170 newPString := newP.(string) 171 172 // JSON Unmarshaling would fail 173 if oldPString == "" { 174 oldPString = "{}" 175 } 176 if newPString == "" { 177 newPString = "{}" 178 } 179 180 log.Printf("[DEBUG]: Old policy: %q\nNew policy: %q", string(oldPString), string(newPString)) 181 182 var oldPolicy, newPolicy iam.Policy 183 if err = json.Unmarshal([]byte(newPString), &newPolicy); err != nil { 184 return err 185 } 186 if err = json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil { 187 return err 188 } 189 190 // Find any Roles and Members that were removed (i.e., those that are present 191 // in the old but absent in the new 192 oldMap := saRolesToMembersMap(oldPolicy.Bindings) 193 newMap := saRolesToMembersMap(newPolicy.Bindings) 194 deleted := make(map[string]map[string]bool) 195 196 // Get each role and its associated members in the old state 197 for role, members := range oldMap { 198 // Initialize map for role 199 if _, ok := deleted[role]; !ok { 200 deleted[role] = make(map[string]bool) 201 } 202 // The role exists in the new state 203 if _, ok := newMap[role]; ok { 204 // Check each memeber 205 for member, _ := range members { 206 // Member does not exist in new state, so it was deleted 207 if _, ok = newMap[role][member]; !ok { 208 deleted[role][member] = true 209 } 210 } 211 } else { 212 // This indicates an entire role was deleted. Mark all members 213 // for delete. 214 for member, _ := range members { 215 deleted[role][member] = true 216 } 217 } 218 } 219 log.Printf("[DEBUG] Roles and Members to be deleted: %#v", deleted) 220 221 // Retrieve existing IAM policy from project. This will be merged 222 // with the policy in the current state 223 // TODO(evanbrown): Add an 'authoritative' flag that allows policy 224 // in manifest to overwrite existing policy. 225 p, err := getServiceAccountIamPolicy(d.Id(), config) 226 if err != nil { 227 return err 228 } 229 log.Printf("[DEBUG] Got existing bindings from service account %q: %#v", d.Id(), p.Bindings) 230 231 // Merge existing policy with policy in the current state 232 log.Printf("[DEBUG] Merging new bindings from service account %q: %#v", d.Id(), newPolicy.Bindings) 233 mergedBindings := saMergeBindings(append(p.Bindings, newPolicy.Bindings...)) 234 235 // Remove any roles and members that were explicitly deleted 236 mergedBindingsMap := saRolesToMembersMap(mergedBindings) 237 for role, members := range deleted { 238 for member, _ := range members { 239 delete(mergedBindingsMap[role], member) 240 } 241 } 242 243 p.Bindings = saRolesToMembersBinding(mergedBindingsMap) 244 log.Printf("[DEBUG] Setting new policy for project: %#v", p) 245 246 dump, _ := json.MarshalIndent(p.Bindings, " ", " ") 247 log.Printf(string(dump)) 248 _, err = config.clientIAM.Projects.ServiceAccounts.SetIamPolicy(d.Id(), 249 &iam.SetIamPolicyRequest{Policy: p}).Do() 250 251 if err != nil { 252 return fmt.Errorf("Error applying IAM policy for service account %q: %s", d.Id(), err) 253 } 254 } 255 return nil 256 } 257 258 // Retrieve the existing IAM Policy for a service account 259 func getServiceAccountIamPolicy(sa string, config *Config) (*iam.Policy, error) { 260 p, err := config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(sa).Do() 261 262 if err != nil { 263 return nil, fmt.Errorf("Error retrieving IAM policy for service account %q: %s", sa, err) 264 } 265 return p, nil 266 } 267 268 // Convert a map of roles->members to a list of Binding 269 func saRolesToMembersBinding(m map[string]map[string]bool) []*iam.Binding { 270 bindings := make([]*iam.Binding, 0) 271 for role, members := range m { 272 b := iam.Binding{ 273 Role: role, 274 Members: make([]string, 0), 275 } 276 for m, _ := range members { 277 b.Members = append(b.Members, m) 278 } 279 bindings = append(bindings, &b) 280 } 281 return bindings 282 } 283 284 // Map a role to a map of members, allowing easy merging of multiple bindings. 285 func saRolesToMembersMap(bindings []*iam.Binding) map[string]map[string]bool { 286 bm := make(map[string]map[string]bool) 287 // Get each binding 288 for _, b := range bindings { 289 // Initialize members map 290 if _, ok := bm[b.Role]; !ok { 291 bm[b.Role] = make(map[string]bool) 292 } 293 // Get each member (user/principal) for the binding 294 for _, m := range b.Members { 295 // Add the member 296 bm[b.Role][m] = true 297 } 298 } 299 return bm 300 } 301 302 // Merge multiple Bindings such that Bindings with the same Role result in 303 // a single Binding with combined Members 304 func saMergeBindings(bindings []*iam.Binding) []*iam.Binding { 305 bm := saRolesToMembersMap(bindings) 306 rb := make([]*iam.Binding, 0) 307 308 for role, members := range bm { 309 var b iam.Binding 310 b.Role = role 311 b.Members = make([]string, 0) 312 for m, _ := range members { 313 b.Members = append(b.Members, m) 314 } 315 rb = append(rb, &b) 316 } 317 318 return rb 319 }