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