/* * Copyright © 2015-2016 Cask Data, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package co.cask.cdap.client; import co.cask.cdap.api.annotation.Beta; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.client.config.ClientConfig; import co.cask.cdap.client.util.RESTClient; import co.cask.cdap.common.FeatureDisabledException; import co.cask.cdap.common.NotFoundException; import co.cask.cdap.common.UnauthenticatedException; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.proto.codec.EntityIdTypeAdapter; import co.cask.cdap.proto.id.EntityId; import co.cask.cdap.proto.security.Action; import co.cask.cdap.proto.security.GrantRequest; import co.cask.cdap.proto.security.Principal; import co.cask.cdap.proto.security.Privilege; import co.cask.cdap.proto.security.RevokeRequest; import co.cask.cdap.proto.security.Role; import co.cask.cdap.security.spi.authorization.AbstractAuthorizer; import co.cask.cdap.security.spi.authorization.RoleAlreadyExistsException; import co.cask.cdap.security.spi.authorization.RoleNotFoundException; import co.cask.cdap.security.spi.authorization.UnauthorizedException; import co.cask.common.http.HttpRequest; import co.cask.common.http.HttpResponse; import co.cask.common.http.ObjectResponse; import com.google.common.io.ByteStreams; import com.google.common.io.InputSupplier; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.Set; import javax.annotation.Nullable; import javax.inject.Inject; /** * Provides ways to interact with the CDAP authorization system. */ @Beta public class AuthorizationClient extends AbstractAuthorizer { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(EntityId.class, new EntityIdTypeAdapter()) .create(); public static final String AUTHORIZATION_BASE = "security/authorization/"; private static final TypeToken<Set<Privilege>> TYPE_OF_PRIVILEGE_SET = new TypeToken<Set<Privilege>>() { }; private static final TypeToken<Set<Role>> TYPE_OF_ROLE_SET = new TypeToken<Set<Role>>() { }; private final RESTClient restClient; private final ClientConfig config; @Inject public AuthorizationClient(ClientConfig config, RESTClient restClient) { this.config = config; this.restClient = restClient; } public AuthorizationClient(ClientConfig config) { this(config, new RESTClient(config)); } @Override public void enforce(EntityId entity, Principal principal, Action action) throws Exception { throw new UnsupportedOperationException("Enforcement is not supported via Java Client. Please instead use the " + "listPrivileges method to view the privileges for a principal."); } @Override public void grant(EntityId entity, Principal principal, Set<Action> actions) throws IOException, UnauthenticatedException, FeatureDisabledException, UnauthorizedException, NotFoundException { GrantRequest grantRequest = new GrantRequest(entity, principal, actions); URL url = config.resolveURLV3(AUTHORIZATION_BASE + "/privileges/grant"); HttpRequest request = HttpRequest.post(url).withBody(GSON.toJson(grantRequest)).build(); executePrivilegeRequest(entity, request); } @Override public void revoke(EntityId entity) throws IOException, UnauthenticatedException, FeatureDisabledException, UnauthorizedException, NotFoundException { revoke(entity, null, null); } @Override public void revoke(EntityId entity, @Nullable Principal principal, @Nullable Set<Action> actions) throws IOException, UnauthenticatedException, FeatureDisabledException, UnauthorizedException, NotFoundException { revoke(new RevokeRequest(entity, principal, actions)); } @Override public Set<Privilege> listPrivileges(Principal principal) throws IOException, FeatureDisabledException, UnauthenticatedException, UnauthorizedException, NotFoundException { URL url = config.resolveURLV3(String.format(AUTHORIZATION_BASE + "%s/%s/privileges", principal.getType(), principal.getName())); HttpRequest request = HttpRequest.get(url).build(); HttpResponse response = doExecuteRequest(request); if (response.getResponseCode() == HttpURLConnection.HTTP_OK) { return ObjectResponse.fromJsonBody(response, TYPE_OF_PRIVILEGE_SET, GSON).getResponseObject(); } throw new IOException(String.format("Cannot list privileges. Reason: %s", response.getResponseBodyAsString())); } @Override public void createRole(Role role) throws IOException, FeatureDisabledException, UnauthenticatedException, UnauthorizedException, RoleAlreadyExistsException, NotFoundException { URL url = config.resolveURLV3(String.format(AUTHORIZATION_BASE + "roles/%s", role.getName())); HttpRequest request = HttpRequest.put(url).build(); HttpResponse httpResponse = doExecuteRequest(request, HttpURLConnection.HTTP_CONFLICT); if (httpResponse.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) { throw new RoleAlreadyExistsException(role); } } @Override public void dropRole(Role role) throws IOException, FeatureDisabledException, UnauthenticatedException, UnauthorizedException, RoleNotFoundException, NotFoundException { URL url = config.resolveURLV3(String.format(AUTHORIZATION_BASE + "roles/%s", role.getName())); HttpRequest request = HttpRequest.delete(url).build(); executeExistingRolesRequest(role, request); } @Override public Set<Role> listAllRoles() throws FeatureDisabledException, UnauthenticatedException, UnauthorizedException, IOException, NotFoundException { return listRolesHelper(null); } @Override public Set<Role> listRoles(Principal principal) throws FeatureDisabledException, UnauthenticatedException, UnauthorizedException, IOException, NotFoundException { return listRolesHelper(principal); } @Override public void addRoleToPrincipal(Role role, Principal principal) throws IOException, FeatureDisabledException, UnauthenticatedException, UnauthorizedException, RoleNotFoundException, NotFoundException { URL url = config.resolveURLV3(String.format(AUTHORIZATION_BASE + "%s/%s/roles/%s", principal.getType(), principal.getName(), role.getName())); HttpRequest request = HttpRequest.put(url).build(); executeExistingRolesRequest(role, request); } @Override public void removeRoleFromPrincipal(Role role, Principal principal) throws IOException, FeatureDisabledException, UnauthenticatedException, UnauthorizedException, RoleNotFoundException, NotFoundException { URL url = config.resolveURLV3(String.format(AUTHORIZATION_BASE + "%s/%s/roles/%s", principal.getType(), principal.getName(), role.getName())); HttpRequest request = HttpRequest.delete(url).build(); executeExistingRolesRequest(role, request); } private void revoke(RevokeRequest revokeRequest) throws IOException, UnauthenticatedException, FeatureDisabledException, UnauthorizedException, NotFoundException { URL url = config.resolveURLV3(AUTHORIZATION_BASE + "/privileges/revoke"); HttpRequest request = HttpRequest.post(url).withBody(GSON.toJson(revokeRequest)).build(); executePrivilegeRequest(revokeRequest.getEntity(), request); } private Set<Role> listRolesHelper(@Nullable Principal principal) throws IOException, FeatureDisabledException, UnauthenticatedException, UnauthorizedException, NotFoundException { URL url = principal == null ? config.resolveURLV3(AUTHORIZATION_BASE + "roles") : config.resolveURLV3(String.format(AUTHORIZATION_BASE + "%s/%s/roles", principal.getType(), principal.getName())); HttpRequest request = HttpRequest.get(url).build(); HttpResponse response = doExecuteRequest(request); if (response.getResponseCode() == HttpURLConnection.HTTP_OK) { return ObjectResponse.fromJsonBody(response, TYPE_OF_ROLE_SET).getResponseObject(); } throw new IOException(String.format("Cannot list roles. Reason: %s", response.getResponseBodyAsString())); } private void executeExistingRolesRequest(Role role, HttpRequest request) throws IOException, UnauthenticatedException, FeatureDisabledException, UnauthorizedException, RoleNotFoundException, NotFoundException { HttpResponse httpResponse = doExecuteRequest(request, HttpURLConnection.HTTP_NOT_FOUND); if (httpResponse.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new RoleNotFoundException(role); } } private HttpResponse executePrivilegeRequest(EntityId entityId, HttpRequest request) throws FeatureDisabledException, UnauthenticatedException, IOException, NotFoundException, UnauthorizedException { HttpResponse httpResponse = doExecuteRequest(request, HttpURLConnection.HTTP_NOT_FOUND); if (HttpURLConnection.HTTP_NOT_FOUND == httpResponse.getResponseCode()) { throw new NotFoundException(entityId); } return httpResponse; } private HttpResponse doExecuteRequest(HttpRequest request, int... additionalAllowedErrorCodes) throws IOException, UnauthenticatedException, FeatureDisabledException, UnauthorizedException { int[] allowedErrorCodes = new int[additionalAllowedErrorCodes.length + 2]; System.arraycopy(additionalAllowedErrorCodes, 0, allowedErrorCodes, 0, additionalAllowedErrorCodes.length); allowedErrorCodes[additionalAllowedErrorCodes.length] = HttpURLConnection.HTTP_FORBIDDEN; allowedErrorCodes[additionalAllowedErrorCodes.length + 1] = HttpURLConnection.HTTP_NOT_IMPLEMENTED; HttpResponse response = restClient.execute(request, config.getAccessToken(), allowedErrorCodes); if (HttpURLConnection.HTTP_FORBIDDEN == response.getResponseCode()) { // TODO:(CDAP-5302) Include the logged in username here InputSupplier<? extends InputStream> requestBody = request.getBody(); String msg = requestBody == null ? String.format("Unauthorized to perform %s %s", request.getMethod(), request.getURL()) : String.format("Unauthorized to perform %s %s with body %s", request.getMethod(), request.getURL(), Bytes.toString(ByteStreams.toByteArray(requestBody))); throw new UnauthorizedException(msg); } if (HttpURLConnection.HTTP_NOT_IMPLEMENTED == response.getResponseCode()) { FeatureDisabledException.Feature feature = FeatureDisabledException.Feature.AUTHORIZATION; String enableConfig = Constants.Security.Authorization.ENABLED; if (response.getResponseBodyAsString().toLowerCase().contains("authentication")) { feature = FeatureDisabledException.Feature.AUTHENTICATION; enableConfig = Constants.Security.ENABLED; } throw new FeatureDisabledException(feature, FeatureDisabledException.CDAP_SITE, enableConfig, "true"); } return response; } }