2016-11-23 06:55:40 +00:00
package google
import (
"encoding/json"
"fmt"
"log"
"sort"
2017-07-04 02:01:08 +00:00
"github.com/hashicorp/errwrap"
2016-11-23 06:55:40 +00:00
"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 ,
2017-12-11 18:24:53 +00:00
Importer : & schema . ResourceImporter {
State : schema . ImportStatePassthrough ,
} ,
2016-11-23 06:55:40 +00:00
Schema : map [ string ] * schema . Schema {
"project" : & schema . Schema {
Type : schema . TypeString ,
2017-11-06 21:27:20 +00:00
Optional : true ,
2017-11-28 00:32:20 +00:00
Computed : true ,
2016-11-23 06:55:40 +00:00
ForceNew : true ,
} ,
"policy_data" : & schema . Schema {
Type : schema . TypeString ,
Required : true ,
DiffSuppressFunc : jsonPolicyDiffSuppress ,
} ,
"authoritative" : & schema . Schema {
2017-09-28 22:36:40 +00:00
Type : schema . TypeBool ,
Optional : true ,
2018-05-14 17:11:11 +00:00
Deprecated : "A future version of Terraform will remove the authoritative field. To ignore changes not managed by Terraform, use google_project_iam_binding and google_project_iam_member instead. See https://www.terraform.io/docs/providers/google/r/google_project_iam.html for more information." ,
2016-11-23 06:55:40 +00:00
} ,
"etag" : & schema . Schema {
Type : schema . TypeString ,
Computed : true ,
} ,
"restore_policy" : & schema . Schema {
Type : schema . TypeString ,
Computed : true ,
} ,
"disable_project" : & schema . Schema {
2017-09-29 18:13:44 +00:00
Deprecated : "This will be removed with the authoritative field. Use lifecycle.prevent_destroy instead." ,
Type : schema . TypeBool ,
Optional : true ,
2016-11-23 06:55:40 +00:00
} ,
} ,
}
}
func resourceGoogleProjectIamPolicyCreate ( d * schema . ResourceData , meta interface { } ) error {
config := meta . ( * Config )
2017-11-06 21:27:20 +00:00
pid , err := getProject ( d , config )
if err != nil {
return err
}
2018-06-13 19:35:49 +00:00
mutexKey := getProjectIamPolicyMutexKey ( pid )
mutexKV . Lock ( mutexKey )
defer mutexKV . Unlock ( mutexKey )
2016-11-23 06:55:40 +00:00
// 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 )
2017-11-06 21:27:20 +00:00
pid , err := getProject ( d , config )
if err != nil {
return err
}
2016-11-23 06:55:40 +00:00
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 ) )
2017-11-28 00:32:20 +00:00
d . Set ( "project" , pid )
2016-11-23 06:55:40 +00:00
return nil
}
func resourceGoogleProjectIamPolicyUpdate ( d * schema . ResourceData , meta interface { } ) error {
log . Printf ( "[DEBUG]: Updating google_project_iam_policy" )
config := meta . ( * Config )
2017-11-06 21:27:20 +00:00
pid , err := getProject ( d , config )
if err != nil {
return err
}
2016-11-23 06:55:40 +00:00
2018-06-13 19:35:49 +00:00
mutexKey := getProjectIamPolicyMutexKey ( pid )
mutexKV . Lock ( mutexKey )
defer mutexKV . Unlock ( mutexKey )
2016-11-23 06:55:40 +00:00
// 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 )
2017-11-06 21:27:20 +00:00
pid , err := getProject ( d , config )
if err != nil {
return err
}
2016-11-23 06:55:40 +00:00
2018-06-13 19:35:49 +00:00
mutexKey := getProjectIamPolicyMutexKey ( pid )
mutexKV . Lock ( mutexKey )
defer mutexKV . Unlock ( mutexKey )
2016-11-23 06:55:40 +00:00
// 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 {
2017-09-14 16:30:06 +00:00
return errwrap . Wrapf ( fmt . Sprintf ( "Error applying IAM policy for project %q. Policy is %#v, error is {{err}}" , pid , policy ) , err )
2016-11-23 06:55:40 +00:00
}
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
}
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
}
2017-03-02 22:00:45 +00:00
oldPolicy . Bindings = mergeBindings ( oldPolicy . Bindings )
newPolicy . Bindings = mergeBindings ( newPolicy . Bindings )
2016-11-23 06:55:40 +00:00
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
}
2018-06-13 19:35:49 +00:00
func getProjectIamPolicyMutexKey ( pid string ) string {
return fmt . Sprintf ( "iam-project-%s" , pid )
}