-
Notifications
You must be signed in to change notification settings - Fork 5
/
remote_config.go
287 lines (242 loc) · 9.34 KB
/
remote_config.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
package config
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/golang-jwt/jwt/v4"
"github.com/harness/ff-proxy/domain"
"github.com/harness/ff-proxy/log"
"github.com/harness/ff-proxy/services"
)
type adminService interface {
PageTargets(ctx context.Context, input services.PageTargetsInput) (services.PageTargetsResult, error)
PageAPIKeys(ctx context.Context, input services.PageAPIKeysInput) (services.PageAPIKeysResult, error)
}
type clientService interface {
Authenticate(ctx context.Context, apiKey string, target domain.Target) (string, error)
}
// RemoteOption is type for passing optional parameters to a RemoteConfig
type RemoteOption func(r *RemoteConfig)
// WithLogger can be used to pass a logger to the RemoteConfig, its default logger
// is one that logs to stderr and has debug logging disabled
func WithLogger(l log.Logger) RemoteOption {
return func(r *RemoteConfig) {
r.log = l
}
}
// WithFetchTargets specifies if the RemoteConfig instance should fetch targets or not
func WithFetchTargets(fetchTargets bool) RemoteOption {
return func(r *RemoteConfig) {
r.fetchTargets = fetchTargets
}
}
// RemoteConfig is a type that retrieves config from the Feature Flags Service
type RemoteConfig struct {
adminService adminService
clientService clientService
log log.Logger
accountIdentifier string
orgIdentifier string
projEnvInfo map[string]EnvironmentDetails
fetchTargets bool
}
// TargetConfig returns the Target information that was retrieved from the Feature Flags Service
func (r RemoteConfig) TargetConfig() map[domain.TargetKey]interface{} {
targetConfig := make(map[domain.TargetKey]interface{})
for _, env := range r.projEnvInfo {
targetKey := domain.NewTargetsKey(env.EnvironmentID)
targetConfig[targetKey] = env.Targets
for _, t := range env.Targets {
k := domain.NewTargetKey(env.EnvironmentID, t.Identifier)
targetConfig[k] = t
}
}
return targetConfig
}
// AuthConfig returns the AuthConfig that was retrieved from the Feature Flags Service
func (r RemoteConfig) AuthConfig() map[domain.AuthAPIKey]string {
authConfig := make(map[domain.AuthAPIKey]string)
for _, env := range r.projEnvInfo {
for _, hashedKey := range env.HashedAPIKeys {
authConfig[domain.NewAuthAPIKey(hashedKey)] = env.EnvironmentID
}
}
return authConfig
}
// Tokens returns the map of environment ids to auth tokens that was retrieved from the Feature Flags Service
func (r RemoteConfig) Tokens() map[string]string {
tokens := make(map[string]string)
for _, env := range r.projEnvInfo {
tokens[env.EnvironmentID] = env.Token
}
return tokens
}
// EnvInfo returns the EnvironmentDetails that was retrieved from the Feature Flags Service
func (r RemoteConfig) EnvInfo() map[string]EnvironmentDetails {
return r.projEnvInfo
}
// NewRemoteConfig creates a RemoteConfig and retrieves the configuration for
// the given Account, Org and APIKeys from the Feature Flags Service
func NewRemoteConfig(ctx context.Context, accountIdentifier string, orgIdentifier string, apiKeys []string, adminService adminService, clientService clientService, opts ...RemoteOption) (RemoteConfig, error) {
rc := &RemoteConfig{
adminService: adminService,
clientService: clientService,
accountIdentifier: accountIdentifier,
orgIdentifier: orgIdentifier,
fetchTargets: true,
}
for _, opt := range opts {
opt(rc)
}
if rc.log == nil {
rc.log = log.NoOpLogger{}
}
rc.log = rc.log.With("component", "RemoteConfig", "account_identifier", accountIdentifier, "org_identifier", orgIdentifier)
envInfos := map[string]EnvironmentDetails{}
for _, key := range apiKeys {
envConfig, err := rc.getConfigForKey(ctx, key)
if err != nil {
rc.log.Error("couldn't fetch info for key, skipping", "api key", key, "err", err)
continue
}
// warn if data has already been set for this environment - this means a user has added 2 keys for the same env
if _, ok := envInfos[envConfig.EnvironmentID]; ok {
rc.log.Warn("environment already configured, have you added multiple keys for the same environment?", "environmentID", envConfig.EnvironmentID, "environment identifier", envConfig.EnvironmentIdentifier, "projectID", envConfig.ProjectIdentifier)
}
envInfos[envConfig.EnvironmentID] = envConfig
}
rc.projEnvInfo = envInfos
return *rc, nil
}
// EnvironmentDetails contains details about a configured environment
type EnvironmentDetails struct {
EnvironmentIdentifier string
EnvironmentID string
ProjectIdentifier string
HashedAPIKeys []string
APIKey string
Token string
Targets []domain.Target
}
func (r RemoteConfig) getConfigForKey(ctx context.Context, apiKey string) (EnvironmentDetails, error) {
// authenticate key and get env/project identifiers
projectIdentifier, environmentIdentifier, environmentID, token, err := getEnvironmentInfo(ctx, apiKey, r.clientService)
if err != nil {
return EnvironmentDetails{}, fmt.Errorf("failed to fetch environment details for key %s: %s", apiKey, err)
}
envInfo := EnvironmentDetails{
EnvironmentIdentifier: environmentIdentifier,
EnvironmentID: environmentID,
ProjectIdentifier: projectIdentifier,
APIKey: apiKey,
Token: token,
HashedAPIKeys: nil,
Targets: nil,
}
// get hashed api keys for environment
apiKeys, err := getAPIKeys(ctx, r.accountIdentifier, r.orgIdentifier, projectIdentifier, environmentIdentifier, r.adminService)
if err != nil {
return EnvironmentDetails{}, err
}
envInfo.HashedAPIKeys = apiKeys
// get targets for environment
var targets []domain.Target
if r.fetchTargets {
targets, err = GetTargets(ctx, r.accountIdentifier, r.orgIdentifier, projectIdentifier, environmentIdentifier, r.adminService)
if err != nil {
return EnvironmentDetails{}, err
}
}
envInfo.Targets = targets
return envInfo, nil
}
// GetTargets retrieves all targets for a given environment
func GetTargets(ctx context.Context, accountIdentifier, orgIdentifier, projectIdentifier, environmentIdentifier string, adminService adminService) ([]domain.Target, error) {
targetInput := services.PageTargetsInput{
AccountIdentifier: accountIdentifier,
OrgIdentifier: orgIdentifier,
ProjectIdentifier: projectIdentifier,
EnvironmentIdentifier: environmentIdentifier,
PageNumber: 0,
PageSize: 100,
}
done := false
targets := []domain.Target{}
for !done {
result, err := adminService.PageTargets(ctx, targetInput)
done = result.Finished
if err != nil {
return []domain.Target{}, fmt.Errorf("failed to get targets: %s", err)
}
for _, t := range result.Targets {
targets = append(targets, domain.Target{Target: t})
}
targetInput.PageNumber++
}
return targets, nil
}
// getAPIKeys retrieves the hashed api keys for an environment
func getAPIKeys(ctx context.Context, accountIdentifier, orgIdentifier, projectIdentifier, environmentIdentifier string, adminService adminService) ([]string, error) {
apiKeysInput := services.PageAPIKeysInput{
AccountIdentifier: accountIdentifier,
OrgIdentifier: orgIdentifier,
ProjectIdentifier: projectIdentifier,
EnvironmentIdentifier: environmentIdentifier,
PageNumber: 0,
PageSize: 100,
}
done := false
apiKeys := []string{}
for !done {
result, err := adminService.PageAPIKeys(ctx, apiKeysInput)
done = result.Finished
if err != nil {
return []string{}, fmt.Errorf("failed to get api keys: %s", err)
}
for _, key := range result.APIKeys {
if key.Key != nil {
apiKeys = append(apiKeys, *key.Key)
}
}
apiKeysInput.PageNumber++
}
return apiKeys, nil
}
// getEnvironmentInfo authenticates an api key and retrieves the project identifier, environment identifier and environment ID from it
func getEnvironmentInfo(ctx context.Context, apiKey string, clientService clientService) (projectIdentifier, environmentIdentifier, environmentID, token string, err error) {
// get bearer token
result, err := clientService.Authenticate(ctx, apiKey, domain.Target{})
if err != nil {
return "", "", "", "", fmt.Errorf("error sending client authentication request: %s", err)
}
// get payload data
payloadIndex := 1
if len(strings.Split(result, ".")) < 2 {
return "", "", "", "", fmt.Errorf("invalid jwt received %s", result)
}
payload := strings.Split(result, ".")[payloadIndex]
payloadData, err := jwt.DecodeSegment(payload)
if err != nil {
return "", "", "", "", fmt.Errorf("failed to parse token claims for key %s: %s", apiKey, err)
}
// extract projectIdentifier, environmentIdentifier, environmentID from token claims
var claims map[string]interface{}
if err = json.Unmarshal(payloadData, &claims); err != nil {
return "", "", "", "", fmt.Errorf("failed to unmarshal token claims for key %s: %s", apiKey, err)
}
var ok bool
environmentIdentifier, ok = claims["environmentIdentifier"].(string)
if !ok {
return "", "", "", "", fmt.Errorf("environment identifier not present in bearer token")
}
environmentID, ok = claims["environment"].(string)
if !ok {
return "", "", "", "", fmt.Errorf("environment id not present in bearer token")
}
projectIdentifier, ok = claims["projectIdentifier"].(string)
if !ok {
return "", "", "", "", fmt.Errorf("project identifier not present in bearer token")
}
return projectIdentifier, environmentIdentifier, environmentID, result, nil
}