Skip to content

Commit 23fc680

Browse files
mostlikeleegeorgekarrv
authored andcommitted
Obfuscate calendar key (#38687)
1 parent 89a3651 commit 23fc680

File tree

20 files changed

+626
-63
lines changed

20 files changed

+626
-63
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* the google calendar intergration api key json is now obfuscated in GET requests

cmd/fleetctl/fleetctl/gitops_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,7 @@ func TestGitOpsFullGlobal(t *testing.T) {
11161116
assert.Len(t, appliedMacProfiles, 1)
11171117
assert.Len(t, appliedWinProfiles, 1)
11181118
require.Len(t, savedAppConfig.Integrations.GoogleCalendar, 1)
1119-
assert.Equal(t, "service@example.com", savedAppConfig.Integrations.GoogleCalendar[0].ApiKey["client_email"])
1119+
assert.Equal(t, "service@example.com", savedAppConfig.Integrations.GoogleCalendar[0].ApiKey.Values["client_email"])
11201120
assert.True(t, savedAppConfig.ActivityExpirySettings.ActivityExpiryEnabled)
11211121
assert.Equal(t, 60, savedAppConfig.ActivityExpirySettings.ActivityExpiryWindow)
11221122
assert.True(t, savedAppConfig.ServerSettings.AIFeaturesDisabled)

ee/server/calendar/google_calendar.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ func NewGoogleCalendar(config *GoogleCalendarConfig) *GoogleCalendar {
7272
switch {
7373
case config.API != nil:
7474
// Use the provided API.
75-
case config.IntegrationConfig.ApiKey[fleet.GoogleCalendarEmail] == loadEmail:
75+
case config.IntegrationConfig.ApiKey.Values[fleet.GoogleCalendarEmail] == loadEmail:
7676
config.API = &GoogleCalendarLoadAPI{Logger: config.Logger}
77-
case config.IntegrationConfig.ApiKey[fleet.GoogleCalendarEmail] == MockEmail:
77+
case config.IntegrationConfig.ApiKey.Values[fleet.GoogleCalendarEmail] == MockEmail:
7878
config.API = &GoogleCalendarMockAPI{config.Logger}
7979
default:
8080
config.API = &GoogleCalendarLowLevelAPI{logger: config.Logger}
@@ -283,8 +283,8 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) withRetry(fn func() (any, error))
283283
func (c *GoogleCalendar) Configure(userEmail string) error {
284284
adjustedUserEmail := adjustEmail(userEmail)
285285
err := c.config.API.Configure(
286-
c.config.Context, c.config.IntegrationConfig.ApiKey[fleet.GoogleCalendarEmail],
287-
c.config.IntegrationConfig.ApiKey[fleet.GoogleCalendarPrivateKey], adjustedUserEmail,
286+
c.config.Context, c.config.IntegrationConfig.ApiKey.Values[fleet.GoogleCalendarEmail],
287+
c.config.IntegrationConfig.ApiKey.Values[fleet.GoogleCalendarPrivateKey], adjustedUserEmail,
288288
c.config.ServerURL,
289289
)
290290
if err != nil {

ee/server/calendar/google_calendar_integration_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ func (s *googleCalendarIntegrationTestSuite) TestCreateGetDeleteEvent() {
5757
Context: context.Background(),
5858
IntegrationConfig: &fleet.GoogleCalendarIntegration{
5959
Domain: "example.com",
60-
ApiKey: map[string]string{
60+
ApiKey: fleet.GoogleCalendarApiKey{Values: map[string]string{
6161
"client_email": loadEmail,
6262
"private_key": s.server.URL,
63-
},
63+
}},
6464
},
6565
Logger: kitlog.NewLogfmtLogger(kitlog.NewSyncWriter(os.Stdout)),
6666
}
@@ -124,10 +124,10 @@ func (s *googleCalendarIntegrationTestSuite) TestFillUpCalendar() {
124124
Context: context.Background(),
125125
IntegrationConfig: &fleet.GoogleCalendarIntegration{
126126
Domain: "example.com",
127-
ApiKey: map[string]string{
127+
ApiKey: fleet.GoogleCalendarApiKey{Values: map[string]string{
128128
"client_email": loadEmail,
129129
"private_key": s.server.URL,
130-
},
130+
}},
131131
},
132132
Logger: kitlog.NewLogfmtLogger(kitlog.NewSyncWriter(os.Stdout)),
133133
}

ee/server/calendar/google_calendar_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,10 @@ func makeConfig(mockAPI *MockGoogleCalendarLowLevelAPI) *GoogleCalendarConfig {
137137
config := &GoogleCalendarConfig{
138138
Context: context.Background(),
139139
IntegrationConfig: &fleet.GoogleCalendarIntegration{
140-
ApiKey: map[string]string{
140+
ApiKey: fleet.GoogleCalendarApiKey{Values: map[string]string{
141141
fleet.GoogleCalendarEmail: baseServiceEmail,
142142
fleet.GoogleCalendarPrivateKey: basePrivateKey,
143-
},
143+
}},
144144
},
145145
Logger: logger,
146146
API: mockAPI,

frontend/interfaces/integration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export interface IIntegrationFormErrors {
6363

6464
export interface IGlobalCalendarIntegration {
6565
domain: string;
66-
api_key_json: string;
66+
api_key_json: Record<string, string>;
6767
}
6868

6969
interface ITeamCalendarSettings {

frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { NotificationContext } from "context/notification";
77
import { AppContext } from "context/app";
88
import configAPI from "services/entities/config";
99
import paths from "router/paths";
10+
import { UNCHANGED_PASSWORD_API_RESPONSE } from "utilities/constants";
1011

1112
// @ts-ignore
1213
import InputField from "components/forms/fields/InputField";
@@ -46,6 +47,17 @@ const API_KEY_JSON_PLACEHOLDER = `{
4647
"universe_domain": "googleapis.com"
4748
}`;
4849

50+
// Check if the API key JSON object contains obfuscated values
51+
const isObfuscatedApiKey = (apiKeyJson: Record<string, string>): boolean => {
52+
if (!apiKeyJson || Object.keys(apiKeyJson).length === 0) {
53+
return false;
54+
}
55+
// If all values are "********", the API key is obfuscated
56+
return Object.values(apiKeyJson).every(
57+
(value) => value === UNCHANGED_PASSWORD_API_RESPONSE
58+
);
59+
};
60+
4961
interface ICalendarsFormErrors {
5062
domain?: string | null;
5163
apiKeyJson?: string | null;
@@ -87,16 +99,27 @@ const Calendars = (): JSX.Element => {
8799
} = useQuery<IConfig, Error, IConfig>(["config"], () => configAPI.loadAll(), {
88100
select: (data: IConfig) => data,
89101
onSuccess: (data) => {
90-
if (data.integrations.google_calendar) {
91-
setFormData({
92-
domain: data.integrations.google_calendar[0].domain,
93-
// Formats string for better UI readability
94-
apiKeyJson: JSON.stringify(
95-
data.integrations.google_calendar[0].api_key_json,
96-
null,
97-
"\t"
98-
),
99-
});
102+
if (
103+
Array.isArray(data.integrations.google_calendar) &&
104+
data.integrations.google_calendar.length > 0
105+
) {
106+
const apiKeyJsonObj = data.integrations.google_calendar[0].api_key_json;
107+
108+
// Check if the API key is obfuscated
109+
if (isObfuscatedApiKey(apiKeyJsonObj)) {
110+
// Show masked value in UI
111+
setFormData({
112+
domain: data.integrations.google_calendar[0].domain,
113+
apiKeyJson: UNCHANGED_PASSWORD_API_RESPONSE,
114+
});
115+
} else {
116+
// Show the actual API key JSON
117+
setFormData({
118+
domain: data.integrations.google_calendar[0].domain,
119+
// Formats string for better UI readability
120+
apiKeyJson: JSON.stringify(apiKeyJsonObj, null, "\t"),
121+
});
122+
}
100123
}
101124
},
102125
});
@@ -115,7 +138,11 @@ const Calendars = (): JSX.Element => {
115138
if (!curFormData.domain && !!curFormData.apiKeyJson) {
116139
errors.domain = "Domain must be completed";
117140
}
118-
if (curFormData.apiKeyJson) {
141+
// Skip JSON validation if the value is the masked placeholder
142+
if (
143+
curFormData.apiKeyJson &&
144+
curFormData.apiKeyJson !== UNCHANGED_PASSWORD_API_RESPONSE
145+
) {
119146
try {
120147
JSON.parse(curFormData.apiKeyJson);
121148
} catch (e: unknown) {
@@ -150,16 +177,33 @@ const Calendars = (): JSX.Element => {
150177

151178
evt.preventDefault();
152179

180+
// Determine the API key to submit
181+
let apiKeyToSubmit;
182+
if (formData.apiKeyJson === UNCHANGED_PASSWORD_API_RESPONSE) {
183+
// User didn't change the masked value, don't send it (backend will preserve existing)
184+
apiKeyToSubmit = undefined;
185+
} else if (
186+
formData.apiKeyJson &&
187+
formData.apiKeyJson !== UNCHANGED_PASSWORD_API_RESPONSE
188+
) {
189+
// User provided a new API key
190+
apiKeyToSubmit = JSON.parse(formData.apiKeyJson);
191+
} else {
192+
// No API key
193+
apiKeyToSubmit = null;
194+
}
195+
153196
// Format for API
154197
const formDataToSubmit =
155-
formData.apiKeyJson === "" && formData.domain === ""
198+
apiKeyToSubmit === null && formData.domain === ""
156199
? [] // Send empty array if no keys are set
157200
: [
158201
{
159202
domain: formData.domain,
160-
api_key_json:
161-
(formData.apiKeyJson && JSON.parse(formData.apiKeyJson)) ||
162-
null,
203+
// Only include api_key_json if it's not undefined (masked value not changed)
204+
...(apiKeyToSubmit !== undefined && {
205+
api_key_json: apiKeyToSubmit,
206+
}),
163207
},
164208
];
165209

@@ -282,6 +326,11 @@ const Calendars = (): JSX.Element => {
282326
inputClassName={`${baseClass}__api-key-json`}
283327
error={formErrors.apiKeyJson}
284328
disabled={gomEnabled}
329+
helpText={
330+
apiKeyJson === UNCHANGED_PASSWORD_API_RESPONSE
331+
? "API key is configured. Replace with a new key to update."
332+
: undefined
333+
}
285334
/>
286335
<InputField
287336
label="Primary domain"

frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ICertificatesCustomSCEP,
77
} from "interfaces/certificates";
88
import deepDifference from "utilities/deep_difference";
9+
import { UNCHANGED_PASSWORD_API_RESPONSE } from "utilities/constants";
910

1011
import { ICertFormData } from "../AddCertAuthorityModal/AddCertAuthorityModal";
1112
import { getDisplayErrMessage } from "../AddCertAuthorityModal/helpers";
@@ -16,8 +17,6 @@ import { IHydrantFormData } from "../HydrantForm/HydrantForm";
1617
import { ISmallstepFormData } from "../SmallstepForm/SmallstepForm";
1718
import { ICustomESTFormData } from "../CustomESTForm/CustomESTForm";
1819

19-
const UNCHANGED_PASSWORD_API_RESPONSE = "********";
20-
2120
export const generateDefaultFormData = (
2221
certAuthority: ICertificateAuthority
2322
): ICertFormData => {

frontend/utilities/constants.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { IHost } from "interfaces/host";
1010
const { origin } = global.window.location;
1111
export const BASE_URL = `${origin}${URL_PREFIX}/api`;
1212

13+
export const UNCHANGED_PASSWORD_API_RESPONSE = "********";
14+
1315
export enum PolicyResponse {
1416
PASSING = "passing",
1517
FAILING = "failing",

server/cron/calendar_cron_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
234234
GoogleCalendar: []*fleet.GoogleCalendarIntegration{
235235
{
236236
Domain: "example.com",
237-
ApiKey: map[string]string{
237+
ApiKey: fleet.GoogleCalendarApiKey{Values: map[string]string{
238238
fleet.GoogleCalendarEmail: "calendar-mock@example.com",
239-
},
239+
}},
240240
},
241241
},
242242
},
@@ -420,9 +420,9 @@ func TestCalendarEvents1KHosts(t *testing.T) {
420420
GoogleCalendar: []*fleet.GoogleCalendarIntegration{
421421
{
422422
Domain: "example.com",
423-
ApiKey: map[string]string{
423+
ApiKey: fleet.GoogleCalendarApiKey{Values: map[string]string{
424424
fleet.GoogleCalendarEmail: "calendar-mock@example.com",
425-
},
425+
}},
426426
},
427427
},
428428
},
@@ -753,9 +753,9 @@ func TestEventBody(t *testing.T) {
753753
GoogleCalendar: []*fleet.GoogleCalendarIntegration{
754754
{
755755
Domain: "example.com",
756-
ApiKey: map[string]string{
756+
ApiKey: fleet.GoogleCalendarApiKey{Values: map[string]string{
757757
fleet.GoogleCalendarEmail: "calendar-mock@example.com",
758-
},
758+
}},
759759
},
760760
},
761761
},

0 commit comments

Comments
 (0)