~juju-qa/ubuntu/xenial/juju/2.0-rc2

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/provider/azure/internal/azureauth/interactive.go

  • Committer: Nicholas Skaggs
  • Date: 2016-09-30 14:39:30 UTC
  • mfrom: (1.8.1)
  • Revision ID: nicholas.skaggs@canonical.com-20160930143930-vwwhrefh6ftckccy
import upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2016 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package azureauth
 
5
 
 
6
import (
 
7
        "fmt"
 
8
        "io"
 
9
        "net/url"
 
10
        "path"
 
11
        "time"
 
12
 
 
13
        "github.com/Azure/azure-sdk-for-go/arm/authorization"
 
14
        "github.com/Azure/azure-sdk-for-go/arm/resources/subscriptions"
 
15
        "github.com/Azure/go-autorest/autorest"
 
16
        "github.com/Azure/go-autorest/autorest/azure"
 
17
        "github.com/Azure/go-autorest/autorest/to"
 
18
        "github.com/juju/errors"
 
19
        "github.com/juju/loggo"
 
20
        "github.com/juju/utils"
 
21
        "github.com/juju/utils/clock"
 
22
 
 
23
        "github.com/juju/juju/provider/azure/internal/ad"
 
24
        "github.com/juju/juju/provider/azure/internal/errorutils"
 
25
        "github.com/juju/juju/provider/azure/internal/tracing"
 
26
)
 
27
 
 
28
var logger = loggo.GetLogger("juju.provider.azure.internal.azureauth")
 
29
 
 
30
const (
 
31
        // jujuApplicationId is the ID of the Azure application that we use
 
32
        // for interactive authentication. When the user logs in, a service
 
33
        // principal will be created in their Active Directory tenant for
 
34
        // the application.
 
35
        jujuApplicationId = "cbb548f1-5039-4836-af0b-727e8571f6a9"
 
36
 
 
37
        // passwordExpiryDuration is how long the application password we
 
38
        // set will remain valid.
 
39
        passwordExpiryDuration = 365 * 24 * time.Hour
 
40
)
 
41
 
 
42
// InteractiveCreateServicePrincipalFunc is a function type for
 
43
// interactively creating service principals for a subscription.
 
44
type InteractiveCreateServicePrincipalFunc func(
 
45
        stderr io.Writer,
 
46
        sender autorest.Sender,
 
47
        requestInspector autorest.PrepareDecorator,
 
48
        resourceManagerEndpoint string,
 
49
        graphEndpoint string,
 
50
        subscriptionId string,
 
51
        clock clock.Clock,
 
52
        newUUID func() (utils.UUID, error),
 
53
) (appId, password string, _ error)
 
54
 
 
55
// InteractiveCreateServicePrincipal interactively creates service
 
56
// principals for a subscription.
 
