001/*
002 * Copyright 2017-2024 Ping Identity Corporation
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017package com.unboundid.scim2.server.utils;
018
019import com.unboundid.scim2.common.Path;
020import com.unboundid.scim2.common.annotations.NotNull;
021import com.unboundid.scim2.common.annotations.Nullable;
022import com.unboundid.scim2.common.exceptions.ScimException;
023import com.unboundid.scim2.common.filters.AndFilter;
024import com.unboundid.scim2.common.filters.ComparisonFilter;
025import com.unboundid.scim2.common.filters.ComplexValueFilter;
026import com.unboundid.scim2.common.filters.ContainsFilter;
027import com.unboundid.scim2.common.filters.EndsWithFilter;
028import com.unboundid.scim2.common.filters.EqualFilter;
029import com.unboundid.scim2.common.filters.Filter;
030import com.unboundid.scim2.common.filters.FilterVisitor;
031import com.unboundid.scim2.common.filters.GreaterThanFilter;
032import com.unboundid.scim2.common.filters.GreaterThanOrEqualFilter;
033import com.unboundid.scim2.common.filters.LessThanFilter;
034import com.unboundid.scim2.common.filters.LessThanOrEqualFilter;
035import com.unboundid.scim2.common.filters.NotEqualFilter;
036import com.unboundid.scim2.common.filters.NotFilter;
037import com.unboundid.scim2.common.filters.OrFilter;
038import com.unboundid.scim2.common.filters.PresentFilter;
039import com.unboundid.scim2.common.filters.StartsWithFilter;
040import com.unboundid.scim2.common.types.AttributeDefinition;
041
042import java.util.ArrayList;
043import java.util.List;
044import java.util.Set;
045
046import static com.unboundid.scim2.server.utils.SchemaChecker.Option.ALLOW_UNDEFINED_ATTRIBUTES;
047import static com.unboundid.scim2.server.utils.SchemaChecker.Option.ALLOW_UNDEFINED_SUB_ATTRIBUTES;
048
049
050
051/**
052 * Filter visitor to check attribute paths against the schema.
053 */
054public final class SchemaCheckFilterVisitor
055    implements FilterVisitor<Filter, Object>
056{
057  @Nullable
058  private final Path parentPath;
059
060  @NotNull
061  private final ResourceTypeDefinition resourceType;
062
063  @NotNull
064  private final SchemaChecker schemaChecker;
065
066  @NotNull
067  private final SchemaChecker.Results results;
068
069
070  private SchemaCheckFilterVisitor(
071      @Nullable final Path parentPath,
072      @NotNull final ResourceTypeDefinition resourceType,
073      @NotNull final SchemaChecker schemaChecker,
074      @NotNull final SchemaChecker.Results results)
075  {
076    this.parentPath = parentPath;
077    this.resourceType = resourceType;
078    this.schemaChecker = schemaChecker;
079    this.results = results;
080  }
081
082
083
084  /**
085   * Check the provided filter against the schema.
086   *
087   * @param filter   The filter to check.
088   * @param resourceTypeDefinition The schema to check the filter against.
089   * @param schemaChecker The object that will enforce schema constraints.
090   * @param enabledOptions  The schema checker enabled options.
091   * @param results  The results of checking the filter.
092   * @throws ScimException If an exception occurs during the operation.
093   */
094  static void checkFilter(
095      @NotNull final Filter filter,
096      @NotNull final ResourceTypeDefinition resourceTypeDefinition,
097      @NotNull final SchemaChecker schemaChecker,
098      @NotNull final Set<SchemaChecker.Option> enabledOptions,
099      @NotNull final SchemaChecker.Results results)
100          throws ScimException
101  {
102    if (enabledOptions.contains(ALLOW_UNDEFINED_ATTRIBUTES) &&
103        enabledOptions.contains(ALLOW_UNDEFINED_SUB_ATTRIBUTES))
104    {
105      // Nothing to check because all undefined attributes are allowed.
106      return;
107    }
108
109    final SchemaCheckFilterVisitor visitor =
110        new SchemaCheckFilterVisitor(
111            null, resourceTypeDefinition, schemaChecker, results);
112    filter.visit(visitor, null);
113  }
114
115
116
117  /**
118   * Check the provided value filter in a patch path against the schema.
119   *
120   * @param parentPath  The parent attribute associated with the value filter.
121   * @param filter   The value filter to check.
122   * @param resourceTypeDefinition The schema to check the filter against.
123   * @param schemaChecker The object that will enforce schema constraints.
124   * @param enabledOptions  The schema checker enabled options.
125   * @param results  The results of checking the filter.
126   * @throws ScimException If an exception occurs during the operation.
127   */
128  static void checkValueFilter(
129      @Nullable final Path parentPath,
130      @NotNull final Filter filter,
131      @NotNull final ResourceTypeDefinition resourceTypeDefinition,
132      @NotNull final SchemaChecker schemaChecker,
133      @NotNull final Set<SchemaChecker.Option> enabledOptions,
134      @NotNull final SchemaChecker.Results results)
135          throws ScimException
136  {
137    if (enabledOptions.contains(ALLOW_UNDEFINED_SUB_ATTRIBUTES))
138    {
139      // Nothing to check because all undefined sub-attributes are allowed.
140      return;
141    }
142
143    final SchemaCheckFilterVisitor visitor =
144        new SchemaCheckFilterVisitor(
145            parentPath, resourceTypeDefinition, schemaChecker, results);
146    filter.visit(visitor, null);
147  }
148
149
150
151  /**
152   * {@inheritDoc}
153   */
154  @NotNull
155  public Filter visit(@NotNull final EqualFilter filter,
156                      @Nullable final Object param)
157      throws ScimException
158  {
159    return visitComparisonFilter(filter, param);
160  }
161
162
163
164  /**
165   * {@inheritDoc}
166   */
167  @NotNull
168  public Filter visit(@NotNull final NotEqualFilter filter,
169                      @Nullable final Object param)
170      throws ScimException
171  {
172    return visitComparisonFilter(filter, param);
173  }
174
175
176
177  /**
178   * {@inheritDoc}
179   */
180  @NotNull
181  public Filter visit(@NotNull final ContainsFilter filter,
182                      @Nullable final Object param)
183      throws ScimException
184  {
185    return visitComparisonFilter(filter, param);
186  }
187
188
189
190  /**
191   * {@inheritDoc}
192   */
193  @NotNull
194  public Filter visit(@NotNull final StartsWithFilter filter,
195                      @Nullable final Object param)
196      throws ScimException
197  {
198    return visitComparisonFilter(filter, param);
199  }
200
201
202
203  /**
204   * {@inheritDoc}
205   */
206  @NotNull
207  public Filter visit(@NotNull final EndsWithFilter filter,
208                      @Nullable final Object param)
209      throws ScimException
210  {
211    return visitComparisonFilter(filter, param);
212  }
213
214
215
216  /**
217   * {@inheritDoc}
218   */
219  @NotNull
220  public Filter visit(@NotNull final PresentFilter filter,
221                      @Nullable final Object param)
222      throws ScimException
223  {
224    checkAttributePath(filter.getAttributePath());
225    return filter;
226  }
227
228
229
230  /**
231   * {@inheritDoc}
232   */
233  @NotNull
234  public Filter visit(@NotNull final GreaterThanFilter filter,
235                      @Nullable final Object param)
236      throws ScimException
237  {
238    return visitComparisonFilter(filter, param);
239  }
240
241
242
243  /**
244   * {@inheritDoc}
245   */
246  @NotNull
247  public Filter visit(@NotNull final GreaterThanOrEqualFilter filter,
248                      @Nullable final Object param)
249      throws ScimException
250  {
251    return visitComparisonFilter(filter, param);
252  }
253
254
255
256  /**
257   * {@inheritDoc}
258   */
259  @NotNull
260  public Filter visit(@NotNull final LessThanFilter filter,
261                      @Nullable final Object param)
262      throws ScimException
263  {
264    return visitComparisonFilter(filter, param);
265  }
266
267
268
269  /**
270   * {@inheritDoc}
271   */
272  @NotNull
273  public Filter visit(@NotNull final LessThanOrEqualFilter filter,
274                      @Nullable final Object param)
275      throws ScimException
276  {
277    return visitComparisonFilter(filter, param);
278  }
279
280
281
282  /**
283   * {@inheritDoc}
284   */
285  @NotNull
286  public Filter visit(@NotNull final AndFilter filter,
287                      @Nullable final Object param)
288      throws ScimException
289  {
290    for (Filter f : filter.getCombinedFilters())
291    {
292      f.visit(this, param);
293    }
294    return filter;
295  }
296
297
298
299  /**
300   * {@inheritDoc}
301   */
302  @NotNull
303  public Filter visit(@NotNull final OrFilter filter,
304                      @Nullable final Object param)
305      throws ScimException
306  {
307    for (Filter f : filter.getCombinedFilters())
308    {
309      f.visit(this, param);
310    }
311    return filter;
312  }
313
314
315
316  /**
317   * {@inheritDoc}
318   */
319  @NotNull
320  public Filter visit(@NotNull final NotFilter filter,
321                      @Nullable final Object param)
322      throws ScimException
323  {
324    filter.getInvertedFilter().visit(this, param);
325    return filter;
326  }
327
328
329
330  /**
331   * {@inheritDoc}
332   */
333  @NotNull
334  public Filter visit(@NotNull final ComplexValueFilter filter,
335                      @Nullable final Object param)
336      throws ScimException
337  {
338    checkAttributePath(filter.getAttributePath());
339    return filter;
340  }
341
342
343
344  @NotNull
345  private Filter visitComparisonFilter(@NotNull final ComparisonFilter filter,
346                                       @Nullable final Object param)
347  {
348    checkAttributePath(filter.getAttributePath());
349    return filter;
350  }
351
352
353
354  private void checkAttributePath(@NotNull final Path path)
355  {
356    if (this.parentPath != null)
357    {
358      final Path fullPath = parentPath.attribute(path);
359      final AttributeDefinition attribute =
360          resourceType.getAttributeDefinition(fullPath);
361
362      // Simple, multi-valued attributes implicitly use "value" as the
363      // name to access sub-attributes. Don't print the sub-attribute undefined
364      // error in this case.
365      if (path.getElement(0).getAttribute().equalsIgnoreCase("value"))
366      {
367        final AttributeDefinition parentAttr =
368                resourceType.getAttributeDefinition(parentPath);
369        if (parentAttr.isMultiValued() &&
370                (parentAttr.getSubAttributes() == null))
371        {
372          return;
373        }
374      }
375
376      if (attribute == null)
377      {
378        // Can't find the definition for the sub-attribute in a value filter.
379        results.addFilterIssue(
380            "Sub-attribute " + path.getElement(0) +
381            " in value filter for path " + parentPath.toString() +
382            " is undefined");
383      }
384    }
385    else
386    {
387      final AttributeDefinition attribute =
388          resourceType.getAttributeDefinition(path);
389      if (attribute == null)
390      {
391        // Can't find the attribute definition for attribute in path.
392        final List<String> messages = new ArrayList<String>();
393        schemaChecker.addMessageForUndefinedAttr(path, "", messages);
394        if (!messages.isEmpty())
395        {
396          for (String m : messages)
397          {
398            results.addFilterIssue(m);
399          }
400        }
401      }
402    }
403  }
404}