-
Notifications
You must be signed in to change notification settings - Fork 0
/
google.go
162 lines (136 loc) · 4.48 KB
/
google.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
package google
import (
"context"
"errors"
"fmt"
"io"
"os"
"sync"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/korylprince/dep-webview-oidc/auth"
"github.com/korylprince/dep-webview-oidc/enrollprofile"
"github.com/korylprince/dep-webview-oidc/header"
"github.com/korylprince/dep-webview-oidc/log"
"golang.org/x/exp/slog"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/sync/errgroup"
admin "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/option"
)
var (
ErrInvalidEmail = errors.New("invalid email")
ErrUnauthorizedEmail = errors.New("unauthorized email")
)
// Authorizer verifies a user is in a Google Group or Groups and sets those groups in the returned Context
type Authorizer struct {
svc *admin.MembersService
allowed []string
logger *slog.Logger
pool chan struct{}
}
type Option func(a *Authorizer)
// WithLogger configures the authorizer with the given logger
// If left unconfigured, logging will be disabled
func WithLogger(logger *slog.Logger) Option {
return func(a *Authorizer) {
a.logger = logger
}
}
// WithWorkerLimit configures the authorizer to limit the number of concurrent API requests across all AuthorizeSession calls.
// If left unconfigured, there is no limit enforced
func WithWorkerLimit(limit int) Option {
return func(a *Authorizer) {
a.pool = make(chan struct{}, limit)
}
}
// New returns a new Authorizer with the given service account json path, user to impersonate, and list of groups (by group email)
func New(jsonPath, impersonateUser string, allowedGroups []string, opts ...Option) (*Authorizer, error) {
buf, err := os.ReadFile(jsonPath)
if err != nil {
return nil, fmt.Errorf("could not read json auth file: %w", err)
}
config, err := google.JWTConfigFromJSON(buf, admin.AdminDirectoryGroupMemberReadonlyScope)
if err != nil {
return nil, fmt.Errorf("could not create config: %w", err)
}
if impersonateUser != "" {
config.Subject = impersonateUser
}
adminSvc, err := admin.NewService(context.Background(), option.WithHTTPClient(config.Client(context.Background())))
if err != nil {
return nil, fmt.Errorf("could not create admin service: %w", err)
}
a := &Authorizer{svc: admin.NewMembersService(adminSvc), allowed: allowedGroups}
for _, opt := range opts {
opt(a)
}
if a.logger == nil {
a.logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError + 1}))
}
a.logger.Info("started", "impersonate_user", impersonateUser)
return a, nil
}
// AuthorizeSession authorizes the user/device session and returns an EnrollContext that can be passed to an EnrollmentGenerator.
// If the request is not authorized, an error of type AuthorizationError is returned.
func (a *Authorizer) AuthorizeSession(ctx context.Context, _ *header.MachineInfo, _ *oauth2.Token, idToken *oidc.IDToken) (enrollprofile.Context, error) {
type claims struct {
Email string `json:"email"`
}
// get email from id_token
email := new(claims)
if err := idToken.Claims(email); err != nil {
return nil, fmt.Errorf("could not parse email from id_token: %w", err)
}
if email.Email == "" {
return nil, ErrInvalidEmail
}
// concurrently check each group for membership
errgrp := new(errgroup.Group)
var mu sync.Mutex
results := make(map[string]bool)
for _, g := range a.allowed {
grp := g
errgrp.Go(func() error {
if a.pool != nil {
a.pool <- struct{}{}
defer func() { <-a.pool }()
}
check, err := a.svc.HasMember(grp, email.Email).Do()
if err != nil {
return fmt.Errorf("could not check membership (%s): %w", grp, err)
}
mu.Lock()
results[grp] = check.IsMember
mu.Unlock()
return nil
})
}
// check authorization
err := errgrp.Wait()
authed := false
var grps []string
for grp, ok := range results {
if ok {
authed = true
grps = append(grps, grp)
}
}
log.Attrs(ctx, slog.String("email", email.Email))
if !authed {
if err != nil {
// if all queries failed, don't return AuthorizationError
if len(results) == 0 {
return nil, fmt.Errorf("could not query group(s): %w", err)
}
return nil, &auth.AuthorizationError{Err: fmt.Errorf("could not query group(s): %w", err)}
}
return nil, &auth.AuthorizationError{Err: ErrUnauthorizedEmail}
}
log.LevelAttrs(ctx, slog.LevelDebug, slog.Any("groups", grps))
if err != nil {
id := log.RequestID(ctx)
a.logger.Warn("error during successful authorization", "req-id", id, "error", err.Error())
}
return enrollprofile.Context{"email": email.Email, "groups": grps}, nil
}