Skip to content

Commit d3d8f7d

Browse files
committed
Mark Observations with Security Context Events
Closes gh-11992
1 parent 99a8717 commit d3d8f7d

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.core.context;
18+
19+
import io.micrometer.observation.Observation;
20+
import io.micrometer.observation.ObservationRegistry;
21+
22+
import org.springframework.security.core.Authentication;
23+
24+
/**
25+
* A {@link SecurityContextChangedListener} that adds events to an existing
26+
* {@link Observation}
27+
*
28+
* If no {@link Observation} is present when an event is fired, then the event is
29+
* unrecorded.
30+
*
31+
* @author Josh Cummings
32+
* @since 6.0
33+
*/
34+
public final class ObservationSecurityContextChangedListener implements SecurityContextChangedListener {
35+
36+
private static final String SECURITY_CONTEXT_CREATED = "security.context.created";
37+
38+
private static final String SECURITY_CONTEXT_CHANGED = "security.context.changed";
39+
40+
private static final String SECURITY_CONTEXT_CLEARED = "security.context.cleared";
41+
42+
private final ObservationRegistry registry;
43+
44+
/**
45+
* Create a {@link ObservationSecurityContextChangedListener}
46+
* @param registry the {@link ObservationRegistry} for looking up the surrounding
47+
* {@link Observation}
48+
*/
49+
public ObservationSecurityContextChangedListener(ObservationRegistry registry) {
50+
this.registry = registry;
51+
}
52+
53+
/**
54+
* {@inheritDoc}
55+
*/
56+
@Override
57+
public void securityContextChanged(SecurityContextChangedEvent event) {
58+
Observation observation = this.registry.getCurrentObservation();
59+
if (observation == null) {
60+
return;
61+
}
62+
if (event.isCleared()) {
63+
observation.event(Observation.Event.of("security.context.cleared"));
64+
return;
65+
}
66+
Authentication oldAuthentication = getAuthentication(event.getOldContext());
67+
Authentication newAuthentication = getAuthentication(event.getNewContext());
68+
if (oldAuthentication == null && newAuthentication == null) {
69+
return;
70+
}
71+
if (oldAuthentication == null) {
72+
observation.event(Observation.Event.of(SECURITY_CONTEXT_CREATED, "%s [%s]").format(SECURITY_CONTEXT_CREATED,
73+
newAuthentication.getClass().getSimpleName()));
74+
return;
75+
}
76+
if (newAuthentication == null) {
77+
observation.event(Observation.Event.of(SECURITY_CONTEXT_CLEARED, "%s [%s]").format(SECURITY_CONTEXT_CLEARED,
78+
oldAuthentication.getClass().getSimpleName()));
79+
return;
80+
}
81+
if (oldAuthentication.equals(newAuthentication)) {
82+
return;
83+
}
84+
observation.event(
85+
Observation.Event.of(SECURITY_CONTEXT_CHANGED, "%s [%s] -> [%s]").format(SECURITY_CONTEXT_CHANGED,
86+
oldAuthentication.getClass().getSimpleName(), newAuthentication.getClass().getSimpleName()));
87+
}
88+
89+
private static Authentication getAuthentication(SecurityContext context) {
90+
if (context == null) {
91+
return null;
92+
}
93+
return context.getAuthentication();
94+
}
95+
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.core.context;
18+
19+
import java.util.function.Supplier;
20+
21+
import io.micrometer.observation.Observation;
22+
import io.micrometer.observation.ObservationRegistry;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.mockito.ArgumentCaptor;
26+
27+
import org.springframework.security.authentication.TestingAuthenticationToken;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.mockito.BDDMockito.given;
31+
import static org.mockito.Mockito.mock;
32+
import static org.mockito.Mockito.verify;
33+
import static org.mockito.Mockito.verifyNoInteractions;
34+
35+
/**
36+
* Tests for {@link ObservationSecurityContextChangedListener}
37+
*/
38+
public class ObservationSecurityContextChangedListenerTests {
39+
40+
private SecurityContext one = new SecurityContextImpl(new TestingAuthenticationToken("user", "pass"));
41+
42+
private SecurityContext two = new SecurityContextImpl(new TestingAuthenticationToken("admin", "pass"));
43+
44+
private ObservationRegistry observationRegistry;
45+
46+
private ObservationSecurityContextChangedListener tested;
47+
48+
@BeforeEach
49+
void setup() {
50+
this.observationRegistry = mock(ObservationRegistry.class);
51+
this.tested = new ObservationSecurityContextChangedListener(this.observationRegistry);
52+
}
53+
54+
@Test
55+
void securityContextChangedWhenNoObservationThenNoEvents() {
56+
given(this.observationRegistry.getCurrentObservation()).willReturn(null);
57+
this.tested.securityContextChanged(new SecurityContextChangedEvent(this.one, this.two));
58+
}
59+
60+
@Test
61+
void securityContextChangedWhenClearedEventThenAddsClearEventToObservation() {
62+
Observation observation = mock(Observation.class);
63+
given(this.observationRegistry.getCurrentObservation()).willReturn(observation);
64+
Supplier<SecurityContext> one = mock(Supplier.class);
65+
this.tested
66+
.securityContextChanged(new SecurityContextChangedEvent(one, SecurityContextChangedEvent.NO_CONTEXT));
67+
ArgumentCaptor<Observation.Event> event = ArgumentCaptor.forClass(Observation.Event.class);
68+
verify(observation).event(event.capture());
69+
assertThat(event.getValue().getName()).isEqualTo("security.context.cleared");
70+
verifyNoInteractions(one);
71+
}
72+
73+
@Test
74+
void securityContextChangedWhenNoChangeThenNoEventAddedToObservation() {
75+
Observation observation = mock(Observation.class);
76+
given(this.observationRegistry.getCurrentObservation()).willReturn(observation);
77+
this.tested.securityContextChanged(new SecurityContextChangedEvent(this.one, this.one));
78+
verifyNoInteractions(observation);
79+
}
80+
81+
@Test
82+
void securityContextChangedWhenChangedEventThenAddsChangeEventToObservation() {
83+
Observation observation = mock(Observation.class);
84+
given(this.observationRegistry.getCurrentObservation()).willReturn(observation);
85+
this.tested.securityContextChanged(new SecurityContextChangedEvent(this.one, this.two));
86+
ArgumentCaptor<Observation.Event> event = ArgumentCaptor.forClass(Observation.Event.class);
87+
verify(observation).event(event.capture());
88+
assertThat(event.getValue().getName()).isEqualTo("security.context.changed");
89+
}
90+
91+
@Test
92+
void securityContextChangedWhenCreatedEventThenAddsCreatedEventToObservation() {
93+
Observation observation = mock(Observation.class);
94+
given(this.observationRegistry.getCurrentObservation()).willReturn(observation);
95+
this.tested.securityContextChanged(new SecurityContextChangedEvent(null, this.one));
96+
ArgumentCaptor<Observation.Event> event = ArgumentCaptor.forClass(Observation.Event.class);
97+
verify(observation).event(event.capture());
98+
assertThat(event.getValue().getName()).isEqualTo("security.context.created");
99+
}
100+
101+
}

0 commit comments

Comments
 (0)