github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/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 saName := d.Id() 122 // The resource doesn't exist anymore 123 d.SetId("") 124 125 return fmt.Errorf("Error getting service account with name %q: %s", saName, err) 126 } 127 return fmt.Errorf("Error reading service account %q: %q", d.Id(), err) 128 } 129 130 d.Set("email", sa.Email) 131 d.Set("unique_id", sa.UniqueId) 132 d.Set("name", sa.Name) 133 d.Set("display_name", sa.DisplayName) 134 return nil 135 } 136 137 func resourceGoogleServiceAccountDelete(d *schema.ResourceData, meta interface{}) error { 138 config := meta.(*Config) 139 name := d.Id() 140 _, err := config.clientIAM.Projects.ServiceAccounts.Delete(name).Do() 141 if err != nil { 142 return err 143 } 144 d.SetId("") 145 return nil 146 } 147 148 func resourceGoogleServiceAccountUpdate(d *schema.ResourceData, meta interface{}) error { 149 config := meta.(*Config) 150 var err error 151 if ok := d.HasChange("display_name"); ok { 152 sa, err := config.clientIAM.Projects.ServiceAccounts.Get(d.Id()).Do() 153 if err != nil { 154 return fmt.Errorf("Error retrieving service account %q: %s", d.Id(), err) 155 } 156 _, err = config.clientIAM.Projects.ServiceAccounts.Update(d.Id(), 157 &iam.ServiceAccount{ 158 DisplayName: d.Get("display_name").(string), 159 Etag: sa.Etag, 160 }).Do() 161 if err != nil { 162 return fmt.Errorf("Error updating service account %q: %s", d.Id(), err) 163 } 164 } 165 166 if ok := d.HasChange("policy_data"); ok { 167 // The policy string is just a marshaled cloudresourcemanager.Policy. 168 // Unmarshal it to a struct that contains the old and new policies 169 oldP, newP := d.GetChange("policy_data") 170 oldPString := oldP.(string) 171 newPString := newP.(string) 172 173 // JSON Unmarshaling would fail 174 if oldPString == "" { 175 oldPString = "{}" 176 } 177 if newPString == "" { 178 newPString = "{}" 179 } 180 181 log.Printf("[DEBUG]: Old policy: %q\nNew policy: %q", string(oldPString), string(newPString)) 182 183 var oldPolicy, newPolicy iam.Policy 184 if err = json.Unmarshal([]byte(newPString), &newPolicy); err != nil { 185 return err 186 } 187 if err = json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil { 188 return err 189 } 190 191 // Find any Roles and Members that were removed (i.e., those that are present 192 // in the old but absent in the new 193 oldMap := saRolesToMembersMap(oldPolicy.Bindings) 194 newMap := saRolesToMembersMap(newPolicy.Bindings) 195 deleted := make(map[string]map[string]bool) 196 197 // Get each role and its associated members in the old state 198 for role, members := range oldMap { 199 // Initialize map for role 200 if _, ok := deleted[role]; !ok { 201 deleted[role] = make(map[string]bool) 202 } 203 // The role exists in the new state 204 if _, ok := newMap[role]; ok { 205 // Check each memeber 206 for member, _ := range members { 207 // Member does not exist in new state, so it was deleted 208 if _, ok = newMap[role][member]; !ok { 209 deleted[role][member] = true 210 } 211 } 212 } else { 213 // This indicates an entire role was deleted. Mark all members 214 // for delete. 215 for member, _ := range members { 216 deleted[role][member] = true 217 } 218 } 219 } 220 log.Printf("[DEBUG] Roles and Members to be deleted: %#v", deleted) 221 222 // Retrieve existing IAM policy from project. This will be merged 223 // with the policy in the current state 224 // TODO(evanbrown): Add an 'authoritative' flag that allows policy 225 // in manifest to overwrite existing policy. 226 p, err := getServiceAccountIamPolicy(d.Id(), config) 227 if err != nil { 228 return err 229 } 230 log.Printf("[DEBUG] Got existing bindings from service account %q: %#v", d.Id(), p.Bindings) 231 232 // Merge existing policy with policy in the current state 233 log.Printf("[DEBUG] Merging new bindings from service account %q: %#v", d.Id(), newPolicy.Bindings) 234 mergedBindings := saMergeBindings(append(p.Bindings, newPolicy.Bindings...)) 235 236 // Remove any roles and members that were explicitly deleted 237 mergedBindingsMap := saRolesToMembersMap(mergedBindings) 238 for role, members := range deleted { 239 for member, _ := range members { 240 delete(mergedBindingsMap[role], member) 241 } 242 } 243 244 p.Bindings = saRolesToMembersBinding(mergedBindingsMap) 245 log.Printf("[DEBUG] Setting new policy for project: %#v", p) 246 247 dump, _ := json.MarshalIndent(p.Bindings, " ", " ") 248 log.Printf(string(dump)) 249 _, err = config.clientIAM.Projects.ServiceAccounts.SetIamPolicy(d.Id(), 250 &iam.SetIamPolicyRequest{Policy: p}).Do() 251 252 if err != nil { 253 return fmt.Errorf("Error applying IAM policy for service account %q: %s", d.Id(), err) 254 } 255 } 256 return nil 257 } 258 259 // Retrieve the existing IAM Policy for a service account 260 func getServiceAccountIamPolicy(sa string, config *Config) (*iam.Policy, error) { 261 p, err := config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(sa).Do() 262 263 if err != nil { 264 return nil, fmt.Errorf("Error retrieving IAM policy for service account %q: %s", sa, err) 265 } 266 return p, nil 267 } 268 269 // Convert a map of roles->members to a list of Binding 270 func saRolesToMembersBinding(m map[string]map[string]bool) []*iam.Binding { 271 bindings := make([]*iam.Binding, 0) 272 for role, members := range m { 273 b := iam.Binding{ 274 Role: role, 275 Members: make([]string, 0), 276 } 277 for m, _ := range members { 278 b.Members = append(b.Members, m) 279 } 280 bindings = append(bindings, &b) 281 } 282 return bindings 283 } 284 285 // Map a role to a map of members, allowing easy merging of multiple bindings. 286 func saRolesToMembersMap(bindings []*iam.Binding) map[string]map[string]bool { 287 bm := make(map[string]map[string]bool) 288 // Get each binding 289 for _, b := range bindings { 290 // Initialize members map 291 if _, ok := bm[b.Role]; !ok { 292 bm[b.Role] = make(map[string]bool) 293 } 294 // Get each member (user/principal) for the binding 295 for _, m := range b.Members { 296 // Add the member 297 bm[b.Role][m] = true 298 } 299 } 300 return bm 301 } 302 303 // Merge multiple Bindings such that Bindings with the same Role result in 304 // a single Binding with combined Members 305 func saMergeBindings(bindings []*iam.Binding) []*iam.Binding { 306 bm := saRolesToMembersMap(bindings) 307 rb := make([]*iam.Binding, 0) 308 309 for role, members := range bm { 310 var b iam.Binding 311 b.Role = role 312 b.Members = make([]string, 0) 313 for m, _ := range members { 314 b.Members = append(b.Members, m) 315 } 316 rb = append(rb, &b) 317 } 318 319 return rb 320 }