io.continual.flowcontrol.impl.jobdb.model.ModelJobDb Maven / Gradle / Ivy
package io.continual.flowcontrol.impl.jobdb.model;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.spec.KeySpec;
import java.util.Collection;
import java.util.LinkedList;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.json.JSONObject;
import io.continual.builder.Builder.BuildFailure;
import io.continual.flowcontrol.FlowControlCallContext;
import io.continual.flowcontrol.impl.jobdb.common.JsonJob;
import io.continual.flowcontrol.jobapi.FlowControlJob;
import io.continual.flowcontrol.jobapi.FlowControlJobDb;
import io.continual.iam.access.AccessControlEntry;
import io.continual.iam.access.AccessControlList;
import io.continual.iam.access.AccessException;
import io.continual.iam.exceptions.IamSvcException;
import io.continual.iam.identity.Identity;
import io.continual.iam.impl.common.CommonJsonIdentity;
import io.continual.services.ServiceContainer;
import io.continual.services.SimpleService;
import io.continual.services.model.core.Model;
import io.continual.services.model.core.ModelObject;
import io.continual.services.model.core.ModelPathList;
import io.continual.services.model.core.ModelRequestContext;
import io.continual.services.model.core.exceptions.ModelItemDoesNotExistException;
import io.continual.services.model.core.exceptions.ModelRequestException;
import io.continual.services.model.core.exceptions.ModelServiceException;
import io.continual.util.data.TypeConvertor;
import io.continual.util.naming.Name;
import io.continual.util.naming.Path;
public class ModelJobDb extends SimpleService implements FlowControlJobDb
{
public ModelJobDb ( ServiceContainer sc, JSONObject config ) throws BuildFailure
{
final JSONObject modelData = config.getJSONObject ( "model" );
fModel = io.continual.builder.Builder.fromJson ( Model.class, modelData, sc );
fModelUser = new CommonJsonIdentity ( "flowControlUser", CommonJsonIdentity.initializeIdentity (), null );
try
{
fEnc = new Enc ( config.getString ( "secretEncryptKey" ) );
}
catch ( GeneralSecurityException e )
{
throw new BuildFailure ( e );
}
}
@Override
public Builder createJob ( FlowControlCallContext fccc )
{
return new ModelFcJobBuilder ( fccc );
}
@Override
public Collection getJobsFor ( FlowControlCallContext fccc ) throws ServiceException
{
try
{
final ModelRequestContext mrc = buildContext ();
final Path path = getBaseJobPath ();
final ModelPathList pathList = fModel.listObjectsStartingWith ( mrc, path ); // FIXME: does this check READ rights already?
final LinkedList result = new LinkedList<> ();
if ( pathList != null )
{
for ( Path p : pathList )
{
try
{
final FlowControlJob job = internalLoadJob ( mrc, p.getItemName ().toString () );
if ( job != null && job.getAccessControlList ().canUser ( fccc.getUser (), AccessControlList.READ ) )
{
result.add ( job );
}
}
catch ( IamSvcException e )
{
throw new ServiceException ( e );
}
}
}
return result;
}
catch ( BuildFailure | ModelServiceException | ModelRequestException e )
{
throw new ServiceException ( e );
}
}
@Override
public FlowControlJob getJob ( FlowControlCallContext fccc, String name ) throws ServiceException, AccessException
{
try
{
final ModelRequestContext mrc = buildContext ();
final FlowControlJob job = internalLoadJob ( mrc, name );
checkAccess ( job, fccc, AccessControlList.READ );
return job;
}
catch ( BuildFailure e )
{
throw new ServiceException ( e );
}
}
@Override
public FlowControlJob getJobAsAdmin ( String name ) throws ServiceException
{
try
{
final ModelRequestContext mrc = buildContext ();
return internalLoadJob ( mrc, name );
}
catch ( BuildFailure e )
{
throw new ServiceException ( e );
}
}
@Override
public void storeJob ( FlowControlCallContext fccc, String name, FlowControlJob job ) throws ServiceException, AccessException
{
try
{
final ModelRequestContext mrc = buildContext ();
final FlowControlJob existing = internalLoadJob ( mrc, name );
checkAccess ( existing, fccc, AccessControlList.UPDATE );
internalStoreJob ( mrc, job );
}
catch ( BuildFailure e )
{
throw new ServiceException ( e );
}
}
@Override
public void removeJob ( FlowControlCallContext fccc, String name ) throws ServiceException, AccessException
{
try
{
final ModelRequestContext mrc = buildContext ();
checkAccess ( internalLoadJob ( mrc, name ), fccc, AccessControlList.UPDATE );
final Path path = jobNameToPath ( name );
fModel.remove ( mrc, path );
}
catch ( BuildFailure | ModelRequestException | ModelServiceException e )
{
throw new ServiceException ( e );
}
}
private final Model fModel;
private final Identity fModelUser;
private final Enc fEnc;
private FlowControlJob internalLoadJob ( ModelRequestContext mrc, String name ) throws ServiceException
{
try
{
final Path path = jobNameToPath ( name );
final ModelObject mo = fModel.load ( mrc, path );
return new ModelFcJob ( name, mo );
}
catch ( ModelItemDoesNotExistException e )
{
return null;
}
catch ( ModelServiceException | ModelRequestException e )
{
throw new ServiceException ( e );
}
}
private FlowControlJob internalStoreJob ( ModelRequestContext mrc, FlowControlJob job ) throws ServiceException
{
try
{
final String name = job.getName ();
final Path path = jobNameToPath ( name );
fModel.store ( mrc, path, ((ModelFcJob)job).toJson() );
return internalLoadJob ( mrc, name );
}
catch ( ModelRequestException | ModelServiceException e )
{
throw new ServiceException ( e );
}
}
private class ModelFcJobBuilder implements Builder
{
public ModelFcJobBuilder ( FlowControlCallContext fccc )
{
fCtx = fccc;
fAces = new LinkedList<> ();
withAccess ( AccessControlEntry.kOwner, AccessControlList.READ, AccessControlList.UPDATE, AccessControlList.DELETE );
}
@Override
public Builder withName ( String name )
{
fName = name;
return this;
}
@Override
public Builder withOwner ( String owner )
{
fOwner = owner;
return this;
}
@Override
public Builder withAccess ( String user, String... ops )
{
fAces.add ( AccessControlEntry.builder ()
.permit ()
.forSubject ( user )
.operations ( ops )
.build ()
);
return this;
}
@Override
public FlowControlJob build () throws RequestException, ServiceException, AccessException
{
if ( fName == null || fName.length () == 0 )
{
throw new RequestException ( "Name is not set." );
}
FlowControlJob existing = getJob ( fCtx, fName );
if ( existing != null )
{
throw new RequestException ( "Job " + fName + " already exists." );
}
try
{
final ModelRequestContext mrc = buildContext ();
final ModelFcJob job = new ModelFcJob ( this );
final AccessControlList acl = job.getAccessControlList ();
acl
.setOwner ( fOwner )
.clear ()
;
for ( AccessControlEntry ace : fAces )
{
acl.addAclEntry ( ace );
}
internalStoreJob ( mrc, job );
return internalLoadJob ( mrc, fName );
}
catch ( BuildFailure e )
{
throw new ServiceException ( e );
}
}
private final FlowControlCallContext fCtx;
private String fName;
private String fOwner;
private LinkedList fAces;
}
private static final byte[] kFixmeSalt = "salty".getBytes ( StandardCharsets.UTF_8 );
static class Enc implements JsonJob.Encryptor
{
public Enc ( String pwd ) throws GeneralSecurityException
{
fEncKey = pwd;
fCipher = Cipher.getInstance ( "AES/CBC/PKCS5Padding" );
SecretKeyFactory factory = SecretKeyFactory.getInstance ( "PBKDF2WithHmacSHA256" );
KeySpec spec = new PBEKeySpec ( fEncKey.toCharArray (), kFixmeSalt, 65536, 256 );
SecretKey tmp = factory.generateSecret ( spec );
fSec = new SecretKeySpec ( tmp.getEncoded (), "AES" );
}
@Override
public String encrypt ( String val ) throws GeneralSecurityException
{
fCipher.init ( Cipher.ENCRYPT_MODE, fSec );
AlgorithmParameters params = fCipher.getParameters ();
byte[] iv = params.getParameterSpec ( IvParameterSpec.class ).getIV ();
byte[] ciphertext = fCipher.doFinal ( val.getBytes ( StandardCharsets.UTF_8 ) );
return TypeConvertor.base64Encode ( ciphertext ) + ":" + TypeConvertor.base64Encode ( iv );
}
@Override
public String decrypt ( String val ) throws GeneralSecurityException
{
final String[] parts = val.split ( ":" );
if ( parts.length != 2 ) throw new GeneralSecurityException ( "Unexpected encrypted text format." );
final byte[] iv = TypeConvertor.base64Decode ( parts[1] );
final byte[] ciphertext = TypeConvertor.base64Decode ( parts[0] );
fCipher.init ( Cipher.DECRYPT_MODE, fSec, new IvParameterSpec ( iv ) );
return new String ( fCipher.doFinal ( ciphertext ), StandardCharsets.UTF_8 );
}
private final String fEncKey;
private final Cipher fCipher;
private final SecretKeySpec fSec;
}
private class ModelFcJob extends JsonJob
{
public ModelFcJob ( ModelFcJobBuilder builder )
{
super ( builder.fName, fEnc );
}
public ModelFcJob ( String name, ModelObject mo )
{
super ( name, fEnc, mo.getData() );
}
}
private static Path getBaseJobPath ( )
{
return Path.fromString ( "/jobs" );
}
private static Path jobNameToPath ( String name )
{
return getBaseJobPath().makeChildItem ( Name.fromString ( name ) );
}
private void checkAccess ( final FlowControlJob job, FlowControlCallContext fccc, String op ) throws AccessException, ServiceException
{
try
{
if ( job == null ) return;
if ( !job.getAccessControlList ().canUser ( fccc.getUser (), op ) )
{
throw new AccessException ( fccc.getUser() + " may not " + op + " job " + job.getId () + "." );
}
}
catch ( IamSvcException e )
{
throw new ServiceException ( e );
}
}
private ModelRequestContext buildContext () throws BuildFailure
{
return fModel.getRequestContextBuilder ().forUser ( fModelUser ).build ();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy