/*
* Copyright 2016 The Simple File Server Authors
*
* 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 org.sfs.auth;
import com.google.common.base.Optional;
import com.google.common.collect.SetMultimap;
import com.google.common.escape.Escaper;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.sfs.Server;
import org.sfs.SfsRequest;
import org.sfs.VertxContext;
import org.sfs.io.BufferWriteEndableWriteStream;
import org.sfs.io.LimitedWriteEndableWriteStream;
import org.sfs.rx.BufferToJsonObject;
import org.sfs.rx.ConnectionCloseTerminus;
import org.sfs.util.HttpBodyLogger;
import org.sfs.util.HttpRequestValidationException;
import org.sfs.vo.PersistentAccount;
import org.sfs.vo.PersistentContainer;
import org.sfs.vo.TransientAccount;
import org.sfs.vo.TransientContainer;
import org.sfs.vo.TransientVersion;
import rx.Observable;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static com.google.common.base.CharMatcher.WHITESPACE;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Objects.equal;
import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Optional.of;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Splitter.on;
import static com.google.common.collect.HashMultimap.create;
import static com.google.common.collect.Iterables.toArray;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.net.UrlEscapers.urlPathSegmentEscaper;
import static io.vertx.core.buffer.Buffer.buffer;
import static io.vertx.core.http.HttpHeaders.AUTHORIZATION;
import static io.vertx.core.http.HttpHeaders.CONTENT_LENGTH;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.lang.System.currentTimeMillis;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.util.Calendar.getInstance;
import static org.sfs.auth.Role.ADMIN;
import static org.sfs.auth.Role.USER;
import static org.sfs.auth.Role.fromValueIfExists;
import static org.sfs.io.AsyncIO.pump;
import static org.sfs.rx.Defer.aVoid;
import static org.sfs.util.DateFormatter.toDateTimeString;
import static org.sfs.util.JsonHelper.getField;
import static org.sfs.util.Limits.MAX_AUTH_REQUEST_SIZE;
import static org.sfs.util.SfsHttpHeaders.X_AUTH_TOKEN;
import static org.sfs.util.SfsHttpUtil.getRemoteServiceUrl;
import static rx.Observable.just;
public class SimpleAuthProvider implements AuthProvider {
private SetMultimap<Role, User> roles = create();
@Override
public Observable<Void> open(VertxContext<Server> vertxContext) {
JsonObject config = vertxContext.verticle().config();
JsonObject jsonObject = config.getJsonObject("auth");
if (jsonObject != null) {
for (String roleName : jsonObject.fieldNames()) {
Role role = fromValueIfExists(roleName);
checkState(role != null, "%s is not a valid role", roleName);
JsonArray jsonUsers = jsonObject.getJsonArray(roleName);
if (jsonUsers != null) {
for (Object o : jsonUsers) {
JsonObject jsonUser = (JsonObject) o;
String id = getField(jsonUser, "id");
String username = getField(jsonUser, "username");
String password = getField(jsonUser, "password");
roles.put(role, new User(id, username, password));
}
}
}
} else {
roles.clear();
}
return aVoid();
}
@Override
public int priority() {
return 0;
}
@Override
public Observable<Void> close(VertxContext<Server> vertxContext) {
roles.clear();
return aVoid();
}
@Override
public Observable<Void> authenticate(SfsRequest sfsRequest) {
return aVoid()
.doOnNext(aVoid -> {
UserAndRole userAndRole = getUserByCredentials(sfsRequest);
sfsRequest.setUserAndRole(userAndRole);
});
}
@Override
public Observable<Boolean> isAuthenticated(SfsRequest sfsRequest) {
return just(sfsRequest.getUserAndRole() != null);
}
@Override
public void handleOpenstackKeystoneAuth(SfsRequest httpServerRequest) {
httpServerRequest.pause();
VertxContext<Server> vertxContext = httpServerRequest.vertxContext();
aVoid()
.flatMap(aVoid -> {
final BufferWriteEndableWriteStream bufferWriteStream = new BufferWriteEndableWriteStream();
LimitedWriteEndableWriteStream limitedWriteStream = new LimitedWriteEndableWriteStream(bufferWriteStream, MAX_AUTH_REQUEST_SIZE);
return pump(httpServerRequest, limitedWriteStream)
.map(aVoid1 -> bufferWriteStream.toBuffer());
})
.map(new HttpBodyLogger())
.map(new BufferToJsonObject())
.map(jsonObject -> {
JsonObject authJsonObject = jsonObject.getJsonObject("auth");
JsonObject passwordCredentialsJson = authJsonObject.getJsonObject("passwordCredentials");
String username = passwordCredentialsJson.getString("username");
String password = passwordCredentialsJson.getString("password");
String tenantName = authJsonObject.getString("tenantName");
if (tenantName == null) {
tenantName = "default";
}
Set<String> selectedRoles = new HashSet<>();
for (Map.Entry<Role, Collection<User>> entry : roles.asMap().entrySet()) {
Role role = entry.getKey();
Collection<User> users = entry.getValue();
for (User user : users) {
if (equal(user.getUsername(), username)
&& equal(user.getPassword(), password)) {
selectedRoles.add(role.value());
}
}
}
if (selectedRoles.isEmpty()) {
JsonObject errorJson = new JsonObject()
.put("message", "Invalid Credentials");
throw new HttpRequestValidationException(HTTP_FORBIDDEN, errorJson);
}
Escaper escaper = urlPathSegmentEscaper();
String serviceUrl = getRemoteServiceUrl(httpServerRequest);
serviceUrl = format("%s/openstackswift001/%s", serviceUrl, escaper.escape(tenantName));
JsonObject endpointJsonObject =
new JsonObject()
.put("region", "ORD")
.put("tenantId", tenantName)
.put("publicURL", serviceUrl)
.put("internalURL", serviceUrl);
JsonArray endpointsJsonArray =
new JsonArray()
.add(endpointJsonObject);
JsonObject serviceCatalogJsonObject =
new JsonObject()
.put("type", "object-store")
.put("name", "openstackswift001")
.put("endpoints", endpointsJsonArray);
JsonArray serviceCatalogJsonArray =
new JsonArray()
.add(serviceCatalogJsonObject);
JsonObject userJsonObject =
new JsonObject()
.put("username", username)
.put("roles_links", new JsonArray())
.put("id", username);
JsonArray roles = new JsonArray();
for (String selectedRole : selectedRoles) {
roles.add(selectedRole);
}
userJsonObject = userJsonObject.put("roles", roles);
Calendar expiresDt = getInstance();
expiresDt.setTimeInMillis(currentTimeMillis() + 86400000);
JsonObject tokenJsonObject =
new JsonObject()
.put("audit_ids", new JsonArray())
.put("expires", toDateTimeString(expiresDt))
.put("issued_at", toDateTimeString(getInstance()))
.put("id", base64().encode((username + ":" + password).getBytes(UTF_8)));
JsonObject metadataJsonObject =
new JsonObject()
.put("is_admin", 0)
.put("roles", new JsonArray());
return new JsonObject()
.put("access",
new JsonObject()
.put("serviceCatalog", serviceCatalogJsonArray)
.put("token", tokenJsonObject)
.put("user", userJsonObject)
.put("metadata", metadataJsonObject)
);
})
.single()
.subscribe(new ConnectionCloseTerminus<JsonObject>(httpServerRequest) {
@Override
public void onNext(JsonObject authenticationResponse) {
Buffer buffer = buffer(authenticationResponse.encode(), UTF_8.toString());
httpServerRequest.response().setStatusCode(HTTP_OK)
.putHeader(CONTENT_LENGTH, valueOf(buffer.length()))
.write(buffer);
}
});
}
@Override
public Observable<Boolean> canObjectUpdate(SfsRequest sfsRequest, TransientVersion version) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
@Override
public Observable<Boolean> canObjectDelete(SfsRequest sfsRequest, TransientVersion version) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
@Override
public Observable<Boolean> canObjectCreate(SfsRequest sfsRequest, TransientVersion version) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
@Override
public Observable<Boolean> canObjectRead(SfsRequest sfsRequest, TransientVersion version) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
@Override
public Observable<Boolean> canContainerUpdate(SfsRequest sfsRequest, PersistentContainer container) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
@Override
public Observable<Boolean> canContainerDelete(SfsRequest sfsRequest, PersistentContainer container) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
@Override
public Observable<Boolean> canContainerCreate(SfsRequest sfsRequest, TransientContainer container) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
@Override
public Observable<Boolean> canContainerRead(SfsRequest sfsRequest, PersistentContainer container) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
@Override
public Observable<Boolean> canAccountUpdate(SfsRequest sfsRequest, PersistentAccount account) {
return aVoid()
.map(aVoid -> isAdmin(sfsRequest));
}
@Override
public Observable<Boolean> canAccountDelete(SfsRequest sfsRequest, PersistentAccount account) {
return aVoid()
.map(aVoid -> isAdmin(sfsRequest));
}
@Override
public Observable<Boolean> canAccountCreate(SfsRequest sfsRequest, TransientAccount account) {
return aVoid()
.map(aVoid -> isAdmin(sfsRequest));
}
@Override
public Observable<Boolean> canAccountRead(SfsRequest sfsRequest, PersistentAccount account) {
return aVoid()
.map(aVoid -> isAdmin(sfsRequest));
}
@Override
public Observable<Boolean> canAdmin(SfsRequest sfsRequest) {
return aVoid()
.map(aVoid -> isAdmin(sfsRequest));
}
@Override
public Observable<Boolean> canContainerListObjects(SfsRequest sfsRequest, PersistentContainer container) {
return aVoid()
.map(aVoid -> isAdminOrUser(sfsRequest));
}
protected boolean isAdminOrUser(SfsRequest sfsRequest) {
UserAndRole userAndRole = sfsRequest.getUserAndRole();
return userAndRole != null && (ADMIN.equals(userAndRole.getRole()) || USER.equals(userAndRole.getRole()));
}
protected boolean isAdmin(SfsRequest sfsRequest) {
UserAndRole userAndRole = sfsRequest.getUserAndRole();
return userAndRole != null && ADMIN.equals(userAndRole.getRole());
}
protected UserAndRole getUserByCredentials(SfsRequest sfsRequest) {
MultiMap headers = sfsRequest.headers();
Optional<String> oToken;
if (headers.contains(X_AUTH_TOKEN)) {
oToken = fromNullable(headers.get(X_AUTH_TOKEN));
} else if (headers.contains(AUTHORIZATION)) {
oToken = extractToken(headers.get(AUTHORIZATION), "Basic");
} else {
oToken = absent();
}
if (oToken.isPresent()) {
String token = oToken.get();
String decoded = new String(base64().decode(token), StandardCharsets.UTF_8);
String[] parts =
toArray(
on(':')
.limit(2)
.split(decoded),
String.class
);
if (parts.length == 2) {
String username = parts[0];
String password = parts[1];
for (Role role : new Role[]{ADMIN, USER}) {
Set<User> usersForRole = this.roles.get(role);
if (usersForRole != null) {
for (User user : usersForRole) {
if (equal(user.getUsername(), username)
&& equal(user.getPassword(), password)) {
return new UserAndRole(role, user);
}
}
}
}
}
}
return null;
}
protected Optional<String> extractToken(String authorizationHeaderValue, String authorizationType) {
if (authorizationHeaderValue != null) {
authorizationHeaderValue = WHITESPACE.trimLeadingFrom(authorizationHeaderValue);
if (authorizationHeaderValue.regionMatches(true, 0, authorizationType, 0, authorizationType.length())) {
String[] values =
toArray(
on(' ')
.limit(2)
.split(authorizationHeaderValue),
String.class);
return of(values[1]);
}
}
return absent();
}
}