57
func InteractiveCreateServicePrincipal(
 
58
        stderr io.Writer,
 
59
        sender autorest.Sender,
 
60
        requestInspector autorest.PrepareDecorator,
 
61
        resourceManagerEndpoint string,
 
62
        graphEndpoint string,
 
63
        subscriptionId string,
 
64
        clock clock.Clock,
 
65
        newUUID func() (utils.UUID, error),
 
66
) (appId, password string, _ error) {
 
67
 
 
68
        subscriptionsClient := subscriptions.Client{
 
69
                subscriptions.NewWithBaseURI(resourceManagerEndpoint),
 
70
        }
 
71
        subscriptionsClient.Sender = sender
 
72
        setClientInspectors(&subscriptionsClient.Client, requestInspector, "azure.subscriptions")
 
73
 
 
74
        oauthConfig, tenantId, err := OAuthConfig(
 
75
                subscriptionsClient,
 
76
                resourceManagerEndpoint,
 
77
                subscriptionId,
 
78
        )
 
79
        if err != nil {
 
80
                return "", "", errors.Trace(err)
 
81
        }
 
82
 
 
83
        client := autorest.NewClientWithUserAgent("juju")
 
84
        client.Sender = sender
 
85
        setClientInspectors(&client, requestInspector, "azure.autorest")
 
86
 
 
87
        // Perform the interactive authentication. The user will be prompted to
 
88
        // open a URL and input a device code, after which they will have to
 
89
        // enter their username and password if they are not already
 
90
        // authenticated with Azure.
 
91
        fmt.Fprintln(stderr, "Initiating interactive authentication.")
 
92
        fmt.Fprintln(stderr)
 
93
        armResource := TokenResource(resourceManagerEndpoint)
 
94
        clientId := jujuApplicationId
 
95
        deviceCode, err := azure.InitiateDeviceAuth(&client, *oauthConfig, clientId, armResource)
 
96
        if err != nil {
 
97
                return "", "", errors.Annotate(err, "initiating interactive authentication")
 
98
        }
 
99
        fmt.Fprintln(stderr, to.String(deviceCode.Message)+"\n")
 
100
        token, err := azure.WaitForUserCompletion(&client, deviceCode)
 
101
        if err != nil {
 
102
                return "", "", errors.Annotate(err, "waiting for interactive authentication to completed")
 
103
        }
 
104
 
 
105
        // Create service principal tokens that we can use to authorize API
 
106
        // requests to Active Directory and Resource Manager. These tokens
 
107
        // are only valid for a short amount of time, so we must create a
 
108
        // service principal password that can be used to obtain new tokens.
 
109
        armSpt, err := azure.NewServicePrincipalTokenFromManualToken(*oauthConfig, clientId, armResource, *token)
 
110
        if err != nil {
 
111
                return "", "", errors.Annotate(err, "creating temporary ARM service principal token")
 
112
        }
 
113
        if client.Sender != nil {
 
114
                armSpt.SetSender(client.Sender)
 
115
        }
 
116
        if err := armSpt.Refresh(); err != nil {
 
117
                return "", "", errors.Trace(err)
 
118
        }
 
119
 
 
120
        // The application requires permissions for both ARM and AD, so we
 
121
        // can use the token for both APIs.
 
122
        graphResource := TokenResource(graphEndpoint)
 
123
        graphToken := armSpt.Token
 
124
        graphToken.Resource = graphResource
 
125
        graphSpt, err := azure.NewServicePrincipalTokenFromManualToken(*oauthConfig, clientId, graphResource, graphToken)
 
126
        if err != nil {
 
127
                return "", "", errors.Annotate(err, "creating temporary Graph service principal token")
 
128
        }
 
129
        if client.Sender != nil {
 
130
                graphSpt.SetSender(client.Sender)
 
131
        }
 
132
        if err := graphSpt.Refresh(); err != nil {
 
133
                return "", "", errors.Trace(err)
 
134
        }
 
135
 
 
136
        directoryURL, err := url.Parse(graphEndpoint)
 
137
        if err != nil {
 
138
                return "", "", errors.Annotate(err, "parsing identity endpoint")
 
139
        }
 
140
        directoryURL.Path = path.Join(directoryURL.Path, tenantId)
 
141
        directoryClient := ad.NewManagementClient(directoryURL.String())
 
142
        authorizationClient := authorization.NewWithBaseURI(resourceManagerEndpoint, subscriptionId)
 
143
        directoryClient.Authorizer = graphSpt
 
144
        authorizationClient.Authorizer = armSpt
 
145
        authorizationClient.Sender = client.Sender
 
146
        directoryClient.Sender = client.Sender
 
147
        setClientInspectors(&directoryClient.Client, requestInspector, "azure.directory")
 
148
        setClientInspectors(&authorizationClient.Client, requestInspector, "azure.authorization")
 
149
 
 
150
        userObject, err := ad.UsersClient{directoryClient}.GetCurrentUser()
 
151
        if err != nil {
 
152
                return "", "", errors.Trace(err)
 
153
        }
 
154
        fmt.Fprintf(stderr, "Authenticated as %q.\n", userObject.DisplayName)
 
155
 
 
156
        fmt.Fprintln(stderr, "Creating/updating service principal.")
 
157
        servicePrincipalObjectId, password, err := createOrUpdateServicePrincipal(
 
158
                ad.ServicePrincipalsClient{directoryClient},
 
159
                subscriptionId,
 
160
                clock,
 
161
                newUUID,
 
162
        )
 
163
        if err != nil {
 
164
                return "", "", errors.Trace(err)
 
165
        }
 
166
 
 
167
        fmt.Fprintln(stderr, "Assigning Owner role to service principal.")
 
168
        if err := createRoleAssignment(
 
169
                authorizationClient,
 
170
                subscriptionId,
 
171
                servicePrincipalObjectId,
 
172
                newUUID,
 
173
        ); err != nil {
 
174
                return "", "", errors.Trace(err)
 
175
        }
 
176
        return jujuApplicationId, password, nil
 
177
}
 
