View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.assetmanager.auth;
23  
24  import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
25  
26  import org.opencastproject.security.api.Role;
27  import org.opencastproject.security.api.SecurityService;
28  import org.opencastproject.security.api.StaticFileAuthorization;
29  import org.opencastproject.security.api.User;
30  
31  import org.apache.commons.lang3.BooleanUtils;
32  import org.osgi.service.component.ComponentContext;
33  import org.osgi.service.component.annotations.Activate;
34  import org.osgi.service.component.annotations.Component;
35  import org.osgi.service.component.annotations.Reference;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  import java.sql.SQLSyntaxErrorException;
40  import java.util.ArrayList;
41  import java.util.Collections;
42  import java.util.Dictionary;
43  import java.util.Hashtable;
44  import java.util.List;
45  import java.util.Objects;
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  import java.util.stream.Collectors;
49  
50  import javax.persistence.EntityManager;
51  import javax.persistence.EntityManagerFactory;
52  import javax.persistence.PersistenceException;
53  import javax.persistence.Query;
54  
55  /**
56   * A simple static file authorization service which allows access to a configured set of patterns.
57   */
58  @Component(
59      property = {
60          "service.description=AssetManager based StaticFileAuthorization",
61      },
62      immediate = true,
63      service = StaticFileAuthorization.class
64  )
65  public class AssetManagerStaticFileAuthorization implements StaticFileAuthorization {
66  
67    private static final Logger logger = LoggerFactory.getLogger(AssetManagerStaticFileAuthorization.class);
68  
69    protected EntityManagerFactory entityManagerFactory;
70    private SecurityService securityService;
71  
72    private Pattern staticFilePattern = Pattern.compile("^/([^/]+)/(?:api|internal)/([^/]+)/.*$");
73  
74    // Settings for role filter
75    private boolean includeAPIRoles = false;
76    private boolean includeCARoles = false;
77    private boolean includeUIRoles = false;
78  
79    @Reference
80    public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) {
81      this.entityManagerFactory = entityManagerFactory;
82    }
83  
84    @Reference
85    void setSecurityService(SecurityService securityService) {
86      this.securityService = securityService;
87    }
88  
89    @Activate
90    public void activate(ComponentContext cc) {
91      List<Pattern> newPattern = new ArrayList<>();
92      Dictionary<String, Object> properties = cc != null ? cc.getProperties() : new Hashtable<>();
93      staticFilePattern = Pattern.compile(Objects.toString(
94          properties.get("pattern"),
95          "^/([^/]+)/(?:api|internal)/([^/]+)/.*$"));
96      includeAPIRoles = BooleanUtils.toBoolean(Objects.toString(properties.get("evaluate.roles.api"), null));
97      includeCARoles = BooleanUtils.toBoolean(Objects.toString(properties.get("evaluate.roles.ca"), null));
98      includeUIRoles = BooleanUtils.toBoolean(Objects.toString(properties.get("evaluate.roles.ui"), null));
99      logger.info("Started authentication handler for {}", staticFilePattern);
100   }
101 
102   @Override
103   public List<Pattern> getProtectedUrlPattern() {
104     return Collections.singletonList(staticFilePattern);
105   }
106 
107   @Override
108   public boolean verifyUrlAccess(final String path) {
109     // Always allow access for admin
110     final User user = securityService.getUser();
111     if (user.hasRole(GLOBAL_ADMIN_ROLE)) {
112       logger.debug("Allow access for admin `{}`", user);
113       return true;
114     }
115 
116     // Check pattern
117     final Matcher m = staticFilePattern.matcher(path);
118     if (!m.matches()) {
119       logger.debug("Path does not match pattern. Preventing access.");
120       return false;
121     }
122 
123     // Check organization
124     final String organizationId = m.group(1);
125     if (!securityService.getOrganization().getId().equals(organizationId)) {
126       logger.debug("The user's organization does not match. Preventing access.");
127       return false;
128     }
129 
130     if (user.getRoles().size() == 0) {
131       logger.debug("User has no roles allowing access.");
132       return false;
133     }
134 
135     // Check role access
136 
137     // As part of the asset manager modules, this will read the internal asset manager state directly and not talk to
138     // the main implementation module since the file authorization needs to be present on all nodes while the main
139     // asset manager implementation runs on the admin node only.
140 
141     // Getting the data directly is more flexible and a lot faster which is important since this may get a high
142     // number of requests. Note that this code is only reading the state and will not modify any data to ensure
143     // consistency with the main implementation. If writing is necessary in the future, we need to enable a way to
144     // talk to the main implementation module like we do with the remote implementations.
145     final List<String> roles = user.getRoles().parallelStream()
146         .map(Role::getName)
147         .filter(roleFilter)
148         .map((role) -> role + " | read")
149         .collect(Collectors.toList());  // ["ROLE_XY | read", ...]
150 
151     StringBuilder properties = new StringBuilder("property_name = ?");
152     for (int i = 1; i < roles.size(); i++) {
153       properties.append(" or property_name = ?");
154     }
155     String sql = "select count(1) from oc_assets_properties "
156         + "where val_bool = true "
157         + "and namespace = ? "
158         + "and mediapackage_id = ? "
159         + "and (" + properties + ")";
160     EntityManager entityManager = entityManagerFactory.createEntityManager();
161     Query q = entityManager.createNativeQuery(sql);
162     q.setParameter(1, "org.opencastproject.assetmanager.security");
163     q.setParameter(2, m.group(2));
164     for (int i = 0; i < roles.size(); i++) {
165       q.setParameter(i + 3, roles.get(i));
166     }
167     try {
168       return ((Long) q.getSingleResult()) > 0;
169     } catch (PersistenceException e) {
170       Throwable parent = e.getCause();
171       if (parent instanceof RuntimeException) {
172         parent = parent.getCause();
173         if (parent instanceof SQLSyntaxErrorException) {
174           // We may get a SyntaxException if the table does not yet exist
175           // This also means that there are no access rules allowing access
176           logger.info("Denying access to static file {}. {}", path, parent.getMessage());
177           return false;
178         }
179       }
180       throw e;
181     }
182   }
183 
184   /**
185    * Filter for removing user interface roles from access control
186    */
187   private final java.util.function.Predicate<String> roleFilter = (name) -> (
188       includeAPIRoles || !name.startsWith("ROLE_API_"))
189       && (includeCARoles  || !name.startsWith("ROLE_CAPTURE_AGENT_"))
190       && (includeUIRoles  || !name.startsWith("ROLE_UI_"));
191 }