11
11
import org .elasticsearch .ElasticsearchException ;
12
12
import org .elasticsearch .action .ActionListener ;
13
13
import org .elasticsearch .common .Strings ;
14
+ import org .elasticsearch .common .cache .Cache ;
15
+ import org .elasticsearch .common .cache .CacheBuilder ;
16
+ import org .elasticsearch .common .hash .MessageDigests ;
14
17
import org .elasticsearch .common .settings .SecureString ;
15
18
import org .elasticsearch .common .settings .Settings ;
19
+ import org .elasticsearch .common .util .concurrent .ReleasableLock ;
16
20
import org .elasticsearch .common .util .concurrent .ThreadContext ;
17
21
import org .elasticsearch .env .Environment ;
18
22
import org .elasticsearch .watcher .ResourceWatcherService ;
25
29
import org .elasticsearch .xpack .core .security .user .User ;
26
30
import org .elasticsearch .xpack .core .ssl .CertUtils ;
27
31
import org .elasticsearch .xpack .core .ssl .SSLConfigurationSettings ;
32
+ import org .elasticsearch .xpack .security .authc .BytesKey ;
33
+ import org .elasticsearch .xpack .security .authc .support .CachingRealm ;
28
34
import org .elasticsearch .xpack .security .authc .support .UserRoleMapper ;
29
35
import org .elasticsearch .xpack .security .authc .support .mapper .CompositeRoleMapper ;
30
36
import org .elasticsearch .xpack .security .authc .support .mapper .NativeRoleMappingStore ;
31
37
32
38
import javax .net .ssl .X509TrustManager ;
33
39
40
+ import java .security .MessageDigest ;
34
41
import java .security .cert .Certificate ;
42
+ import java .security .cert .CertificateEncodingException ;
35
43
import java .security .cert .CertificateException ;
36
44
import java .security .cert .X509Certificate ;
37
45
import java .util .Collections ;
46
+ import java .util .Iterator ;
38
47
import java .util .List ;
39
48
import java .util .Map ;
49
+ import java .util .concurrent .locks .ReadWriteLock ;
50
+ import java .util .concurrent .locks .ReentrantReadWriteLock ;
40
51
import java .util .regex .Matcher ;
41
52
import java .util .regex .Pattern ;
42
53
43
- public class PkiRealm extends Realm {
54
+ public class PkiRealm extends Realm implements CachingRealm {
44
55
45
56
public static final String PKI_CERT_HEADER_NAME = "__SECURITY_CLIENT_CERTIFICATE" ;
46
57
47
58
// For client based cert validation, the auth type must be specified but UNKNOWN is an acceptable value
48
59
private static final String AUTH_TYPE = "UNKNOWN" ;
49
60
61
+ // the lock is used in an odd manner; when iterating over the cache we cannot have modifiers other than deletes using
62
+ // the iterator but when not iterating we can modify the cache without external locking. When making normal modifications to the cache
63
+ // the read lock is obtained so that we can allow concurrent modifications; however when we need to iterate over the keys or values of
64
+ // the cache the write lock must obtained to prevent any modifications
65
+ private final ReleasableLock readLock ;
66
+ private final ReleasableLock writeLock ;
67
+
68
+ {
69
+ final ReadWriteLock iterationLock = new ReentrantReadWriteLock ();
70
+ readLock = new ReleasableLock (iterationLock .readLock ());
71
+ writeLock = new ReleasableLock (iterationLock .writeLock ());
72
+ }
73
+
50
74
private final X509TrustManager trustManager ;
51
75
private final Pattern principalPattern ;
52
76
private final UserRoleMapper roleMapper ;
53
-
77
+ private final Cache < BytesKey , User > cache ;
54
78
55
79
public PkiRealm (RealmConfig config , ResourceWatcherService watcherService , NativeRoleMappingStore nativeRoleMappingStore ) {
56
80
this (config , new CompositeRoleMapper (PkiRealmSettings .TYPE , config , watcherService , nativeRoleMappingStore ));
@@ -62,6 +86,10 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ
62
86
this .trustManager = trustManagers (config );
63
87
this .principalPattern = PkiRealmSettings .USERNAME_PATTERN_SETTING .get (config .settings ());
64
88
this .roleMapper = roleMapper ;
89
+ this .cache = CacheBuilder .<BytesKey , User >builder ()
90
+ .setExpireAfterWrite (PkiRealmSettings .CACHE_TTL_SETTING .get (config .settings ()))
91
+ .setMaximumWeight (PkiRealmSettings .CACHE_MAX_USERS_SETTING .get (config .settings ()))
92
+ .build ();
65
93
}
66
94
67
95
@ Override
@@ -77,18 +105,28 @@ public X509AuthenticationToken token(ThreadContext context) {
77
105
@ Override
78
106
public void authenticate (AuthenticationToken authToken , ActionListener <AuthenticationResult > listener ) {
79
107
X509AuthenticationToken token = (X509AuthenticationToken )authToken ;
80
- if (isCertificateChainTrusted (trustManager , token , logger ) == false ) {
81
- listener .onResponse (AuthenticationResult .unsuccessful ("Certificate for " + token .dn () + " is not trusted" , null ));
82
- } else {
83
- final Map <String , Object > metadata = Collections .singletonMap ("pki_dn" , token .dn ());
84
- final UserRoleMapper .UserData user = new UserRoleMapper .UserData (token .principal (),
85
- token .dn (), Collections .emptySet (), metadata , this .config );
86
- roleMapper .resolveRoles (user , ActionListener .wrap (
87
- roles -> listener .onResponse (AuthenticationResult .success (
88
- new User (token .principal (), roles .toArray (new String [roles .size ()]), null , null , metadata , true )
89
- )),
90
- listener ::onFailure
91
- ));
108
+ try {
109
+ final BytesKey fingerprint = computeFingerprint (token .credentials ()[0 ]);
110
+ User user = cache .get (fingerprint );
111
+ if (user != null ) {
112
+ listener .onResponse (AuthenticationResult .success (user ));
113
+ } else if (isCertificateChainTrusted (trustManager , token , logger ) == false ) {
114
+ listener .onResponse (AuthenticationResult .unsuccessful ("Certificate for " + token .dn () + " is not trusted" , null ));
115
+ } else {
116
+ final Map <String , Object > metadata = Collections .singletonMap ("pki_dn" , token .dn ());
117
+ final UserRoleMapper .UserData userData = new UserRoleMapper .UserData (token .principal (),
118
+ token .dn (), Collections .emptySet (), metadata , this .config );
119
+ roleMapper .resolveRoles (userData , ActionListener .wrap (roles -> {
120
+ final User computedUser =
121
+ new User (token .principal (), roles .toArray (new String [roles .size ()]), null , null , metadata , true );
122
+ try (ReleasableLock ignored = readLock .acquire ()) {
123
+ cache .put (fingerprint , computedUser );
124
+ }
125
+ listener .onResponse (AuthenticationResult .success (computedUser ));
126
+ }, listener ::onFailure ));
127
+ }
128
+ } catch (CertificateEncodingException e ) {
129
+ listener .onResponse (AuthenticationResult .unsuccessful ("Certificate for " + token .dn () + " has encoding issues" , e ));
92
130
}
93
131
}
94
132
@@ -196,4 +234,29 @@ private static X509TrustManager trustManagersFromCAs(Settings settings, Environm
196
234
}
197
235
}
198
236
237
+ @ Override
238
+ public void expire (String username ) {
239
+ try (ReleasableLock ignored = writeLock .acquire ()) {
240
+ Iterator <User > userIterator = cache .values ().iterator ();
241
+ while (userIterator .hasNext ()) {
242
+ if (userIterator .next ().principal ().equals (username )) {
243
+ userIterator .remove ();
244
+ // do not break since there is no guarantee username is unique in this realm
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ @ Override
251
+ public void expireAll () {
252
+ try (ReleasableLock ignored = readLock .acquire ()) {
253
+ cache .invalidateAll ();
254
+ }
255
+ }
256
+
257
+ private static BytesKey computeFingerprint (X509Certificate certificate ) throws CertificateEncodingException {
258
+ MessageDigest digest = MessageDigests .sha256 ();
259
+ digest .update (certificate .getEncoded ());
260
+ return new BytesKey (digest .digest ());
261
+ }
199
262
}
0 commit comments