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