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