/**
*
* Copyright
* 2009-2015 Jayway Products AB
* 2016-2017 Föreningen Sambruk
*
* Licensed under AGPL, Version 3.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.gnu.org/licenses/agpl.txt
*
* 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 se.streamsource.streamflow.web.application.security;
import java.io.IOException;
import net.sf.ehcache.Element;
import org.qi4j.api.configuration.Configuration;
import org.qi4j.api.entity.EntityReference;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.injection.scope.This;
import org.qi4j.api.injection.scope.Uses;
import org.qi4j.api.mixin.Mixins;
import org.qi4j.api.service.Activatable;
import org.qi4j.api.service.ServiceComposite;
import org.qi4j.api.structure.Module;
import org.qi4j.api.unitofwork.ConcurrentEntityModificationException;
import org.qi4j.api.unitofwork.NoSuchEntityException;
import org.qi4j.api.unitofwork.UnitOfWork;
import org.qi4j.api.unitofwork.UnitOfWorkCompletionException;
import org.qi4j.api.usecase.Usecase;
import org.qi4j.api.usecase.UsecaseBuilder;
import org.qi4j.api.value.ValueBuilder;
import org.qi4j.spi.service.ServiceDescriptor;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeRequest;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.ClientResource;
import org.restlet.resource.ResourceException;
import org.restlet.routing.Filter;
import org.restlet.security.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.streamsource.infrastructure.circuitbreaker.CircuitBreaker;
import se.streamsource.infrastructure.circuitbreaker.service.ServiceCircuitBreaker;
import se.streamsource.streamflow.api.workspace.cases.contact.ContactDTO;
import se.streamsource.streamflow.api.workspace.cases.contact.ContactEmailDTO;
import se.streamsource.streamflow.api.workspace.cases.contact.ContactPhoneDTO;
import se.streamsource.streamflow.server.plugin.authentication.Authenticator;
import se.streamsource.streamflow.server.plugin.authentication.UserDetailsValue;
import se.streamsource.streamflow.web.domain.entity.organization.OrganizationsEntity;
import se.streamsource.streamflow.web.domain.entity.user.UserEntity;
import se.streamsource.streamflow.web.domain.entity.user.UsersEntity;
import se.streamsource.streamflow.web.domain.interaction.security.Authentication;
import se.streamsource.streamflow.web.domain.structure.organization.Organization;
import se.streamsource.streamflow.web.domain.structure.organization.Organizations;
import se.streamsource.streamflow.web.domain.structure.user.Contactable;
import se.streamsource.streamflow.web.infrastructure.caching.Caches;
import se.streamsource.streamflow.web.infrastructure.caching.Caching;
import se.streamsource.streamflow.web.infrastructure.caching.CachingService;
@Mixins(AuthenticationFilterService.Mixin.class)
public interface AuthenticationFilterService extends ServiceComposite, Configuration, Activatable, ServiceCircuitBreaker
{
public int beforeHandle(Request request, Response response, Context context);
abstract class Mixin
implements AuthenticationFilterService
{
private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
private static Usecase verifyUsecase = UsecaseBuilder.newUsecase("Verify password");
private static Usecase addUsecase = UsecaseBuilder.newUsecase("Add new user");
private static Usecase updateUsecase = UsecaseBuilder.newUsecase("Update user");
@Uses
ServiceDescriptor descriptor;
@This
Configuration<AuthenticationFilterServiceConfiguration> config;
@Service
CachingService cachingService;
Caching caching;
@Structure
Module module;
CircuitBreaker circuitBreaker;
public void activate() throws Exception
{
circuitBreaker = descriptor.metaInfo( CircuitBreaker.class );
config.configuration();
caching = new Caching( cachingService, Caches.VERIFIEDUSERS );
}
public void passivate() throws Exception
{
logger.info( "Passivated" );
}
public CircuitBreaker getCircuitBreaker()
{
return circuitBreaker;
}
public int beforeHandle(Request request, Response response, Context context)
{
ChallengeResponse challengeResponse = request.getChallengeResponse();
if (challengeResponse == null)
{
response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
response.getChallengeRequests().add(new ChallengeRequest(ChallengeScheme.HTTP_BASIC, "Streamflow"));
return Filter.STOP;
} else
{
String username = challengeResponse.getIdentifier();
String password = new String(challengeResponse.getSecret());
UnitOfWork unitOfWork = module.unitOfWorkFactory().newUnitOfWork(verifyUsecase);
Authentication localUser = null;
try
{
try
{
localUser = unitOfWork.get(Authentication.class, username);
} catch (NoSuchEntityException e)
{
// This is ok
}
Element cachedUser = caching.get(username);
if (cachedUser != null && localUser != null && localUser.login(password))
{
setUserCredentials(request, context, username);
return Filter.CONTINUE;
}
boolean authorized = false;
if (!UserEntity.ADMINISTRATOR_USERNAME.equals(username) && config.configuration().enabled().get() && circuitBreaker.isOn())
{
ClientResource clientResource = new ClientResource(config.configuration().url().get() + "/authentication/userdetails" );
clientResource.setChallengeResponse(ChallengeScheme.HTTP_BASIC, username, password);
try
{
// Call plugin
Representation result = clientResource.get();
String json;
try
{
json = result.getText();
} catch (IOException e)
{
throw new ResourceException(Status.CLIENT_ERROR_UNAUTHORIZED,
"Could not get userdetails for externally validated user");
}
UserDetailsValue externalUser = module.valueBuilderFactory().newValueFromJSON(UserDetailsValue.class, json);
if (localUser == null)
{
createNewUser(externalUser, username, password);
} else
{
updateUser(externalUser, (Contactable.Data) localUser, password);
}
caching.put(new Element(username, username));
setUserCredentials(request, context, username);
authorized = true;
logger.debug("User: " + username + " - successfully authenticated agains external system");
circuitBreaker.success();
} catch (ResourceException e)
{
if (Status.CLIENT_ERROR_UNAUTHORIZED.equals(clientResource.getResponse().getStatus()))
{
circuitBreaker.success();
// Authentication failed
response.setStatus(clientResource.getResponse().getStatus());
response.getChallengeRequests()
.add(new ChallengeRequest(ChallengeScheme.HTTP_BASIC, "Streamflow"));
return Filter.STOP;
} else
{
// Send this to CircuitBreaker
circuitBreaker.throwable( e );
}
if (localUser != null && localUser.login(password))
{
authorized = true;
setUserCredentials(request, context, username);
}
}
} else
{
if (localUser != null && localUser.login(password))
{
authorized = true;
setUserCredentials(request, context, username);
}
}
if (!authorized)
{
response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
response.getChallengeRequests().add(new ChallengeRequest(ChallengeScheme.HTTP_BASIC, "Streamflow"));
response.setEntity(Authenticator.error.authentication_bad_username_password.toString(),
MediaType.TEXT_PLAIN);
return Filter.STOP;
}
} finally
{
unitOfWork.discard();
}
return Filter.CONTINUE;
}
}
private void updateUser(UserDetailsValue externalUser, Contactable.Data user, String password)
{
boolean modified = false;
if (!externalUser.name().get().equals(user.contact().get().name().get()))
{
modified = true;
}
if (!externalUser.emailAddress().get()
.equals(user.contact().get().emailAddresses().get().get(0).emailAddress().get()))
{
modified = true;
}
if (!externalUser.phoneNumber().get()
.equals(user.contact().get().phoneNumbers().get().get(0).phoneNumber().get()))
{
modified = true;
}
if (!((Authentication) user).login(password))
{
modified = true;
}
if (modified)
{
try
{
UnitOfWork unitOfWork = module.unitOfWorkFactory().newUnitOfWork(updateUsecase);
Organization org = unitOfWork.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ).organization().get();
UserEntity userEntity = unitOfWork.get(UserEntity.class, EntityReference.getEntityReference( user )
.identity());
userEntity.resetPassword(password);
userEntity.join( org );
ValueBuilder<ContactDTO> contactBuilder = module.valueBuilderFactory().newValueBuilder(ContactDTO.class);
contactBuilder.prototype().name().set(externalUser.name().get());
ValueBuilder<ContactEmailDTO> emailBuilder = module.valueBuilderFactory().newValueBuilder(ContactEmailDTO.class);
emailBuilder.prototype().emailAddress().set(externalUser.emailAddress().get());
contactBuilder.prototype().emailAddresses().get().add(emailBuilder.newInstance());
ValueBuilder<ContactPhoneDTO> phoneBuilder = module.valueBuilderFactory().newValueBuilder(ContactPhoneDTO.class);
phoneBuilder.prototype().phoneNumber().set(externalUser.phoneNumber().get());
contactBuilder.prototype().phoneNumbers().get().add(phoneBuilder.newInstance());
((Contactable) userEntity).updateContact(contactBuilder.newInstance());
unitOfWork.complete();
} catch (ConcurrentEntityModificationException e)
{
throw new ResourceException(Status.CLIENT_ERROR_UNAUTHORIZED, "Could not update user in local repository");
} catch (UnitOfWorkCompletionException e)
{
throw new ResourceException(Status.CLIENT_ERROR_UNAUTHORIZED, "Could not update user in local repository");
}
}
}
private void createNewUser(UserDetailsValue externalUser, String username, String password)
{
try
{
UnitOfWork unitOfWork = module.unitOfWorkFactory().newUnitOfWork(addUsecase);
UsersEntity usersEntity = unitOfWork.get(UsersEntity.class, UsersEntity.USERS_ID);
Organization org = unitOfWork.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ).organization().get();
se.streamsource.streamflow.web.domain.structure.user.User user = usersEntity.createUser(username, password);
user.join( org );
ValueBuilder<ContactDTO> contactBuilder = module.valueBuilderFactory().newValueBuilder(ContactDTO.class);
contactBuilder.prototype().name().set(externalUser.name().get());
ValueBuilder<ContactEmailDTO> emailBuilder = module.valueBuilderFactory().newValueBuilder(ContactEmailDTO.class);
emailBuilder.prototype().emailAddress().set(externalUser.emailAddress().get());
contactBuilder.prototype().emailAddresses().get().add(emailBuilder.newInstance());
ValueBuilder<ContactPhoneDTO> phoneBuilder = module.valueBuilderFactory().newValueBuilder(ContactPhoneDTO.class);
phoneBuilder.prototype().phoneNumber().set(externalUser.phoneNumber().get());
contactBuilder.prototype().phoneNumbers().get().add(phoneBuilder.newInstance());
((Contactable) user).updateContact(contactBuilder.newInstance());
unitOfWork.complete();
} catch (ConcurrentEntityModificationException e)
{
throw new ResourceException(Status.CLIENT_ERROR_UNAUTHORIZED, "Could not add user to local repository");
} catch (UnitOfWorkCompletionException e)
{
throw new ResourceException(Status.CLIENT_ERROR_UNAUTHORIZED, "Could not add user to local repository");
}
}
private void setUserCredentials(Request request, Context context, String username)
{
request.getClientInfo().setUser(new User(username));
context.getDefaultEnroler().enrole(request.getClientInfo());
}
}
}