View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.core.logic;
20  
21  import java.lang.reflect.Method;
22  import java.lang.reflect.Modifier;
23  import java.time.OffsetDateTime;
24  import java.util.ArrayList;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  import org.apache.commons.lang3.ArrayUtils;
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.commons.lang3.tuple.Pair;
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.core.LoggerContext;
35  import org.apache.logging.log4j.core.config.LoggerConfig;
36  import org.apache.syncope.common.lib.SyncopeClientException;
37  import org.apache.syncope.common.lib.audit.AuditEntry;
38  import org.apache.syncope.common.lib.audit.EventCategory;
39  import org.apache.syncope.common.lib.to.AuditConfTO;
40  import org.apache.syncope.common.lib.types.AnyTypeKind;
41  import org.apache.syncope.common.lib.types.AuditElements;
42  import org.apache.syncope.common.lib.types.AuditElements.EventCategoryType;
43  import org.apache.syncope.common.lib.types.AuditLoggerName;
44  import org.apache.syncope.common.lib.types.ClientExceptionType;
45  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
46  import org.apache.syncope.common.lib.types.IdRepoImplementationType;
47  import org.apache.syncope.common.lib.types.MatchingRule;
48  import org.apache.syncope.common.lib.types.ResourceOperation;
49  import org.apache.syncope.common.lib.types.UnmatchingRule;
50  import org.apache.syncope.core.logic.audit.AuditAppender;
51  import org.apache.syncope.core.logic.init.AuditLoader;
52  import org.apache.syncope.core.persistence.api.dao.AuditConfDAO;
53  import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
54  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
55  import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
56  import org.apache.syncope.core.persistence.api.entity.AuditConf;
57  import org.apache.syncope.core.persistence.api.entity.EntityFactory;
58  import org.apache.syncope.core.provisioning.api.AuditManager;
59  import org.apache.syncope.core.provisioning.api.ImplementationLookup;
60  import org.apache.syncope.core.provisioning.api.data.AuditDataBinder;
61  import org.apache.syncope.core.spring.security.AuthContextUtils;
62  import org.springframework.boot.logging.LogLevel;
63  import org.springframework.boot.logging.LoggingSystem;
64  import org.springframework.core.io.Resource;
65  import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
66  import org.springframework.core.io.support.ResourcePatternResolver;
67  import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
68  import org.springframework.core.type.classreading.MetadataReader;
69  import org.springframework.core.type.classreading.MetadataReaderFactory;
70  import org.springframework.security.access.prepost.PreAuthorize;
71  import org.springframework.transaction.annotation.Transactional;
72  import org.springframework.util.ClassUtils;
73  import org.springframework.util.SystemPropertyUtils;
74  
75  public class AuditLogic extends AbstractTransactionalLogic<AuditConfTO> {
76  
77      protected static final List<EventCategory> EVENTS = new ArrayList<>();
78  
79      protected final AuditConfDAO auditConfDAO;
80  
81      protected final ExternalResourceDAO resourceDAO;
82  
83      protected final EntityFactory entityFactory;
84  
85      protected final ImplementationLookup implementationLookup;
86  
87      protected final AuditDataBinder binder;
88  
89      protected final AuditManager auditManager;
90  
91      protected final List<AuditAppender> auditAppenders;
92  
93      protected final LoggingSystem loggingSystem;
94  
95      public AuditLogic(
96              final AuditConfDAO auditConfDAO,
97              final ExternalResourceDAO resourceDAO,
98              final EntityFactory entityFactory,
99              final ImplementationLookup implementationLookup,
100             final AuditDataBinder binder,
101             final AuditManager auditManager,
102             final List<AuditAppender> auditAppenders,
103             final LoggingSystem loggingSystem) {
104 
105         this.auditConfDAO = auditConfDAO;
106         this.resourceDAO = resourceDAO;
107         this.entityFactory = entityFactory;
108         this.implementationLookup = implementationLookup;
109         this.binder = binder;
110         this.auditManager = auditManager;
111         this.auditAppenders = auditAppenders;
112         this.loggingSystem = loggingSystem;
113     }
114 
115     @PreAuthorize("hasRole('" + IdRepoEntitlement.AUDIT_LIST + "')")
116     @Transactional(readOnly = true)
117     public List<AuditConfTO> list() {
118         return auditConfDAO.findAll().stream().map(binder::getAuditTO).collect(Collectors.toList());
119     }
120 
121     @PreAuthorize("hasRole('" + IdRepoEntitlement.AUDIT_READ + "')")
122     @Transactional(readOnly = true)
123     public AuditConfTO read(final String key) {
124         return Optional.ofNullable(auditConfDAO.find(key)).map(binder::getAuditTO).
125                 orElseThrow(() -> new NotFoundException("Audit " + key));
126     }
127 
128     @PreAuthorize("hasRole('" + IdRepoEntitlement.AUDIT_SET + "')")
129     public void set(final AuditConfTO auditTO) {
130         AuditConf audit = Optional.ofNullable(auditConfDAO.find(auditTO.getKey())).
131                 orElseGet(() -> {
132                     AuditConf ac = entityFactory.newEntity(AuditConf.class);
133                     ac.setKey(auditTO.getKey());
134                     return ac;
135                 });
136         audit.setActive(auditTO.isActive());
137         audit = auditConfDAO.save(audit);
138 
139         setLevel(audit.getKey(), audit.isActive() ? LogLevel.DEBUG : LogLevel.OFF);
140     }
141 
142     @PreAuthorize("hasRole('" + IdRepoEntitlement.AUDIT_DELETE + "')")
143     public void delete(final String key) {
144         AuditConf audit = Optional.ofNullable(auditConfDAO.find(key)).
145                 orElseThrow(() -> new NotFoundException("Audit " + key));
146         auditConfDAO.delete(audit);
147 
148         setLevel(audit.getKey(), LogLevel.OFF);
149     }
150 
151     protected void setLevel(final String key, final LogLevel level) {
152         String auditLoggerName = AuditLoggerName.getAuditEventLoggerName(AuthContextUtils.getDomain(), key);
153 
154         LoggerContext logCtx = (LoggerContext) LogManager.getContext(false);
155         LoggerConfig logConf = logCtx.getConfiguration().getLoggerConfig(auditLoggerName);
156 
157         auditAppenders.stream().
158                 filter(appender -> appender.getEvents().stream().
159                 anyMatch(event -> key.equalsIgnoreCase(event.toAuditKey()))).
160                 forEach(auditAppender -> AuditLoader.addAppenderToLoggerContext(logCtx, auditAppender, logConf));
161 
162         loggingSystem.setLogLevel(auditLoggerName, level);
163     }
164 
165     @PreAuthorize("hasRole('" + IdRepoEntitlement.AUDIT_LIST + "') "
166             + "or hasRole('" + IdRepoEntitlement.NOTIFICATION_LIST + "')")
167     public List<EventCategory> events() {
168         synchronized (EVENTS) {
169             if (!EVENTS.isEmpty()) {
170                 return EVENTS;
171             }
172         }
173 
174         Set<EventCategory> events = new HashSet<>();
175 
176         EventCategory authenticationEventCategory = new EventCategory();
177         authenticationEventCategory.setCategory(AuditElements.AUTHENTICATION_CATEGORY);
178         authenticationEventCategory.getEvents().add(AuditElements.LOGIN_EVENT);
179         events.add(authenticationEventCategory);
180 
181         implementationLookup.getClassNames(IdRepoImplementationType.TASKJOB_DELEGATE).forEach(clazz -> {
182             EventCategory eventCategory = new EventCategory(EventCategoryType.TASK);
183             eventCategory.setCategory(StringUtils.substringAfterLast(clazz, '.'));
184             events.add(eventCategory);
185         });
186 
187         events.add(new EventCategory(EventCategoryType.WA));
188         events.add(new EventCategory(EventCategoryType.CUSTOM));
189 
190         try {
191             ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
192             MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
193 
194             String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
195                     + ClassUtils.convertClassNameToResourcePath(
196                             SystemPropertyUtils.resolvePlaceholders(getClass().getPackage().getName()))
197                     + "/**/*.class";
198 
199             Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
200             for (Resource resource : resources) {
201                 if (resource.isReadable()) {
202                     MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
203                     Class<?> clazz = Class.forName(metadataReader.getClassMetadata().getClassName());
204 
205                     if (AbstractLogic.class.isAssignableFrom(clazz)) {
206                         EventCategory eventCategory = new EventCategory();
207                         eventCategory.setCategory(clazz.getSimpleName());
208                         for (Method method : clazz.getDeclaredMethods()) {
209                             if (Modifier.isPublic(method.getModifiers())
210                                     && !eventCategory.getEvents().contains(method.getName())) {
211 
212                                 eventCategory.getEvents().add(method.getName());
213                             }
214                         }
215 
216                         events.add(eventCategory);
217                     }
218                 }
219             }
220 
221             for (AnyTypeKind anyTypeKind : AnyTypeKind.values()) {
222                 resourceDAO.findAll().forEach(resource -> {
223                     EventCategory propEventCategory = new EventCategory(EventCategoryType.PROPAGATION);
224                     EventCategory pullEventCategory = new EventCategory(EventCategoryType.PULL);
225                     EventCategory pushEventCategory = new EventCategory(EventCategoryType.PUSH);
226 
227                     propEventCategory.setCategory(anyTypeKind.name());
228                     propEventCategory.setSubcategory(resource.getKey());
229 
230                     pullEventCategory.setCategory(anyTypeKind.name());
231                     pushEventCategory.setCategory(anyTypeKind.name());
232                     pullEventCategory.setSubcategory(resource.getKey());
233                     pushEventCategory.setSubcategory(resource.getKey());
234 
235                     for (ResourceOperation resourceOperation : ResourceOperation.values()) {
236                         propEventCategory.getEvents().add(resourceOperation.name().toLowerCase());
237                     }
238                     pullEventCategory.getEvents().add(ResourceOperation.DELETE.name().toLowerCase());
239 
240                     for (UnmatchingRule unmatching : UnmatchingRule.values()) {
241                         String event = UnmatchingRule.toEventName(unmatching);
242                         pullEventCategory.getEvents().add(event);
243                         pushEventCategory.getEvents().add(event);
244                     }
245 
246                     for (MatchingRule matching : MatchingRule.values()) {
247                         String event = MatchingRule.toEventName(matching);
248                         pullEventCategory.getEvents().add(event);
249                         pushEventCategory.getEvents().add(event);
250                     }
251 
252                     events.add(propEventCategory);
253                     events.add(pullEventCategory);
254                     events.add(pushEventCategory);
255                 });
256             }
257         } catch (Exception e) {
258             LOG.error("Failure retrieving audit/notification events", e);
259         }
260 
261         EVENTS.addAll(events);
262         return EVENTS;
263     }
264 
265     @PreAuthorize("hasRole('" + IdRepoEntitlement.AUDIT_SEARCH + "')")
266     @Transactional(readOnly = true)
267     public Pair<Integer, List<AuditEntry>> search(
268             final String entityKey,
269             final int page,
270             final int size,
271             final AuditElements.EventCategoryType type,
272             final String category,
273             final String subcategory,
274             final List<String> events,
275             final AuditElements.Result result,
276             final OffsetDateTime before,
277             final OffsetDateTime after,
278             final List<OrderByClause> orderBy) {
279 
280         int count = auditConfDAO.countEntries(entityKey, type, category, subcategory, events, result, before, after);
281         List<AuditEntry> matching = auditConfDAO.searchEntries(
282                 entityKey, page, size, type, category, subcategory, events, result, before, after, orderBy);
283         return Pair.of(count, matching);
284     }
285 
286     @PreAuthorize("isAuthenticated()")
287     public void create(final AuditEntry auditEntry) {
288         boolean authorized =
289                 AuthContextUtils.getAuthorizations().containsKey(IdRepoEntitlement.AUDIT_SET)
290                 || AuthContextUtils.getAuthorizations().containsKey(IdRepoEntitlement.ANONYMOUS)
291                 && AuditElements.EventCategoryType.WA == auditEntry.getLogger().getType();
292         if (authorized) {
293             auditManager.audit(
294                     auditEntry.getWho(),
295                     auditEntry.getLogger().getType(),
296                     auditEntry.getLogger().getCategory(),
297                     auditEntry.getLogger().getSubcategory(),
298                     auditEntry.getLogger().getEvent(),
299                     auditEntry.getLogger().getResult(),
300                     auditEntry.getBefore(),
301                     auditEntry.getOutput(),
302                     auditEntry.getInputs());
303         } else {
304             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.DelegatedAdministration);
305             sce.getElements().add("Not allowed to create Audit entries");
306             throw sce;
307         }
308     }
309 
310     @Override
311     protected AuditConfTO resolveReference(final Method method, final Object... args)
312             throws UnresolvedReferenceException {
313 
314         String key = null;
315 
316         if (ArrayUtils.isNotEmpty(args)) {
317             for (int i = 0; key == null && i < args.length; i++) {
318                 if (args[i] instanceof String) {
319                     key = (String) args[i];
320                 } else if (args[i] instanceof AuditConfTO) {
321                     key = ((AuditConfTO) args[i]).getKey();
322                 }
323             }
324         }
325 
326         if (key != null) {
327             try {
328                 return binder.getAuditTO(auditConfDAO.find(key));
329             } catch (Throwable ignore) {
330                 LOG.debug("Unresolved reference", ignore);
331                 throw new UnresolvedReferenceException(ignore);
332             }
333         }
334 
335         throw new UnresolvedReferenceException();
336     }
337 }