1package azuresecrets
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"math/rand"
8	"os"
9	"strings"
10	"time"
11
12	"github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac"
13	"github.com/Azure/azure-sdk-for-go/services/preview/authorization/mgmt/2018-01-01-preview/authorization"
14	"github.com/Azure/go-autorest/autorest/azure"
15	"github.com/Azure/go-autorest/autorest/date"
16	"github.com/Azure/go-autorest/autorest/to"
17	"github.com/hashicorp/errwrap"
18	multierror "github.com/hashicorp/go-multierror"
19	uuid "github.com/hashicorp/go-uuid"
20	"github.com/hashicorp/vault/sdk/logical"
21)
22
23const (
24	appNamePrefix  = "vault-"
25	retryTimeout   = 80 * time.Second
26	clientLifetime = 30 * time.Minute
27)
28
29// client offers higher level Azure operations that provide a simpler interface
30// for handlers. It in turn relies on a Provider interface to access the lower level
31// Azure Client SDK methods.
32type client struct {
33	provider   AzureProvider
34	settings   *clientSettings
35	expiration time.Time
36	passwords  passwords
37}
38
39// Valid returns whether the client defined and not expired.
40func (c *client) Valid() bool {
41	return c != nil && time.Now().Before(c.expiration)
42}
43
44// createApp creates a new Azure application.
45// An Application is a needed to create service principals used by
46// the caller for authentication.
47func (c *client) createApp(ctx context.Context) (app *graphrbac.Application, err error) {
48	name, err := uuid.GenerateUUID()
49	if err != nil {
50		return nil, err
51	}
52
53	name = appNamePrefix + name
54
55	appURL := fmt.Sprintf("https://%s", name)
56
57	result, err := c.provider.CreateApplication(ctx, graphrbac.ApplicationCreateParameters{
58		AvailableToOtherTenants: to.BoolPtr(false),
59		DisplayName:             to.StringPtr(name),
60		Homepage:                to.StringPtr(appURL),
61		IdentifierUris:          to.StringSlicePtr([]string{appURL}),
62	})
63
64	return &result, err
65}
66
67// createSP creates a new service principal.
68func (c *client) createSP(
69	ctx context.Context,
70	app *graphrbac.Application,
71	duration time.Duration) (svcPrinc *graphrbac.ServicePrincipal, password string, err error) {
72
73	// Generate a random key (which must be a UUID) and password
74	keyID, err := uuid.GenerateUUID()
75	if err != nil {
76		return nil, "", err
77	}
78
79	password, err = c.passwords.generate(ctx)
80	if err != nil {
81		return nil, "", err
82	}
83
84	resultRaw, err := retry(ctx, func() (interface{}, bool, error) {
85		now := time.Now().UTC()
86		result, err := c.provider.CreateServicePrincipal(ctx, graphrbac.ServicePrincipalCreateParameters{
87			AppID:          app.AppID,
88			AccountEnabled: to.BoolPtr(true),
89			PasswordCredentials: &[]graphrbac.PasswordCredential{
90				graphrbac.PasswordCredential{
91					StartDate: &date.Time{Time: now},
92					EndDate:   &date.Time{Time: now.Add(duration)},
93					KeyID:     to.StringPtr(keyID),
94					Value:     to.StringPtr(password),
95				},
96			},
97		})
98
99		// Propagation delays within Azure can cause this error occasionally, so don't quit on it.
100		if err != nil && strings.Contains(err.Error(), "does not reference a valid application object") {
101			return nil, false, nil
102		}
103
104		return result, true, err
105	})
106
107	if err != nil {
108		return nil, "", errwrap.Wrapf("error creating service principal: {{err}}", err)
109	}
110
111	result := resultRaw.(graphrbac.ServicePrincipal)
112
113	return &result, password, nil
114}
115
116// addAppPassword adds a new password to an App's credentials list.
117func (c *client) addAppPassword(ctx context.Context, appObjID string, duration time.Duration) (keyID string, password string, err error) {
118	keyID, err = uuid.GenerateUUID()
119	if err != nil {
120		return "", "", err
121	}
122
123	// Key IDs are not secret, and they're a convenient way for an operator to identify Vault-generated
124	// passwords. These must be UUIDs, so the three leading bytes will be used as an indicator.
125	keyID = "ffffff" + keyID[6:]
126
127	password, err = c.passwords.generate(ctx)
128	if err != nil {
129		return "", "", err
130	}
131
132	now := time.Now().UTC()
133	cred := graphrbac.PasswordCredential{
134		StartDate: &date.Time{Time: now},
135		EndDate:   &date.Time{Time: now.Add(duration)},
136		KeyID:     to.StringPtr(keyID),
137		Value:     to.StringPtr(password),
138	}
139
140	// Load current credentials
141	resp, err := c.provider.ListApplicationPasswordCredentials(ctx, appObjID)
142	if err != nil {
143		return "", "", errwrap.Wrapf("error fetching credentials: {{err}}", err)
144	}
145	curCreds := *resp.Value
146
147	// Add and save credentials
148	curCreds = append(curCreds, cred)
149
150	if _, err := c.provider.UpdateApplicationPasswordCredentials(ctx, appObjID,
151		graphrbac.PasswordCredentialsUpdateParameters{
152			Value: &curCreds,
153		},
154	); err != nil {
155		if strings.Contains(err.Error(), "size of the object has exceeded its limit") {
156			err = errors.New("maximum number of Application passwords reached")
157		}
158		return "", "", errwrap.Wrapf("error updating credentials: {{err}}", err)
159	}
160
161	return keyID, password, nil
162}
163
164// deleteAppPassword removes a password, if present, from an App's credentials list.
165func (c *client) deleteAppPassword(ctx context.Context, appObjID, keyID string) error {
166	// Load current credentials
167	resp, err := c.provider.ListApplicationPasswordCredentials(ctx, appObjID)
168	if err != nil {
169		return errwrap.Wrapf("error fetching credentials: {{err}}", err)
170	}
171	curCreds := *resp.Value
172
173	// Remove credential
174	found := false
175	for i := range curCreds {
176		if to.String(curCreds[i].KeyID) == keyID {
177			curCreds[i] = curCreds[len(curCreds)-1]
178			curCreds = curCreds[:len(curCreds)-1]
179			found = true
180			break
181		}
182	}
183
184	// KeyID is not present, so nothing to do
185	if !found {
186		return nil
187	}
188
189	// Save new credentials list
190	if _, err := c.provider.UpdateApplicationPasswordCredentials(ctx, appObjID,
191		graphrbac.PasswordCredentialsUpdateParameters{
192			Value: &curCreds,
193		},
194	); err != nil {
195		return errwrap.Wrapf("error updating credentials: {{err}}", err)
196	}
197
198	return nil
199}
200
201// deleteApp deletes an Azure application.
202func (c *client) deleteApp(ctx context.Context, appObjectID string) error {
203	resp, err := c.provider.DeleteApplication(ctx, appObjectID)
204
205	// Don't consider it an error if the object wasn't present
206	if err != nil && resp.Response != nil && resp.StatusCode == 404 {
207		return nil
208	}
209
210	return err
211}
212
213// assignRoles assigns Azure roles to a service principal.
214func (c *client) assignRoles(ctx context.Context, sp *graphrbac.ServicePrincipal, roles []*AzureRole) ([]string, error) {
215	var ids []string
216
217	for _, role := range roles {
218		assignmentID, err := uuid.GenerateUUID()
219		if err != nil {
220			return nil, err
221		}
222
223		resultRaw, err := retry(ctx, func() (interface{}, bool, error) {
224			ra, err := c.provider.CreateRoleAssignment(ctx, role.Scope, assignmentID,
225				authorization.RoleAssignmentCreateParameters{
226					RoleAssignmentProperties: &authorization.RoleAssignmentProperties{
227						RoleDefinitionID: to.StringPtr(role.RoleID),
228						PrincipalID:      sp.ObjectID,
229					},
230				})
231
232			// Propagation delays within Azure can cause this error occasionally, so don't quit on it.
233			if err != nil && strings.Contains(err.Error(), "PrincipalNotFound") {
234				return nil, false, nil
235			}
236
237			return to.String(ra.ID), true, err
238		})
239
240		if err != nil {
241			return nil, errwrap.Wrapf("error while assigning roles: {{err}}", err)
242		}
243
244		ids = append(ids, resultRaw.(string))
245	}
246
247	return ids, nil
248}
249
250// unassignRoles deletes role assignments, if they existed.
251// This is a clean-up operation that isn't essential to revocation. As such, an
252// attempt is made to remove all assignments, and not return immediately if there
253// is an error.
254func (c *client) unassignRoles(ctx context.Context, roleIDs []string) error {
255	var merr *multierror.Error
256
257	for _, id := range roleIDs {
258		if _, err := c.provider.DeleteRoleAssignmentByID(ctx, id); err != nil {
259			merr = multierror.Append(merr, errwrap.Wrapf("error unassigning role: {{err}}", err))
260		}
261	}
262
263	return merr.ErrorOrNil()
264}
265
266// addGroupMemberships adds the service principal to the Azure groups.
267func (c *client) addGroupMemberships(ctx context.Context, sp *graphrbac.ServicePrincipal, groups []*AzureGroup) error {
268	for _, group := range groups {
269		_, err := retry(ctx, func() (interface{}, bool, error) {
270			_, err := c.provider.AddGroupMember(ctx, group.ObjectID,
271				graphrbac.GroupAddMemberParameters{
272					URL: to.StringPtr(
273						fmt.Sprintf("%s%s/directoryObjects/%s",
274							c.settings.Environment.GraphEndpoint,
275							c.settings.TenantID,
276							*sp.ObjectID,
277						),
278					),
279				})
280
281			// Propagation delays within Azure can cause this error occasionally, so don't quit on it.
282			if err != nil && strings.Contains(err.Error(), "Request_ResourceNotFound") {
283				return nil, false, nil
284			}
285
286			return nil, true, err
287		})
288
289		if err != nil {
290			return errwrap.Wrapf("error while adding group membership: {{err}}", err)
291		}
292	}
293
294	return nil
295}
296
297// removeGroupMemberships removes the passed service principal from the passed
298// groups. This is a clean-up operation that isn't essential to revocation. As
299// such, an attempt is made to remove all memberships, and not return
300// immediately if there is an error.
301func (c *client) removeGroupMemberships(ctx context.Context, servicePrincipalObjectID string, groupIDs []string) error {
302	var merr *multierror.Error
303
304	for _, id := range groupIDs {
305		if _, err := c.provider.RemoveGroupMember(ctx, servicePrincipalObjectID, id); err != nil {
306			merr = multierror.Append(merr, errwrap.Wrapf("error removing group membership: {{err}}", err))
307		}
308	}
309
310	return merr.ErrorOrNil()
311}
312
313// groupObjectIDs is a helper for converting a list of AzureGroup
314// objects to a list of their object IDs.
315func groupObjectIDs(groups []*AzureGroup) []string {
316	groupIDs := make([]string, 0, len(groups))
317	for _, group := range groups {
318		groupIDs = append(groupIDs, group.ObjectID)
319
320	}
321	return groupIDs
322}
323
324// search for roles by name
325func (c *client) findRoles(ctx context.Context, roleName string) ([]authorization.RoleDefinition, error) {
326	return c.provider.ListRoles(ctx, fmt.Sprintf("subscriptions/%s", c.settings.SubscriptionID), fmt.Sprintf("roleName eq '%s'", roleName))
327}
328
329// findGroups is used to find a group by name. It returns all groups matching
330// the passsed name.
331func (c *client) findGroups(ctx context.Context, groupName string) ([]graphrbac.ADGroup, error) {
332	return c.provider.ListGroups(ctx, fmt.Sprintf("displayName eq '%s'", groupName))
333}
334
335// clientSettings is used by a client to configure the connections to Azure.
336// It is created from a combination of Vault config settings and environment variables.
337type clientSettings struct {
338	SubscriptionID string
339	TenantID       string
340	ClientID       string
341	ClientSecret   string
342	Environment    azure.Environment
343	PluginEnv      *logical.PluginEnvironment
344}
345
346// getClientSettings creates a new clientSettings object.
347// Environment variables have higher precedence than stored configuration.
348func (b *azureSecretBackend) getClientSettings(ctx context.Context, config *azureConfig) (*clientSettings, error) {
349	firstAvailable := func(opts ...string) string {
350		for _, s := range opts {
351			if s != "" {
352				return s
353			}
354		}
355		return ""
356	}
357
358	settings := new(clientSettings)
359
360	settings.ClientID = firstAvailable(os.Getenv("AZURE_CLIENT_ID"), config.ClientID)
361	settings.ClientSecret = firstAvailable(os.Getenv("AZURE_CLIENT_SECRET"), config.ClientSecret)
362
363	settings.SubscriptionID = firstAvailable(os.Getenv("AZURE_SUBSCRIPTION_ID"), config.SubscriptionID)
364	if settings.SubscriptionID == "" {
365		return nil, errors.New("subscription_id is required")
366	}
367
368	settings.TenantID = firstAvailable(os.Getenv("AZURE_TENANT_ID"), config.TenantID)
369	if settings.TenantID == "" {
370		return nil, errors.New("tenant_id is required")
371	}
372
373	envName := firstAvailable(os.Getenv("AZURE_ENVIRONMENT"), config.Environment, "AZUREPUBLICCLOUD")
374	env, err := azure.EnvironmentFromName(envName)
375	if err != nil {
376		return nil, err
377	}
378	settings.Environment = env
379
380	pluginEnv, err := b.System().PluginEnv(ctx)
381	if err != nil {
382		return nil, errwrap.Wrapf("error loading plugin environment: {{err}}", err)
383	}
384	settings.PluginEnv = pluginEnv
385
386	return settings, nil
387}
388
389// retry will repeatedly call f until one of:
390//
391//   * f returns true
392//   * the context is cancelled
393//   * 80 seconds elapses. Vault's default request timeout is 90s; we want to expire before then.
394//
395// Delays are random but will average 5 seconds.
396func retry(ctx context.Context, f func() (interface{}, bool, error)) (interface{}, error) {
397	delayTimer := time.NewTimer(0)
398	if _, hasTimeout := ctx.Deadline(); !hasTimeout {
399		var cancel func()
400		ctx, cancel = context.WithTimeout(ctx, retryTimeout)
401		defer cancel()
402	}
403
404	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
405	for {
406		if result, done, err := f(); done {
407			return result, err
408		}
409
410		delay := time.Duration(2000+rng.Intn(6000)) * time.Millisecond
411		delayTimer.Reset(delay)
412
413		select {
414		case <-delayTimer.C:
415			// Retry loop
416		case <-ctx.Done():
417			return nil, fmt.Errorf("retry failed: %w", ctx.Err())
418		}
419	}
420}
421