Dans une précédente étape d'intégration je constatait que la partie et workflow et provisioning restait floue pour moi. Pour aller plus loin qu'un simple chargement d'identité et de compte j'ai mis au point un service d'identité qui gère un cycle de vie simpliste: création, mise à jour, suppression. J'ai adapté le connecteur RestPerson et la ressource Person pour traiter la gestion du changelog. Cela tient principalement au script groovy suivant:
def buildConnectorObject(node) {
return [
__UID__:node.get("personId").textValue(),
__NAME__:node.get("personId").textValue(),
__ENABLE__:node.get("status").textValue().equals("ACTIVE"),
personId:node.get("personId").textValue(),
prenom:node.get("firstname").textValue(),
nom:node.get("lastname").textValue(),
operationalUnit:node.get("operationalUnit").textValue()
];
}
log.info("Entering " + action + " Script");
WebClient webClient = client;
ObjectMapper mapper = new ObjectMapper();
if (action.equalsIgnoreCase("GET_LATEST_SYNC_TOKEN")) {
switch (objectClass) {
case "__PERSON__":
latestToken = new Date().getTime();
break;
default:
latestToken = null;
}
return latestToken;
} else if (action.equalsIgnoreCase("SYNC")) {
def result = [];
switch (objectClass) {
case "__PERSON__":
webClient.path("/persons/changelog");
if (token != null) {
webClient.query("from", token.toString());
}
log.ok("Sending GET to {0}", webClient.getCurrentURI().toASCIIString());
Response response = webClient.get();
log.ok("CHANGELOG response: {0} {1}", response.getStatus(), response.getHeaders());
if (response.getStatus() != 200) {
throw new RuntimeException("Unexpected response from server: "
+ response.getStatus() + " " + response.getHeaders());
}
ArrayNode node = mapper.readTree(response.getEntity());
for (int i = 0; i < node.size(); i++) {
if (node.get(i).get("deleted").booleanValue()) {
result.add([
operation:"DELETE",
uid:node.get(i).get("person").get("personId").textValue(),
token:node.get(i).get("lastChangeDate").longValue(),
attributes:[:]
]);
} else {
result.add([
operation:"CREATE_OR_UPDATE",
uid:node.get(i).get("person").get("personId").textValue(),
token:node.get(i).get("lastChangeDate").longValue(),
attributes:buildConnectorObject(node.get(i).get("person"))
]);
}
}
break;
}
log.ok("Sync script: found " + result.size() + " events to sync");
return result;
} else {
log.error("Sync script: action '" + action + "' is not implemented in this script");
return null;
}
Le service d'identité expose une ressource changelog auquel on passe un point de synchronisation et ce service renvoie des changements qui sont des suppressions ou des modifications. Le script groovy requête le service d'identité et lui passe un point de synchronisation, puis en retour analyse les changements et retourne des objets pour Syncope.
Mais pas que. Comme expliqué dans le schéma, tout doit arriver dans le ProvisioningManager qui ensuite dispatch des traitements et en particulier le lancement du workflow. La documentation parlait d'un workflow de base, mais je ne l'avais pas identifié, puisque le seul présenté est celui de Flowable pour les USERs. Comme je travaille sur des objets cela n'aide pas, mais finalement on a bien une classe qui le supporte.
Comme suggéré, j'ai une implémentation du workflow et du provisioning manager par défaut, de façon que pour un objet PERSON nouveau ou supprimé, son compte primaire soit créé ou supprimé.
Ce que je montre dans une vidéo, c'est que ça fait le job simpliste que je demande, mais je me retrouve dans une situation où je ne sais pas vraiment où coder la logique "métier" qui accompagne le cycle de vie des identités.
Est-ce dans le Provisioning Manager ou plus loin dans le workflow ? En tout cas, pour l'instant, c'est au fin fond d'un code Java, ce qui ne devrait pas enchanter des clients.
Cela dit techniquement quand dans une tâche de workflow Objet, je fais des opérations sur des USERs, le programme bug et ne traite pas toute les opérations attendues. Je peux en conclure que pour une raison mystérieuse, dans un workflow on n'introduit pas d'autre objet que ceux prévus pour lui.
@Configuration("PersonProvisioningConfiguration")
@EnableConfigurationProperties(ProvisioningProperties.class)
public class PersonProvisioningConfiguration {
//Le provisioning Manager qui implémente la gestion des objets PERSON
@Bean
public AnyObjectProvisioningManager anyObjectProvisioningManager(
final AnyObjectWorkflowAdapter awfAdapter,
final PropagationManager propagationManager,
final PropagationTaskExecutor taskExecutor,
final AnyObjectDAO anyObjectDAO) {
return new PersonProvisioningManager(
awfAdapter,
propagationManager,
taskExecutor,
anyObjectDAO);
}
//Le workflow basique qui peut servir pour les objets PERSON
@Bean
public AnyObjectWorkflowAdapter awfAdapter(
final AnyObjectDataBinder anyObjectDataBinder,
final AnyObjectDAO anyObjectDAO,
final GroupDAO groupDAO,
final EntityFactory entityFactory,
final ApplicationEventPublisher publisher) {
return new PersonAnyObjectWorkflowAdapter(
anyObjectDataBinder,
anyObjectDAO,
groupDAO,
entityFactory,
publisher);
}
}
public class PersonProvisioningManager extends DefaultAnyObjectProvisioningManager {
@Autowired
UserProvisioningManager userProvisioningManager;
@Autowired
protected UserDAO userDAO;
protected static final Logger LOG = LogManager.getLogger(PersonProvisioningManager.class);
public PersonProvisioningManager(AnyObjectWorkflowAdapter awfAdapter, PropagationManager propagationManager, PropagationTaskExecutor taskExecutor, AnyObjectDAO anyObjectDAO) {
super(awfAdapter, propagationManager, taskExecutor, anyObjectDAO);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public ProvisioningResult<String> create(
final AnyObjectCR anyObjectCR,
final Set<String> excludedResources,
final boolean nullPriorityAsync,
final String creator,
final String context){
//on teste si c'est un objet PERSON car alors on va peut-être créer un compte d'accès primaire
if(!anyObjectCR.getType().equals("PERSON")){
LOG.info("pas un objet PERSON <> "+anyObjectCR.getType());
return super.create(anyObjectCR, excludedResources, nullPriorityAsync, creator, context);
}
LOG.info("1 Création d'une personne name="+anyObjectCR.getName());
//on fait appel au workflow objet qui reconnait les PERSON
WorkflowResult<String> created = awfAdapter.create(anyObjectCR, creator, context);
LOG.info("retour du workflow "+created.getResult());
//je sais pas ce qu'il raconte en cas de réussite ou échec ?
//on cherche un USER avec un personId == anyObjectCR.getName();
List<User> users = null;
boolean erreur = false;
try{
users = userDAO.findByDerAttrValue("personName==", anyObjectCR.getName(), false);
}catch(Exception ex){
LOG.error("Erreur recherche USER "+ex);
erreur = true;
}
if(!erreur && users != null && !users.isEmpty()){
//on trouve un des comptes
LOG.info("2 Déjà des comptes, mais conforme aux besoins métiers ?");
}else if(!erreur){
LOG.info("2 Pas comptes, mais répondre aux besoins métiers ?");
UserCR novo = new UserCR.Builder("/", "usr-"+anyObjectCR.getName())
.plainAttr(new Attr.Builder("personName").value(anyObjectCR.getName()).build()).build();
userProvisioningManager.create(novo, nullPriorityAsync, creator, context);
}
//Je sais pas encore comment gérer cette partie
List<PropagationTaskInfo> taskInfos = propagationManager.getCreateTasks(
AnyTypeKind.ANY_OBJECT,
created.getResult(),
null,
created.getPropByRes(),
excludedResources);
//en fait je retourne zéro pour l'instant
LOG.info("3 Taches de propagation nb="+taskInfos.size());
for(PropagationTaskInfo taskInfo : taskInfos){
LOG.info("4 Tache de propagation key="+taskInfo.getKey());
}
PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, creator);
return new ProvisioningResult<>(created.getResult(), propagationReporter.getStatuses());
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public List<PropagationStatus> delete(
final String key,
final Set<String> excludedResources,
final boolean nullPriorityAsync,
final String eraser,
final String context) {
LOG.info("1 Suppression un objet clé="+key);
Optional<? extends AnyObject> anyObject = anyObjectDAO.findById(key);
boolean objDel = false;
if(anyObject.isEmpty() || !anyObject.isPresent()){
LOG.warn("Pas retrouvé l'objet clé="+key);
}else{
LOG.info("2 Suppression un objet type="+anyObject.get().getType()+
" avec name="+anyObject.get().getName());
objDel = true;
}
if(objDel && anyObject.get().getType().toString().equals("JPAAnyType[PERSON]")){
LOG.info("3 Suppression un objet PERSON dans provisioning name="+
anyObject.get().getName()+" key="+key);
List<User> users = null;
boolean erreur = false;
try{
users = userDAO.findByDerAttrValue("personName==", anyObject.get().getName(), false);
}catch(Exception ex){
LOG.error("Erreur recherche USER "+ex);
erreur = true;
}
if(!erreur && users != null && !users.isEmpty()){
//on trouve un des comptes
LOG.info("4 Trouver des comptes, on flingue");
for(User user : users){
userDAO.delete(user);
}
}
}
PropagationByResource<String> propByRes = new PropagationByResource<>();
propByRes.set(ResourceOperation.DELETE, anyObjectDAO.findAllResourceKeys(key));
// Note here that we can only notify about "delete", not any other
// task defined in workflow process definition: this because this
// information could only be available after awfAdapter.delete(), which
// will also effectively remove user from db, thus making virtually
// impossible by NotificationManager to fetch required any object information
//En gros les informations / objet produits dans le workflow sont inaccessibles car il va supprimer l'objet
//donc seuls les "ressources liées" sont prévenues
List<PropagationTaskInfo> taskInfos = propagationManager.getDeleteTasks(
AnyTypeKind.ANY_OBJECT,
key,
propByRes,
null,
excludedResources);
PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, eraser);
awfAdapter.delete(key, eraser, context);
return propagationReporter.getStatuses();
}
}
public class PersonAnyObjectWorkflowAdapter extends DefaultAnyObjectWorkflowAdapter {
protected static final Logger LOG = LogManager.getLogger(PersonAnyObjectWorkflowAdapter.class);
@Autowired
protected UserDAO userDAO;
public PersonAnyObjectWorkflowAdapter(AnyObjectDataBinder dataBinder,
AnyObjectDAO anyObjectDAO, GroupDAO groupDAO, EntityFactory entityFactory,
ApplicationEventPublisher publisher) {
super(dataBinder, anyObjectDAO, groupDAO, entityFactory, publisher);
}
@Override
protected WorkflowResult<String> doCreate(
final AnyObjectCR anyObjectCR, final String creator,
final String context) {
if(anyObjectCR.getType().equals("PERSON")){
LOG.info("Création un objet PERSON dans le workflow name="+anyObjectCR.getName());
}
//on fait rien de plus
return super.doCreate(anyObjectCR, creator, context);
}
@Override
protected WorkflowResult<AnyObjectUR> doUpdate(
final AnyObject anyObject, final AnyObjectUR anyObjectUR,
final String updater, final String context) {
LOG.info("A Mise à jour un objet type="+anyObject.getType().toString());
if(anyObject.getType().toString().equals("JPAAnyType[PERSON]")){
LOG.info("B Mise à jour un objet PERSON dans le workflow name="+
anyObject.getName()+" key="+anyObjectUR.getKey());
}
//on fait rien de plus
return super.doUpdate(anyObject, anyObjectUR, updater, context);
}
@Override
protected void doDelete(final AnyObject anyObject, final String eraser, final String context) {
LOG.info("A Suppression un objet type="+anyObject.getType().toString());
//si je décommente alors ça plante tous le traitement de PULL qui fait du DELETE !!
//en même temps c'est déjà fait au niveau du gestionnaire de provisioning
/*if(anyObject.getType().toString().equals("JPAAnyType[PERSON]")){
LOG.info("B Suppression un objet PERSON dans le workflow name="+
anyObject.getName()+" key="+anyObject.getKey());
List<User> users = null;
boolean erreur = false;
try{
users = userDAO.findByDerAttrValue("personName==", anyObject.getName(), false);
}catch(Exception ex){
LOG.error("Erreur recherche USER "+ex);
erreur = true;
}
if(!erreur && users != null && !users.isEmpty()){
//on trouve un des comptes
LOG.info("C Trouver des comptes, on flingue");
for(User user : users){
userDAO.delete(user);
}
}
}*/
anyObjectDAO.delete(anyObject);
publisher.publishEvent(
new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, anyObject, AuthContextUtils.getDomain()));
}