2 * ============LICENSE_START=======================================================
3 * ONAP : ccsdk features
4 * ================================================================================
5 * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property.
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END=========================================================
22 package org.onap.ccsdk.features.sdnr.wt.oauthprovider.http;
24 import com.fasterxml.jackson.databind.ObjectMapper;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.List;
32 import java.util.Optional;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import javax.servlet.ServletException;
36 import javax.servlet.ServletOutputStream;
37 import javax.servlet.http.HttpServlet;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpServletResponse;
40 import org.apache.shiro.codec.Base64;
41 import org.jolokia.osgi.security.Authenticator;
42 import org.onap.ccsdk.features.sdnr.wt.common.http.BaseHTTPClient;
43 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.Config;
44 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.NoDefinitionFoundException;
45 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthProviderConfig;
46 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthToken;
47 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OdlPolicy;
48 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.UserTokenPayload;
49 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService;
50 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService.PublicOAuthProviderConfig;
51 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.OAuthProviderFactory;
52 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.TokenCreator;
53 import org.opendaylight.aaa.api.IDMStoreException;
54 import org.opendaylight.aaa.api.IdMService;
55 import org.opendaylight.aaa.shiro.filters.backport.BearerToken;
56 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.ShiroConfiguration;
57 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.shiro.configuration.Main;
58 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.shiro.configuration.Urls;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
62 public class AuthHttpServlet extends HttpServlet {
64 private static final Logger LOG = LoggerFactory.getLogger(AuthHttpServlet.class.getName());
65 private static final long serialVersionUID = 1L;
66 private static final String BASEURI = "/oauth";
67 private static final String LOGINURI = BASEURI + "/login";
68 //private static final String LOGOUTURI = BASEURI + "/logout";
69 private static final String PROVIDERSURI = BASEURI + "/providers";
70 public static final String REDIRECTURI = BASEURI + "/redirect";
71 private static final String POLICIESURI = BASEURI + "/policies";
72 //private static final String PROVIDERID_REGEX = "^\\" + BASEURI + "\\/providers\\/([^\\/]+)$";
73 private static final String REDIRECTID_REGEX = "^\\" + BASEURI + "\\/redirect\\/([^\\/]+)$";
74 //private static final Pattern PROVIDERID_PATTERN = Pattern.compile(PROVIDERID_REGEX);
75 private static final Pattern REDIRECTID_PATTERN = Pattern.compile(REDIRECTID_REGEX);
77 private static final String DEFAULT_DOMAIN = "sdn";
78 private static final String HEAEDER_AUTHORIZATION = "Authorization";
80 private static final String CLASSNAME_ODLBASICAUTH =
81 "org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter";
82 private static final String CLASSNAME_ODLBEARERANDBASICAUTH =
83 "org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter2";
84 private static final String CLASSNAME_ODLMDSALAUTH =
85 "org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter";
87 private final ObjectMapper mapper;
88 /* state <=> AuthProviderService> */
89 private final Map<String, AuthService> providerStore;
90 private Authenticator odlAuthenticator;
91 private IdMService odlIdentityService;
92 private final TokenCreator tokenCreator;
93 private final Config config;
94 private ShiroConfiguration shiroConfiguration;
96 public AuthHttpServlet() throws IOException {
97 this.tokenCreator = TokenCreator.getInstance();
98 this.config = Config.getInstance();
99 this.mapper = new ObjectMapper();
100 this.providerStore = new HashMap<>();
101 for (OAuthProviderConfig pc : config.getProviders()) {
102 this.providerStore.put(pc.getId(), OAuthProviderFactory.create(pc.getType(), pc,
103 this.config.getRedirectUri(), TokenCreator.getInstance()));
108 public void setOdlAuthenticator(Authenticator odlAuthenticator) {
109 this.odlAuthenticator = odlAuthenticator;
112 public void setOdlIdentityService(IdMService odlIdentityService) {
113 this.odlIdentityService = odlIdentityService;
116 public void setShiroConfiguration(ShiroConfiguration shiroConfiguration) {
117 this.shiroConfiguration = shiroConfiguration;
121 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
122 LOG.debug("GET request for {}", req.getRequestURI());
124 if (PROVIDERSURI.equals(req.getRequestURI())) {
125 this.sendResponse(resp, HttpServletResponse.SC_OK, getConfigs(this.providerStore.values()));
126 } else if (POLICIESURI.equals(req.getRequestURI())) {
127 this.sendResponse(resp, HttpServletResponse.SC_OK, this.getPoliciesForUser(req));
128 } else if (req.getRequestURI().startsWith(REDIRECTURI)) {
129 this.handleRedirect(req, resp);
131 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
137 * find out what urls can be accessed by user and which are forbidden
139 * urlEntries: "anon" -> any access allowed "authcXXX" -> no grouping rule -> any access for user allowed "authcXXX,
140 * roles[abc] -> user needs to have role abc "authcXXX, roles["abc,def"] -> user needs to have roles abc AND def
141 * "authcXXX, anyroles[abc] -> user needs to have role abc "authcXXX, anyroles["abc,def"] -> user needs to have
148 private List<OdlPolicy> getPoliciesForUser(HttpServletRequest req) {
149 List<Urls> urlRules = this.shiroConfiguration.getUrls();
150 UserTokenPayload data = this.getUserInfo(req);
151 List<OdlPolicy> policies = new ArrayList<>();
152 if (urlRules != null) {
153 LOG.debug("try to find rules for user {} with roles {}",
154 data == null ? "null" : data.getPreferredUsername(), data == null ? "null" : data.getRoles());
155 final String regex = "^([^,]+)[,]?[\\ ]?([anyroles]+)?(\\[\"?([a-zA-Z,]+)\"?\\])?";
156 final Pattern pattern = Pattern.compile(regex);
158 for (Urls urlRule : urlRules) {
159 matcher = pattern.matcher(urlRule.getPairValue());
160 if (matcher.find()) {
162 final String authClass = getAuthClass(matcher.group(1));
163 Optional<OdlPolicy> policy = Optional.empty();
164 if (authClass.equals(CLASSNAME_ODLBASICAUTH)
165 || authClass.equals(CLASSNAME_ODLBEARERANDBASICAUTH)) {
166 policy = this.getTokenBasedPolicy(urlRule, matcher, data);
167 } else if (authClass.equals(CLASSNAME_ODLMDSALAUTH)) {
168 policy = this.getMdSalBasedPolicy(urlRule, matcher, data);
170 if (policy.isPresent()) {
171 policies.add(policy.get());
173 LOG.warn("unable to get policy for authClass {} for entry {}", authClass,
174 urlRule.getPairValue());
176 } catch (NoDefinitionFoundException e) {
177 LOG.warn("unknown authClass: ", e);
181 LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
185 LOG.debug("no url rules found");
191 * extract policy rule for user from MD-SAL
198 private Optional<OdlPolicy> getMdSalBasedPolicy(Urls urlRule, Matcher matcher, UserTokenPayload data) {
200 return Optional.empty();
204 * extract policy rule for user from url rules of config
210 private Optional<OdlPolicy> getTokenBasedPolicy(Urls urlRule, Matcher matcher, UserTokenPayload data) {
211 final String url = urlRule.getPairKey();
212 if (!urlRule.getPairValue().contains(",")) {
213 LOG.debug("found rule without roles for '{}'", matcher.group(1));
214 //not important if anon or authcXXX
215 if (data != null || "anon".equals(matcher.group(1))) {
216 return Optional.of(OdlPolicy.allowAll(url));
218 } else if (data != null) {
219 LOG.debug("found rule with roles '{}'", matcher.group(4));
220 if ("roles".equals(matcher.group(2))) {
221 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), false)) {
222 Optional.of(OdlPolicy.allowAll(url));
224 Optional.of(OdlPolicy.denyAll(url));
226 } else if ("anyroles".equals(matcher.group(2))) {
227 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), true)) {
228 Optional.of(OdlPolicy.allowAll(url));
230 Optional.of(OdlPolicy.denyAll(url));
233 LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
236 return Optional.of(OdlPolicy.denyAll(url));
238 return Optional.empty();
241 private String getAuthClass(String key) throws NoDefinitionFoundException {
242 if ("anon".equals(key)) {
245 Optional<Main> main =
246 this.shiroConfiguration.getMain().stream().filter((e) -> e.getPairKey().equals(key)).findFirst();
247 if (main.isPresent()) {
248 return main.get().getPairValue();
250 throw new NoDefinitionFoundException("unable to find def for " + key);
253 private UserTokenPayload getUserInfo(HttpServletRequest req) {
255 UserTokenPayload data = TokenCreator.getInstance().decode(req);
259 } else if (isBasic(req)) {
260 String username = getBasicAuthUsername(req);
261 if (username != null) {
262 final String domain = getBasicAuthDomain(username);
263 if (!username.contains("@")) {
264 username = String.format("%s@%s", username, domain);
266 List<String> roles = this.odlIdentityService.listRoles(username, domain);
267 return UserTokenPayload.create(username, roles);
273 private static String getBasicAuthDomain(String username) {
274 if (username.contains("@")) {
275 return username.split("@")[1];
277 return DEFAULT_DOMAIN;
280 private static String getBasicAuthUsername(HttpServletRequest req) {
281 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
282 final String decoded = Base64.decodeToString(header.substring(6));
283 // attempt to decode username/password; otherwise decode as token
284 if (decoded.contains(":")) {
285 return decoded.split(":")[0];
287 LOG.warn("unable to detect username from basicauth header {}", header);
291 private static boolean isBasic(HttpServletRequest req) {
292 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
293 return header == null ? false : header.startsWith("Basic");
296 private static boolean isBearer(HttpServletRequest req) {
297 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
298 return header == null ? false : header.startsWith("Bearer");
301 private boolean rolesMatch(List<String> userRoles, List<String> policyRoles, boolean any) {
303 for (String policyRole : policyRoles) {
304 if (userRoles.contains(policyRole)) {
310 for (String policyRole : policyRoles) {
311 if (!userRoles.contains(policyRole)) {
320 private void fillHost(HttpServletRequest req) {
321 String hostUrl = this.config.getHost();
322 if (hostUrl == null) {
323 final String tmp = req.getRequestURL().toString();
324 final String regex = "^(http[s]{0,1}:\\/\\/[^\\/]+)";
325 final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
326 final Matcher matcher = pattern.matcher(tmp);
327 if (matcher.find()) {
328 hostUrl = matcher.group(1);
329 this.config.setHost(hostUrl);
335 private List<PublicOAuthProviderConfig> getConfigs(Collection<AuthService> values) {
336 List<PublicOAuthProviderConfig> configs = new ArrayList<>();
337 for (AuthService svc : values) {
338 configs.add(svc.getConfig(this.config.getHost()));
344 * GET /oauth/redirect/{providerID}
348 * @throws IOException
350 private void handleRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
351 final String uri = req.getRequestURI();
352 final Matcher matcher = REDIRECTID_PATTERN.matcher(uri);
353 if (matcher.find()) {
354 AuthService provider = this.providerStore.getOrDefault(matcher.group(1), null);
355 if (provider != null) {
356 provider.setLocalHostUrl(this.config.getHost());
357 provider.handleRedirect(req, resp);
361 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
365 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
367 LOG.debug("POST request for {}", req.getRequestURI());
368 if (this.config.doSupportOdlUsers() && LOGINURI.equals(req.getRequestURI())) {
369 final String username = req.getParameter("username");
370 final String domain = req.getParameter("domain");
372 this.doLogin(username, req.getParameter("password"), domain != null ? domain : DEFAULT_DOMAIN);
374 sendResponse(resp, HttpServletResponse.SC_OK, new OAuthToken(token));
375 LOG.debug("login for odluser {} succeeded", username);
378 LOG.debug("login failed");
382 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
386 protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
387 // final String uri = req.getRequestURI();
388 // final Matcher matcher = PROVIDERID_PATTERN.matcher(uri);
389 // if (matcher.find()) {
390 // final String id = matcher.group(1);
391 // final OAuthProviderConfig config = this.mapper.readValue(req.getInputStream(), OAuthProviderConfig.class);
392 // //this.providerStore.put(id, config);
393 // sendResponse(resp, HttpServletResponse.SC_OK, "");
396 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
400 protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
401 // final String uri = req.getRequestURI();
402 // final Matcher matcher = PROVIDERID_PATTERN.matcher(uri);
403 // if (matcher.find()) {
404 // final String id = matcher.group(1);
405 // this.providerStore.remove(id);
406 // sendResponse(resp, HttpServletResponse.SC_OK, "");
409 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
412 private BearerToken doLogin(String username, String password, String domain) {
413 if (!username.contains("@")) {
414 username = String.format("%s@%s", username, domain);
416 HttpServletRequest req = new HeadersOnlyHttpServletRequest(
417 Map.of("Authorization", BaseHTTPClient.getAuthorizationHeaderValue(username, password)));
418 if (this.odlAuthenticator.authenticate(req)) {
420 LOG.info("userids={}", this.odlIdentityService.listUserIDs());
421 LOG.info("domains={}", this.odlIdentityService.listDomains(username));
422 } catch (IDMStoreException e) {
425 List<String> roles = this.odlIdentityService.listRoles(username, domain);
426 UserTokenPayload data = new UserTokenPayload();
427 data.setPreferredUsername(username);
428 data.setFamilyName("");
429 data.setGivenName(username);
430 data.setExp(this.tokenCreator.getDefaultExp());
431 data.setRoles(roles);
432 return this.tokenCreator.createNewJWT(data);
440 private void sendResponse(HttpServletResponse resp, int code, Object data) throws IOException {
441 byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
443 resp.setStatus(code);
444 resp.setContentLength(output.length);
445 resp.setContentType("application/json");
446 ServletOutputStream os = null;
447 os = resp.getOutputStream();