1package tfc 2 3import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/hashicorp/vault/sdk/framework" 9 "github.com/hashicorp/vault/sdk/logical" 10) 11 12// terraformRoleEntry is a Vault role construct that maps to TFC/TFE 13type terraformRoleEntry struct { 14 Name string `json:"name"` 15 Organization string `json:"organization,omitempty"` 16 TeamID string `json:"team_id,omitempty"` 17 UserID string `json:"user_id,omitempty"` 18 TTL time.Duration `json:"ttl"` 19 MaxTTL time.Duration `json:"max_ttl"` 20 Token string `json:"token,omitempty"` 21 TokenID string `json:"token_id,omitempty"` 22} 23 24func (r *terraformRoleEntry) toResponseData() map[string]interface{} { 25 respData := map[string]interface{}{ 26 "name": r.Name, 27 "ttl": r.TTL.Seconds(), 28 "max_ttl": r.MaxTTL.Seconds(), 29 } 30 if r.Organization != "" { 31 respData["organization"] = r.Organization 32 } 33 if r.TeamID != "" { 34 respData["team_id"] = r.TeamID 35 } 36 if r.UserID != "" { 37 respData["user_id"] = r.UserID 38 } 39 return respData 40} 41 42func pathRole(b *tfBackend) []*framework.Path { 43 return []*framework.Path{ 44 { 45 Pattern: "role/" + framework.GenericNameRegex("name"), 46 Fields: map[string]*framework.FieldSchema{ 47 "name": { 48 Type: framework.TypeLowerCaseString, 49 Description: "Name of the role", 50 Required: true, 51 }, 52 "organization": { 53 Type: framework.TypeString, 54 Description: "Name of the Terraform Cloud or Enterprise organization", 55 }, 56 "team_id": { 57 Type: framework.TypeString, 58 Description: "ID of the Terraform Cloud or Enterprise team under organization (e.g., settings/teams/team-xxxxxxxxxxxxx)", 59 }, 60 "user_id": { 61 Type: framework.TypeString, 62 Description: "ID of the Terraform Cloud or Enterprise user (e.g., user-xxxxxxxxxxxxxxxx)", 63 }, 64 "ttl": { 65 Type: framework.TypeDurationSecond, 66 Description: "Default lease for generated credentials. If not set or set to 0, will use system default.", 67 }, 68 "max_ttl": { 69 Type: framework.TypeDurationSecond, 70 Description: "Maximum time for role. If not set or set to 0, will use system default.", 71 }, 72 }, 73 Operations: map[logical.Operation]framework.OperationHandler{ 74 logical.ReadOperation: &framework.PathOperation{ 75 Callback: b.pathRolesRead, 76 }, 77 logical.CreateOperation: &framework.PathOperation{ 78 Callback: b.pathRolesWrite, 79 }, 80 logical.UpdateOperation: &framework.PathOperation{ 81 Callback: b.pathRolesWrite, 82 }, 83 logical.DeleteOperation: &framework.PathOperation{ 84 Callback: b.pathRolesDelete, 85 }, 86 }, 87 HelpSynopsis: pathRoleHelpSynopsis, 88 HelpDescription: pathRoleHelpDescription, 89 }, 90 { 91 Pattern: "role/?$", 92 93 Operations: map[logical.Operation]framework.OperationHandler{ 94 logical.ListOperation: &framework.PathOperation{ 95 Callback: b.pathRolesList, 96 }, 97 }, 98 99 HelpSynopsis: pathRoleListHelpSynopsis, 100 HelpDescription: pathRoleListHelpDescription, 101 }, 102 } 103} 104 105func (b *tfBackend) pathRolesList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 106 entries, err := req.Storage.List(ctx, "role/") 107 if err != nil { 108 return nil, err 109 } 110 111 return logical.ListResponse(entries), nil 112} 113 114func (b *tfBackend) pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 115 entry, err := b.getRole(ctx, req.Storage, d.Get("name").(string)) 116 if err != nil { 117 return nil, err 118 } 119 120 if entry == nil { 121 return nil, nil 122 } 123 124 return &logical.Response{ 125 Data: entry.toResponseData(), 126 }, nil 127} 128 129func (b *tfBackend) pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 130 name := d.Get("name").(string) 131 if name == "" { 132 return logical.ErrorResponse("missing role name"), nil 133 } 134 135 roleEntry, err := b.getRole(ctx, req.Storage, name) 136 if err != nil { 137 return nil, err 138 } 139 140 if roleEntry == nil { 141 roleEntry = &terraformRoleEntry{} 142 } 143 144 createOperation := (req.Operation == logical.CreateOperation) 145 146 roleEntry.Name = name 147 if organization, ok := d.GetOk("organization"); ok { 148 roleEntry.Organization = organization.(string) 149 } else if createOperation { 150 roleEntry.Organization = d.Get("organization").(string) 151 } 152 153 if teamID, ok := d.GetOk("team_id"); ok { 154 roleEntry.TeamID = teamID.(string) 155 } else if createOperation { 156 roleEntry.TeamID = d.Get("team_id").(string) 157 } 158 159 if userID, ok := d.GetOk("user_id"); ok { 160 roleEntry.UserID = userID.(string) 161 } else if createOperation { 162 roleEntry.UserID = d.Get("user_id").(string) 163 } 164 165 if roleEntry.UserID != "" && (roleEntry.Organization != "" || roleEntry.TeamID != "") { 166 return logical.ErrorResponse("cannot provide a user_id in combination with organization or team_id"), nil 167 } 168 169 if roleEntry.UserID == "" && roleEntry.Organization == "" && roleEntry.TeamID == "" { 170 return logical.ErrorResponse("must provide an organization name, team id, or user id"), nil 171 } 172 173 if ttlRaw, ok := d.GetOk("ttl"); ok { 174 roleEntry.TTL = time.Duration(ttlRaw.(int)) * time.Second 175 } else if req.Operation == logical.CreateOperation { 176 roleEntry.TTL = time.Duration(d.Get("ttl").(int)) * time.Second 177 } 178 179 if maxTTLRaw, ok := d.GetOk("max_ttl"); ok { 180 roleEntry.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second 181 } else if req.Operation == logical.CreateOperation { 182 roleEntry.MaxTTL = time.Duration(d.Get("max_ttl").(int)) * time.Second 183 } 184 185 if roleEntry.MaxTTL != 0 && roleEntry.TTL > roleEntry.MaxTTL { 186 return logical.ErrorResponse("ttl cannot be greater than max_ttl"), nil 187 } 188 189 // if we're creating a role to manage a Team or Organization, we need to 190 // create the token now. User tokens will be created when credentials are 191 // read. 192 if roleEntry.Organization != "" || roleEntry.TeamID != "" { 193 token, err := b.createToken(ctx, req.Storage, roleEntry) 194 if err != nil { 195 return nil, err 196 } 197 198 roleEntry.Token = token.Token 199 roleEntry.TokenID = token.ID 200 } 201 202 if err := setRole(ctx, req.Storage, name, roleEntry); err != nil { 203 return nil, err 204 } 205 206 return nil, nil 207} 208 209func (b *tfBackend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 210 err := req.Storage.Delete(ctx, "role/"+d.Get("name").(string)) 211 if err != nil { 212 return nil, fmt.Errorf("error deleting terraform role: %w", err) 213 } 214 215 return nil, nil 216} 217 218func setRole(ctx context.Context, s logical.Storage, name string, roleEntry *terraformRoleEntry) error { 219 entry, err := logical.StorageEntryJSON("role/"+name, roleEntry) 220 if err != nil { 221 return err 222 } 223 224 if entry == nil { 225 return fmt.Errorf("failed to create storage entry for role") 226 } 227 228 if err := s.Put(ctx, entry); err != nil { 229 return err 230 } 231 232 return nil 233} 234 235func (b *tfBackend) getRole(ctx context.Context, s logical.Storage, name string) (*terraformRoleEntry, error) { 236 if name == "" { 237 return nil, fmt.Errorf("missing role name") 238 } 239 240 entry, err := s.Get(ctx, "role/"+name) 241 if err != nil { 242 return nil, err 243 } 244 245 if entry == nil { 246 return nil, nil 247 } 248 249 var role terraformRoleEntry 250 251 if err := entry.DecodeJSON(&role); err != nil { 252 return nil, err 253 } 254 return &role, nil 255} 256 257const ( 258 pathRoleHelpSynopsis = `Manages the Vault role for generating Terraform Cloud / Enterprise tokens.` 259 pathRoleHelpDescription = ` 260This path allows you to read and write roles used to generate Terraform Cloud / 261Enterprise tokens. You can configure a role to manage an organization's token, a 262team's token, or a user's dynamic tokens. 263 264A Terraform Cloud/Enterprise Organization can only have one active token at a 265time. To manage an Organization's token, set the organization field. 266 267A Terraform Cloud/Enterprise Team can only have one active token at a time. To 268manage a Teams's token, set the team_id field. 269 270A Terraform Cloud/Enterprise User can have multiple API tokens. To manage a 271User's token, set the user_id field. 272` 273 274 pathRoleListHelpSynopsis = `List the existing roles in Terraform Cloud / Enterprise backend` 275 pathRoleListHelpDescription = `Roles will be listed by the role name.` 276) 277