178
 
 
179
func setClientInspectors(
 
180
        client *autorest.Client,
 
181
        requestInspector autorest.PrepareDecorator,
 
182
        loggingModule string,
 
183
) {
 
184
        logger := loggo.GetLogger(loggingModule)
 
185
        client.ResponseInspector = tracing.RespondDecorator(logger)
 
186
        client.RequestInspector = tracing.PrepareDecorator(logger)
 
187
        if requestInspector != nil {
 
188
                tracer := client.RequestInspector
 
189
                client.RequestInspector = func(p autorest.Preparer) autorest.Preparer {
 
190
                        p = tracer(p)
 
191
                        p = requestInspector(p)
 
192
                        return p
 
193
                }
 
194
        }
 
195
}
 
196
 
 
197
func createOrUpdateServicePrincipal(
 
198
        client ad.ServicePrincipalsClient,
 
199
        subscriptionId string,
 
200
        clock clock.Clock,
 
201
        newUUID func() (utils.UUID, error),
 
202
) (servicePrincipalObjectId, password string, _ error) {
 
203
        passwordCredential, err := preparePasswordCredential(clock, newUUID)
 
204
        if err != nil {
 
205
                return "", "", errors.Annotate(err, "preparing password credential")
 
206
        }
 
207
 
 
208
        servicePrincipal, err := client.Create(
 
209
                ad.ServicePrincipalCreateParameters{
 
210
                        ApplicationID:       jujuApplicationId,
 
211
                        AccountEnabled:      true,
 
212
                        PasswordCredentials: []ad.PasswordCredential{passwordCredential},
 
213
                },
 
214
                nil, // abort
 
215
        )
 
216
        if err != nil {
 
217
                if !isMultipleObjectsWithSameKeyValueErr(err) {
 
218
                        return "", "", errors.Trace(err)
 
219
                }
 
220
                // The service principal already exists, so we'll fall out
 
221
                // and update the service principal's password credentials.
 
222
        } else {
 
223
                // The service principal was created successfully, with the
 
224
                // requested password credential.
 
225
                return servicePrincipal.ObjectID, passwordCredential.Value, nil
 
226
        }
 
227
 
 
228
        // The service principal already exists, so we need to query
 
229
        // its object ID, and fetch the existing password credentials
 
230
        // to update.
 
231
        servicePrincipal, err = getServicePrincipal(client)
 
232
        if err != nil {
 
233
                return "", "", errors.Trace(err)
 
234
        }
 
235
        if err := addServicePrincipalPasswordCredential(
 
236
                client, servicePrincipal.ObjectID,
 
237
                passwordCredential,
 
238
        ); err != nil {
 
239
                return "", "", errors.Annotate(err, "updating password credentials")
 
240
        }
 
241
        return servicePrincipal.ObjectID, passwordCredential.Value, nil
 
242
}
 
243
 
 
244
func isMultipleObjectsWithSameKeyValueErr(err error) bool {
 
245
        if err, ok := errorutils.ServiceError(err); ok {
 
246
                return err.Code == "Request_MultipleObjectsWithSameKeyValue"
 
247
        }
 
248
        return false
 
249
}
 
250
 
 
251
func preparePasswordCredential(
 
252
        clock clock.Clock,
 
253
        newUUID func() (utils.UUID, error),
 
254
) (ad.PasswordCredential, error) {
 
255
        password, err := newUUID()
 
256
        if err != nil {
 
257
                return ad.PasswordCredential{}, errors.Annotate(err, "generating password")
 
258
        }
 
259
        passwordKeyUUID, err := newUUID()
 
260
        if err != nil {
 
261
                return ad.PasswordCredential{}, errors.Annotate(err, "generating password key ID")
 
262
        }
 
263
        startDate := clock.Now().UTC()
 
264
        endDate := startDate.Add(passwordExpiryDuration)
 
265
        return ad.PasswordCredential{
 
266
                CustomKeyIdentifier: []byte("juju-" + startDate.Format("20060102")),
 
267
                KeyId:               passwordKeyUUID.String(),
 
268
                Value:               password.String(),
 
269
                StartDate:           startDate,
 
270
                EndDate:             endDate,
 
271
        }, nil
 
272
}
 
