1package openldap
2
3import (
4	"context"
5	"encoding/base64"
6	"fmt"
7	"path"
8	"time"
9
10	"github.com/go-ldap/ldif"
11	"github.com/hashicorp/go-multierror"
12	"github.com/hashicorp/vault/sdk/framework"
13	"github.com/hashicorp/vault/sdk/helper/parseutil"
14	"github.com/hashicorp/vault/sdk/logical"
15	"github.com/mitchellh/mapstructure"
16)
17
18const (
19	secretCredsType = "creds"
20
21	dynamicRolePath = "role/"
22	dynamicCredPath = "creds/"
23)
24
25func (b *backend) pathDynamicRoles() []*framework.Path {
26	return []*framework.Path{
27		// POST/GET/DELETE role/:name
28		{
29			Pattern: path.Join(dynamicRolePath, framework.GenericNameRegex("name")),
30			Fields: map[string]*framework.FieldSchema{
31				"name": {
32					Type:        framework.TypeLowerCaseString,
33					Description: "Name of the role (lowercase)",
34					Required:    true,
35				},
36				"creation_ldif": {
37					Type:        framework.TypeString,
38					Description: "LDIF string used to create new entities within OpenLDAP. This LDIF can be templated.",
39					Required:    true,
40				},
41				"deletion_ldif": {
42					Type:        framework.TypeString,
43					Description: "LDIF string used to delete entities created within OpenLDAP. This LDIF can be templated.",
44					Required:    true,
45				},
46				"rollback_ldif": {
47					Type:        framework.TypeString,
48					Description: "LDIF string used to rollback changes in the event of a failure to create credentials. This LDIF can be templated.",
49				},
50				"username_template": {
51					Type:        framework.TypeString,
52					Description: "The template used to create a username",
53				},
54				"default_ttl": {
55					Type:        framework.TypeDurationSecond,
56					Description: "Default TTL for dynamic credentials",
57				},
58				"max_ttl": {
59					Type:        framework.TypeDurationSecond,
60					Description: "Max TTL a dynamic credential can be extended to",
61				},
62			},
63			ExistenceCheck: b.pathDynamicRoleExistenceCheck,
64			Operations: map[logical.Operation]framework.OperationHandler{
65				logical.UpdateOperation: &framework.PathOperation{
66					Callback: b.pathDynamicRoleCreateUpdate,
67				},
68				logical.CreateOperation: &framework.PathOperation{
69					Callback: b.pathDynamicRoleCreateUpdate,
70				},
71				logical.ReadOperation: &framework.PathOperation{
72					Callback: b.pathDynamicRoleRead,
73				},
74				logical.DeleteOperation: &framework.PathOperation{
75					Callback: b.pathDynamicRoleDelete,
76				},
77			},
78			HelpSynopsis:    staticRoleHelpSynopsis,
79			HelpDescription: staticRoleHelpDescription,
80		},
81		// LIST role
82		{
83			Pattern: dynamicRolePath + "?$",
84			Operations: map[logical.Operation]framework.OperationHandler{
85				logical.ListOperation: &framework.PathOperation{
86					Callback: b.pathDynamicRoleList,
87				},
88			},
89			HelpSynopsis:    "List all the dynamic roles Vault is currently managing in OpenLDAP.",
90			HelpDescription: "List all the dynamic roles being managed by Vault.",
91		},
92		// GET credentials
93		{
94			Pattern: path.Join(dynamicCredPath, framework.MatchAllRegex("name")),
95			Fields: map[string]*framework.FieldSchema{
96				"name": {
97					Type:        framework.TypeLowerCaseString,
98					Description: "Name of the dynamic role.",
99				},
100			},
101			Operations: map[logical.Operation]framework.OperationHandler{
102				logical.ReadOperation: &framework.PathOperation{
103					Callback: b.pathDynamicCredsRead,
104				},
105			},
106			HelpSynopsis: "Request LDAP credentials for a dynamic role. These credentials are " +
107				"created within OpenLDAP when querying this endpoint.",
108			HelpDescription: "This path requests new LDAP credentials for a certain dynamic role. " +
109				"The credentials are created within OpenLDAP based on the creation_ldif specified " +
110				"within the dynamic role configuration.",
111		},
112	}
113}
114
115func dynamicSecretCreds(b *backend) *framework.Secret {
116	return &framework.Secret{
117		Type: secretCredsType,
118		Fields: map[string]*framework.FieldSchema{
119			"username": {
120				Type:        framework.TypeString,
121				Description: "Username of the generated account",
122			},
123			"password": {
124				Type:        framework.TypeString,
125				Description: "Password to access the generated account",
126			},
127			"distinguished_names": {
128				Type: framework.TypeStringSlice,
129				Description: "List of the distinguished names (DN) created. Each name in this list corresponds to" +
130					"each action taken within the creation_ldif statements. This does not de-duplicate entries, " +
131					"so this will have one entry for each LDIF statement within creation_ldif.",
132			},
133		},
134
135		Renew:  b.secretCredsRenew(),
136		Revoke: b.secretCredsRevoke(),
137	}
138}
139
140func (b *backend) pathDynamicRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
141	rawData := data.Raw
142	err := convertToDuration(rawData, "default_ttl", "max_ttl")
143	if err != nil {
144		return nil, fmt.Errorf("failed to convert TTLs to duration: %w", err)
145	}
146
147	roleName := data.Get("name").(string)
148	dRole, err := retrieveDynamicRole(ctx, req.Storage, roleName)
149	if err != nil {
150		return nil, fmt.Errorf("unable to look for existing role: %w", err)
151	}
152	if dRole == nil {
153		if req.Operation == logical.UpdateOperation {
154			return nil, fmt.Errorf("unable to update role: role does not exist")
155		}
156		dRole = &dynamicRole{}
157	}
158	err = mapstructure.WeakDecode(rawData, dRole)
159	if err != nil {
160		return nil, fmt.Errorf("failed to decode request: %w", err)
161	}
162
163	dRole.CreationLDIF = decodeBase64(dRole.CreationLDIF)
164	dRole.RollbackLDIF = decodeBase64(dRole.RollbackLDIF)
165	dRole.DeletionLDIF = decodeBase64(dRole.DeletionLDIF)
166
167	err = validateDynamicRole(dRole)
168	if err != nil {
169		return nil, err
170	}
171
172	err = storeDynamicRole(ctx, req.Storage, dRole)
173	if err != nil {
174		return nil, fmt.Errorf("failed to save dynamic role: %w", err)
175	}
176
177	return nil, nil
178}
179
180func validateDynamicRole(dRole *dynamicRole) error {
181	if dRole.CreationLDIF == "" {
182		return fmt.Errorf("missing creation_ldif")
183	}
184
185	if dRole.DeletionLDIF == "" {
186		return fmt.Errorf("missing deletion_ldif")
187	}
188
189	err := assertValidLDIFTemplate(dRole.CreationLDIF)
190	if err != nil {
191		return fmt.Errorf("invalid creation_ldif: %w", err)
192	}
193
194	err = assertValidLDIFTemplate(dRole.DeletionLDIF)
195	if err != nil {
196		return fmt.Errorf("invalid deletion_ldif: %w", err)
197	}
198
199	if dRole.RollbackLDIF != "" {
200		err = assertValidLDIFTemplate(dRole.RollbackLDIF)
201		if err != nil {
202			return fmt.Errorf("invalid rollback_ldif: %w", err)
203		}
204	}
205
206	return nil
207}
208
209// convertToDuration all keys in the data map into time.Duration objects. Keys not found in the map will be ignored
210func convertToDuration(data map[string]interface{}, keys ...string) error {
211	merr := new(multierror.Error)
212	for _, key := range keys {
213		val, exists := data[key]
214		if !exists {
215			continue
216		}
217
218		dur, err := parseutil.ParseDurationSecond(val)
219		if err != nil {
220			merr = multierror.Append(merr, fmt.Errorf("invalid duration %s: %w", key, err))
221			continue
222		}
223		data[key] = dur
224	}
225	return merr.ErrorOrNil()
226}
227
228// decodeBase64 attempts to base64 decode the provided string. If the string is not base64 encoded, this
229// returns the original string.
230// This is equivalent to "if string is base64 encoded, decode it and return, otherwise return the original string"
231func decodeBase64(str string) string {
232	if str == "" {
233		return ""
234	}
235	decoded, err := base64.StdEncoding.DecodeString(str)
236	if err != nil {
237		return str
238	}
239	return string(decoded)
240}
241
242func assertValidLDIFTemplate(rawTemplate string) error {
243	// Test the template to ensure there aren't any errors in the template syntax
244	now := time.Now()
245	exp := now.Add(24 * time.Hour)
246	testTemplateData := dynamicTemplateData{
247		Username:              "testuser",
248		Password:              "testpass",
249		DisplayName:           "testdisplayname",
250		RoleName:              "testrolename",
251		IssueTime:             now.Format(time.RFC3339),
252		IssueTimeSeconds:      now.Unix(),
253		ExpirationTime:        exp.Format(time.RFC3339),
254		ExpirationTimeSeconds: exp.Unix(),
255	}
256
257	testLDIF, err := applyTemplate(rawTemplate, testTemplateData)
258	if err != nil {
259		return fmt.Errorf("invalid template: %w", err)
260	}
261
262	// Test the LDIF to ensure there aren't any errors in the syntax
263	entries, err := ldif.Parse(testLDIF)
264	if err != nil {
265		return fmt.Errorf("LDIF is invalid: %w", err)
266	}
267
268	if len(entries.Entries) == 0 {
269		return fmt.Errorf("must specify at least one LDIF entry")
270	}
271
272	return nil
273}
274
275func (b *backend) pathDynamicRoleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
276	roleName := data.Get("name").(string)
277
278	dRole, err := retrieveDynamicRole(ctx, req.Storage, roleName)
279	if err != nil {
280		return nil, fmt.Errorf("failed to retrieve dynamic role: %w", err)
281	}
282	if dRole == nil {
283		return nil, nil
284	}
285
286	resp := &logical.Response{
287		Data: map[string]interface{}{
288			"creation_ldif":     dRole.CreationLDIF,
289			"deletion_ldif":     dRole.DeletionLDIF,
290			"rollback_ldif":     dRole.RollbackLDIF,
291			"username_template": dRole.UsernameTemplate,
292			"default_ttl":       dRole.DefaultTTL.Seconds(),
293			"max_ttl":           dRole.MaxTTL.Seconds(),
294		},
295	}
296	return resp, nil
297}
298
299func (b *backend) pathDynamicRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
300	roles, err := req.Storage.List(ctx, dynamicRolePath)
301	if err != nil {
302		return nil, fmt.Errorf("failed to list roles: %w", err)
303	}
304
305	return logical.ListResponse(roles), nil
306}
307
308func (b *backend) pathDynamicRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
309	roleName := data.Get("name").(string)
310	role, err := retrieveDynamicRole(ctx, req.Storage, roleName)
311	if err != nil {
312		return false, fmt.Errorf("error finding role: %w", err)
313	}
314	return role != nil, nil
315}
316
317func (b *backend) pathDynamicRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
318	roleName := data.Get("name").(string)
319
320	err := deleteDynamicRole(ctx, req.Storage, roleName)
321	if err != nil {
322		return nil, fmt.Errorf("failed to delete role: %w", err)
323	}
324	return nil, nil
325}
326