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