forked from firebase/firebase-admin-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tenant_mgt.go
353 lines (303 loc) · 10.5 KB
/
tenant_mgt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
// Copyright 2019 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"firebase.google.com/go/internal"
"google.golang.org/api/iterator"
)
const (
tenantMgtEndpoint = "https://identitytoolkit.googleapis.com/v2beta1"
)
// Tenant represents a tenant in a multi-tenant application.
//
// Multi-tenancy support requires Google Cloud's Identity Platform (GCIP). To learn more about GCIP,
// including pricing and features, see https://cloud.google.com/identity-platform.
//
// Before multi-tenancy can be used in a Google Cloud Identity Platform project, tenants must be
// enabled in that project via the Cloud Console UI.
//
// A tenant configuration provides information such as the display name, tenant identifier and email
// authentication configuration. For OIDC/SAML provider configuration management, TenantClient
// instances should be used instead of a Tenant to retrieve the list of configured IdPs on a tenant.
// When configuring these providers, note that tenants will inherit whitelisted domains and
// authenticated redirect URIs of their parent project.
//
// All other settings of a tenant will also be inherited. These will need to be managed from the
// Cloud Console UI.
type Tenant struct {
ID string `json:"name"`
DisplayName string `json:"displayName"`
AllowPasswordSignUp bool `json:"allowPasswordSignup"`
EnableEmailLinkSignIn bool `json:"enableEmailLinkSignin"`
}
// TenantClient is used for managing users, configuring SAML/OIDC providers, and generating email
// links for specific tenants.
//
// Before multi-tenancy can be used in a Google Cloud Identity Platform project, tenants must be
// enabled in that project via the Cloud Console UI.
//
// Each tenant contains its own identity providers, settings and users. TenantClient enables
// managing users and SAML/OIDC configurations of specific tenants. It also supports verifying ID
// tokens issued to users who are signed into specific tenants.
//
// TenantClient instances for a specific tenantID can be instantiated by calling
// [TenantManager.AuthForTenant(tenantID)].
type TenantClient struct {
*baseClient
}
// TenantID returns the ID of the tenant to which this TenantClient instance belongs.
func (tc *TenantClient) TenantID() string {
return tc.tenantID
}
// TenantManager is the interface used to manage tenants in a multi-tenant application.
//
// This supports creating, updating, listing, deleting the tenants of a Firebase project. It also
// supports creating new TenantClient instances scoped to specific tenant IDs.
type TenantManager struct {
base *baseClient
endpoint string
projectID string
httpClient *internal.HTTPClient
}
func newTenantManager(client *internal.HTTPClient, conf *internal.AuthConfig, base *baseClient) *TenantManager {
return &TenantManager{
base: base,
endpoint: tenantMgtEndpoint,
projectID: conf.ProjectID,
httpClient: client,
}
}
// AuthForTenant creates a new TenantClient scoped to a given tenantID.
func (tm *TenantManager) AuthForTenant(tenantID string) (*TenantClient, error) {
if tenantID == "" {
return nil, errors.New("tenantID must not be empty")
}
return &TenantClient{
baseClient: tm.base.withTenantID(tenantID),
}, nil
}
// Tenant returns the tenant with the given ID.
func (tm *TenantManager) Tenant(ctx context.Context, tenantID string) (*Tenant, error) {
if tenantID == "" {
return nil, errors.New("tenantID must not be empty")
}
req := &internal.Request{
Method: http.MethodGet,
URL: fmt.Sprintf("/tenants/%s", tenantID),
}
var tenant Tenant
if _, err := tm.makeRequest(ctx, req, &tenant); err != nil {
return nil, err
}
tenant.ID = extractResourceID(tenant.ID)
return &tenant, nil
}
// CreateTenant creates a new tenant with the given options.
func (tm *TenantManager) CreateTenant(ctx context.Context, tenant *TenantToCreate) (*Tenant, error) {
if tenant == nil {
return nil, errors.New("tenant must not be nil")
}
req := &internal.Request{
Method: http.MethodPost,
URL: "/tenants",
Body: internal.NewJSONEntity(tenant.ensureParams()),
}
var result Tenant
if _, err := tm.makeRequest(ctx, req, &result); err != nil {
return nil, err
}
result.ID = extractResourceID(result.ID)
return &result, nil
}
// UpdateTenant updates an existing tenant with the given options.
func (tm *TenantManager) UpdateTenant(ctx context.Context, tenantID string, tenant *TenantToUpdate) (*Tenant, error) {
if tenantID == "" {
return nil, errors.New("tenantID must not be empty")
}
if tenant == nil {
return nil, errors.New("tenant must not be nil")
}
mask, err := tenant.params.UpdateMask()
if err != nil {
return nil, fmt.Errorf("failed to construct update mask: %v", err)
}
if len(mask) == 0 {
return nil, errors.New("no parameters specified in the update request")
}
req := &internal.Request{
Method: http.MethodPatch,
URL: fmt.Sprintf("/tenants/%s", tenantID),
Body: internal.NewJSONEntity(tenant.params),
Opts: []internal.HTTPOption{
internal.WithQueryParam("updateMask", strings.Join(mask, ",")),
},
}
var result Tenant
if _, err := tm.makeRequest(ctx, req, &result); err != nil {
return nil, err
}
result.ID = extractResourceID(result.ID)
return &result, nil
}
// DeleteTenant deletes the tenant with the given ID.
func (tm *TenantManager) DeleteTenant(ctx context.Context, tenantID string) error {
if tenantID == "" {
return errors.New("tenantID must not be empty")
}
req := &internal.Request{
Method: http.MethodDelete,
URL: fmt.Sprintf("/tenants/%s", tenantID),
}
_, err := tm.makeRequest(ctx, req, nil)
return err
}
// Tenants returns an iterator over tenants in the project.
//
// If nextPageToken is empty, the iterator will start at the beginning. Otherwise,
// iterator starts after the token.
func (tm *TenantManager) Tenants(ctx context.Context, nextPageToken string) *TenantIterator {
it := &TenantIterator{
ctx: ctx,
tm: tm,
}
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
it.fetch,
func() int { return len(it.tenants) },
func() interface{} { b := it.tenants; it.tenants = nil; return b })
it.pageInfo.MaxSize = maxConfigs
it.pageInfo.Token = nextPageToken
return it
}
func (tm *TenantManager) makeRequest(ctx context.Context, req *internal.Request, v interface{}) (*internal.Response, error) {
if tm.projectID == "" {
return nil, errors.New("project id not available")
}
req.URL = fmt.Sprintf("%s/projects/%s%s", tm.endpoint, tm.projectID, req.URL)
return tm.httpClient.DoAndUnmarshal(ctx, req, v)
}
const (
tenantDisplayNameKey = "displayName"
allowPasswordSignUpKey = "allowPasswordSignup"
enableEmailLinkSignInKey = "enableEmailLinkSignin"
)
// TenantToCreate represents the options used to create a new tenant.
type TenantToCreate struct {
params nestedMap
}
// DisplayName sets the display name of the new tenant.
func (t *TenantToCreate) DisplayName(name string) *TenantToCreate {
return t.set(tenantDisplayNameKey, name)
}
// AllowPasswordSignUp enables or disables email sign-in provider.
func (t *TenantToCreate) AllowPasswordSignUp(allow bool) *TenantToCreate {
return t.set(allowPasswordSignUpKey, allow)
}
// EnableEmailLinkSignIn enables or disables email link sign-in.
//
// Disabling this makes the password required for email sign-in.
func (t *TenantToCreate) EnableEmailLinkSignIn(enable bool) *TenantToCreate {
return t.set(enableEmailLinkSignInKey, enable)
}
func (t *TenantToCreate) set(key string, value interface{}) *TenantToCreate {
t.ensureParams().Set(key, value)
return t
}
func (t *TenantToCreate) ensureParams() nestedMap {
if t.params == nil {
t.params = make(nestedMap)
}
return t.params
}
// TenantToUpdate represents the options used to update an existing tenant.
type TenantToUpdate struct {
params nestedMap
}
// DisplayName sets the display name of the new tenant.
func (t *TenantToUpdate) DisplayName(name string) *TenantToUpdate {
return t.set(tenantDisplayNameKey, name)
}
// AllowPasswordSignUp enables or disables email sign-in provider.
func (t *TenantToUpdate) AllowPasswordSignUp(allow bool) *TenantToUpdate {
return t.set(allowPasswordSignUpKey, allow)
}
// EnableEmailLinkSignIn enables or disables email link sign-in.
//
// Disabling this makes the password required for email sign-in.
func (t *TenantToUpdate) EnableEmailLinkSignIn(enable bool) *TenantToUpdate {
return t.set(enableEmailLinkSignInKey, enable)
}
func (t *TenantToUpdate) set(key string, value interface{}) *TenantToUpdate {
if t.params == nil {
t.params = make(nestedMap)
}
t.params.Set(key, value)
return t
}
// TenantIterator is an iterator over tenants.
type TenantIterator struct {
tm *TenantManager
ctx context.Context
nextFunc func() error
pageInfo *iterator.PageInfo
tenants []*Tenant
}
// PageInfo supports pagination.
func (it *TenantIterator) PageInfo() *iterator.PageInfo {
return it.pageInfo
}
// Next returns the next Tenant. The error value of [iterator.Done] is
// returned if there are no more results. Once Next returns [iterator.Done], all
// subsequent calls will return [iterator.Done].
func (it *TenantIterator) Next() (*Tenant, error) {
if err := it.nextFunc(); err != nil {
return nil, err
}
tenant := it.tenants[0]
it.tenants = it.tenants[1:]
return tenant, nil
}
func (it *TenantIterator) fetch(pageSize int, pageToken string) (string, error) {
params := map[string]string{
"pageSize": strconv.Itoa(pageSize),
}
if pageToken != "" {
params["pageToken"] = pageToken
}
req := &internal.Request{
Method: http.MethodGet,
URL: "/tenants",
Opts: []internal.HTTPOption{
internal.WithQueryParams(params),
},
}
var result struct {
Tenants []Tenant `json:"tenants"`
NextPageToken string `json:"nextPageToken"`
}
if _, err := it.tm.makeRequest(it.ctx, req, &result); err != nil {
return "", err
}
for i := range result.Tenants {
result.Tenants[i].ID = extractResourceID(result.Tenants[i].ID)
it.tenants = append(it.tenants, &result.Tenants[i])
}
it.pageInfo.Token = result.NextPageToken
return result.NextPageToken, nil
}