1package azuresecrets 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/Azure/azure-sdk-for-go/services/preview/authorization/mgmt/2018-01-01-preview/authorization" 11 "github.com/Azure/go-autorest/autorest/to" 12 "github.com/hashicorp/errwrap" 13 "github.com/hashicorp/vault/sdk/framework" 14 "github.com/hashicorp/vault/sdk/helper/jsonutil" 15 "github.com/hashicorp/vault/sdk/logical" 16) 17 18const ( 19 rolesStoragePath = "roles" 20 21 credentialTypeSP = 0 22) 23 24// Role is a Vault role construct that maps to Azure roles or Applications 25type Role struct { 26 CredentialType int `json:"credential_type"` // Reserved. Always SP at this time. 27 AzureRoles []*azureRole `json:"azure_roles"` 28 ApplicationID string `json:"application_id"` 29 ApplicationObjectID string `json:"application_object_id"` 30 TTL time.Duration `json:"ttl"` 31 MaxTTL time.Duration `json:"max_ttl"` 32} 33 34// azureRole is an Azure Role (https://docs.microsoft.com/en-us/azure/role-based-access-control/overview) applied 35// to a scope. RoleName and RoleID are both traits of the role. RoleID is the unique identifier, but RoleName is 36// more useful to a human (thought it is not unique). 37type azureRole struct { 38 RoleName string `json:"role_name"` // e.g. Owner 39 RoleID string `json:"role_id"` // e.g. /subscriptions/e0a207b2-.../providers/Microsoft.Authorization/roleDefinitions/de139f84-... 40 Scope string `json:"scope"` // e.g. /subscriptions/e0a207b2-... 41} 42 43func pathsRole(b *azureSecretBackend) []*framework.Path { 44 return []*framework.Path{ 45 { 46 Pattern: "roles/" + framework.GenericNameRegex("name"), 47 Fields: map[string]*framework.FieldSchema{ 48 "name": { 49 Type: framework.TypeLowerCaseString, 50 Description: "Name of the role.", 51 }, 52 "application_object_id": { 53 Type: framework.TypeString, 54 Description: "Application Object ID to use for static service principal credentials.", 55 }, 56 "azure_roles": { 57 Type: framework.TypeString, 58 Description: "JSON list of Azure roles to assign.", 59 }, 60 "ttl": { 61 Type: framework.TypeDurationSecond, 62 Description: "Default lease for generated credentials. If not set or set to 0, will use system default.", 63 }, 64 "max_ttl": { 65 Type: framework.TypeDurationSecond, 66 Description: "Maximum time a service principal. If not set or set to 0, will use system default.", 67 }, 68 }, 69 Callbacks: map[logical.Operation]framework.OperationFunc{ 70 logical.ReadOperation: b.pathRoleRead, 71 logical.CreateOperation: b.pathRoleUpdate, 72 logical.UpdateOperation: b.pathRoleUpdate, 73 logical.DeleteOperation: b.pathRoleDelete, 74 }, 75 HelpSynopsis: roleHelpSyn, 76 HelpDescription: roleHelpDesc, 77 ExistenceCheck: b.pathRoleExistenceCheck, 78 }, 79 { 80 Pattern: "roles/?", 81 Callbacks: map[logical.Operation]framework.OperationFunc{ 82 logical.ListOperation: b.pathRoleList, 83 }, 84 HelpSynopsis: roleListHelpSyn, 85 HelpDescription: roleListHelpDesc, 86 }, 87 } 88 89} 90 91// pathRoleUpdate creates or updates Vault roles. 92// 93// Basic validity check are made to verify that the provided fields meet requirements 94// for the given credential type. 95// 96// Dynamic Service Principal: 97// Azure roles are checked for existence. The Azure role lookup step will allow the 98// operator to provide a role name or ID. ID is unambigious and will be used if provided. 99// Given just role name, a search will be performed and if exactly one match is found, 100// that role will be used. 101// 102// Static Service Principal: 103// The provided Application Object ID is checked for existence. 104func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 105 var resp *logical.Response 106 107 client, err := b.getClient(ctx, req.Storage) 108 if err != nil { 109 return nil, err 110 } 111 112 // load or create role 113 name := d.Get("name").(string) 114 role, err := getRole(ctx, name, req.Storage) 115 if err != nil { 116 return nil, errwrap.Wrapf("error reading role: {{err}}", err) 117 } 118 119 if role == nil { 120 if req.Operation == logical.UpdateOperation { 121 return nil, errors.New("role entry not found during update operation") 122 } 123 role = &Role{ 124 CredentialType: credentialTypeSP, 125 } 126 } 127 128 // load and validate TTLs 129 if ttlRaw, ok := d.GetOk("ttl"); ok { 130 role.TTL = time.Duration(ttlRaw.(int)) * time.Second 131 } else if req.Operation == logical.CreateOperation { 132 role.TTL = time.Duration(d.Get("ttl").(int)) * time.Second 133 } 134 135 if maxTTLRaw, ok := d.GetOk("max_ttl"); ok { 136 role.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second 137 } else if req.Operation == logical.CreateOperation { 138 role.MaxTTL = time.Duration(d.Get("max_ttl").(int)) * time.Second 139 } 140 141 if role.MaxTTL != 0 && role.TTL > role.MaxTTL { 142 return logical.ErrorResponse("ttl cannot be greater than max_ttl"), nil 143 } 144 145 // update and verify Application Object ID if provided 146 if appObjectID, ok := d.GetOk("application_object_id"); ok { 147 role.ApplicationObjectID = appObjectID.(string) 148 } 149 150 if role.ApplicationObjectID != "" { 151 app, err := client.provider.GetApplication(ctx, role.ApplicationObjectID) 152 if err != nil { 153 return nil, errwrap.Wrapf("error loading Application: {{err}}", err) 154 } 155 role.ApplicationID = to.String(app.AppID) 156 } 157 158 // update and verify Azure roles, including looking up each role by ID or name. 159 if roles, ok := d.GetOk("azure_roles"); ok { 160 parsedRoles := make([]*azureRole, 0) // non-nil to avoid a "missing roles" error later 161 162 err := jsonutil.DecodeJSON([]byte(roles.(string)), &parsedRoles) 163 if err != nil { 164 return logical.ErrorResponse(fmt.Sprintf("error parsing Azure roles '%s': %s", roles.(string), err.Error())), nil 165 } 166 role.AzureRoles = parsedRoles 167 } 168 169 roleSet := make(map[string]bool) 170 for _, r := range role.AzureRoles { 171 var roleDef authorization.RoleDefinition 172 if r.RoleID != "" { 173 roleDef, err = client.provider.GetRoleByID(ctx, r.RoleID) 174 if err != nil { 175 if strings.Contains(err.Error(), "RoleDefinitionDoesNotExist") { 176 return logical.ErrorResponse(fmt.Sprintf("no role found for role_id: '%s'", r.RoleID)), nil 177 } 178 return nil, errwrap.Wrapf("unable to lookup Azure role: {{err}}", err) 179 } 180 } else { 181 defs, err := client.findRoles(ctx, r.RoleName) 182 if err != nil { 183 return nil, errwrap.Wrapf("unable to lookup Azure role: {{err}}", err) 184 } 185 if l := len(defs); l == 0 { 186 return logical.ErrorResponse(fmt.Sprintf("no role found for role_name: '%s'", r.RoleName)), nil 187 } else if l > 1 { 188 return logical.ErrorResponse(fmt.Sprintf("multiple matches found for role_name: '%s'. Specify role by ID instead.", r.RoleName)), nil 189 } 190 roleDef = defs[0] 191 } 192 193 roleDefID := to.String(roleDef.ID) 194 roleDefName := to.String(roleDef.RoleName) 195 196 r.RoleName, r.RoleID = roleDefName, roleDefID 197 198 rsKey := r.RoleID + "||" + r.Scope 199 if roleSet[rsKey] { 200 return logical.ErrorResponse(fmt.Sprintf("duplicate role_id and scope: '%s', '%s'", r.RoleID, r.Scope)), nil 201 } 202 roleSet[rsKey] = true 203 } 204 205 if role.ApplicationObjectID == "" && len(role.AzureRoles) == 0 { 206 return logical.ErrorResponse("either Azure role definitions or an Application Object ID must be provided"), nil 207 } 208 209 // save role 210 err = saveRole(ctx, req.Storage, role, name) 211 if err != nil { 212 return nil, errwrap.Wrapf("error storing role: {{err}}", err) 213 } 214 215 return resp, nil 216} 217 218func (b *azureSecretBackend) pathRoleRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 219 var data = make(map[string]interface{}) 220 221 name := d.Get("name").(string) 222 223 r, err := getRole(ctx, name, req.Storage) 224 if err != nil { 225 return nil, errwrap.Wrapf("error reading role: {{err}}", err) 226 } 227 228 if r == nil { 229 return nil, nil 230 } 231 232 data["ttl"] = r.TTL / time.Second 233 data["max_ttl"] = r.MaxTTL / time.Second 234 data["azure_roles"] = r.AzureRoles 235 data["application_object_id"] = r.ApplicationObjectID 236 237 return &logical.Response{ 238 Data: data, 239 }, nil 240} 241 242func (b *azureSecretBackend) pathRoleList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 243 roles, err := req.Storage.List(ctx, rolesStoragePath+"/") 244 if err != nil { 245 return nil, errwrap.Wrapf("error listing roles: {{err}}", err) 246 } 247 248 return logical.ListResponse(roles), nil 249} 250 251func (b *azureSecretBackend) pathRoleDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 252 name := d.Get("name").(string) 253 254 err := req.Storage.Delete(ctx, fmt.Sprintf("%s/%s", rolesStoragePath, name)) 255 if err != nil { 256 return nil, errwrap.Wrapf("error deleting role: {{err}}", err) 257 } 258 259 return nil, nil 260} 261 262func (b *azureSecretBackend) pathRoleExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) { 263 name := d.Get("name").(string) 264 265 role, err := getRole(ctx, name, req.Storage) 266 if err != nil { 267 return false, errwrap.Wrapf("error reading role: {{err}}", err) 268 } 269 270 return role != nil, nil 271} 272 273func saveRole(ctx context.Context, s logical.Storage, c *Role, name string) error { 274 entry, err := logical.StorageEntryJSON(fmt.Sprintf("%s/%s", rolesStoragePath, name), c) 275 if err != nil { 276 return err 277 } 278 279 return s.Put(ctx, entry) 280} 281 282func getRole(ctx context.Context, name string, s logical.Storage) (*Role, error) { 283 entry, err := s.Get(ctx, fmt.Sprintf("%s/%s", rolesStoragePath, name)) 284 if err != nil { 285 return nil, err 286 } 287 288 if entry == nil { 289 return nil, nil 290 } 291 292 role := new(Role) 293 if err := entry.DecodeJSON(role); err != nil { 294 return nil, err 295 } 296 return role, nil 297} 298 299const roleHelpSyn = "Manage the Vault roles used to generate Azure credentials." 300const roleHelpDesc = ` 301This path allows you to read and write roles that are used to generate Azure login 302credentials. These roles are associated with either an existing Application, or a set 303of Azure roles, which are used to control permissions to Azure resources. 304 305If the backend is mounted at "azure", you would create a Vault role at "azure/roles/my_role", 306and request credentials from "azure/creds/my_role". 307 308Each Vault role is configured with the standard ttl parameters and either a list of Azure 309roles and scopes, or an Application Object ID. Any Azure roles will be fetched during the 310Vault role creation and must exist for the request to succeed. Similarly, the Application 311Object ID will be verified if provided. When a used requests credentials against the Vault 312role, a new password will be created for the Application if an Application Object ID was 313configured. Otherwise, a new service principal will be created and the configured set of 314Azure roles are assigned to it. 315` 316const roleListHelpSyn = `List existing roles.` 317const roleListHelpDesc = `List existing roles by name.` 318