273
 
 
274
func addServicePrincipalPasswordCredential(
 
275
        client ad.ServicePrincipalsClient,
 
276
        servicePrincipalObjectId string,
 
277
        passwordCredential ad.PasswordCredential,
 
278
) error {
 
279
        existing, err := client.ListPasswordCredentials(servicePrincipalObjectId)
 
280
        if err != nil {
 
281
                return errors.Trace(err)
 
282
        }
 
283
        passwordCredentials := append(existing.Value, passwordCredential)
 
284
        _, err = client.UpdatePasswordCredentials(
 
285
                servicePrincipalObjectId,
 
286
                ad.PasswordCredentialsUpdateParameters{passwordCredentials},
 
287
        )
 
288
        return errors.Trace(err)
 
289
}
 
290
 
 
291
func getServicePrincipal(client ad.ServicePrincipalsClient) (ad.ServicePrincipal, error) {
 
292
        // TODO(axw) filter by Service Principal Name (SPN).
 
293
        // It works without that, but the response is noisy.
 
294
        result, err := client.List("")
 
295
        if err != nil {
 
296
                return ad.ServicePrincipal{}, errors.Annotate(err, "listing service principals")
 
297
        }
 
298
        for _, sp := range result.Value {
 
299
                if sp.ApplicationID == jujuApplicationId {
 
300
                        return sp, nil
 
301
                }
 
302
        }
 
303
        return ad.ServicePrincipal{}, errors.NotFoundf("service principal")
 
304
}
 
305
 
 
306
func createRoleAssignment(
 
307
        authorizationClient authorization.ManagementClient,
 
308
        subscriptionId string,
 
309
        servicePrincipalObjectId string,
 
310
        newUUID func() (utils.UUID, error),
 
311
) error {
 
312
        // Find the role definition with the name "Owner".
 
313
        roleScope := path.Join("subscriptions", subscriptionId)
 
314
        roleDefinitionsClient := authorization.RoleDefinitionsClient{authorizationClient}
 
315
        result, err := roleDefinitionsClient.List(roleScope, "roleName eq 'Owner'")
 
316
        if err != nil {
 
317
                return errors.Annotate(err, "listing role definitions")
 
318
        }
 
319
        if result.Value == nil || len(*result.Value) == 0 {
 
320
                return errors.NotFoundf("Owner role definition")
 
321
        }
 
322
        roleDefinitionId := (*result.Value)[0].ID
 
323
 
 
324
        // The UUID value for the role assignment name is unimportant. Azure
 
325
        // will prevent multiple role assignments for the same role definition
 
326
        // and principal pair.
 
327
        roleAssignmentUUID, err := newUUID()
 
328
        if err != nil {
 
329
                return errors.Annotate(err, "generating role assignment ID")
 
330
        }
 
331
        roleAssignmentsClient := authorization.RoleAssignmentsClient{authorizationClient}
 
332
        roleAssignmentName := roleAssignmentUUID.String()
 
333
        if _, err := roleAssignmentsClient.Create(roleScope, roleAssignmentName, authorization.RoleAssignmentCreateParameters{
 
334
                Properties: &authorization.RoleAssignmentProperties{
 
335
                        RoleDefinitionID: roleDefinitionId,
 
336
                        PrincipalID:      to.StringPtr(servicePrincipalObjectId),
 
337
                },
 
338
        }); err != nil {
 
339
                if err, ok := errorutils.ServiceError(err); ok {
 
340
                        const serviceErrorCodeRoleAssignmentExists = "RoleAssignmentExists"
 
341
                        if err.Code == serviceErrorCodeRoleAssignmentExists {
 
342
                                return nil
 
343
                        }
 
344
                }
 
345
                return errors.Annotate(err, "creating role assignment")
 
346
        }
 
347
        return nil
 
348
}