Статьи

Разрешение разрешения с Neo4j — Часть 1

i_can_haz_permissions

Люди производят много контента. Сообщения, текстовые файлы, электронные таблицы, презентации, отчеты, финансовые показатели и т. Д., Список можно продолжить. Обычно организации хотят иметь хранилище всего этого контента где-то централизованным (на случай, если ноутбук, например, сломается, будет потерян или украден). Это приводит к некоторой группировке и структуре разрешений. Вы не хотите, чтобы сотрудники просматривали записи о людских ресурсах друг друга, если только они не работают в отделе кадров, не работают в том же отделе расчета заработной платы, не выпускают квартальные данные и т. Д. По мере того, как эти данные растут, больше не становится просто ориентироваться, а поисковая система должна иметь смысл. всего этого

Но что, если ваша поисковая система выдаст 1000 результатов для запроса, а пользователь, выполняющий поиск, должен иметь доступ только к 4 вещам? Как вы справляетесь с этим? Проверять пользовательские права на каждый файл в реальном времени? Медленный. Предварительно рассчитать все разрешения документа для пользователя при входе в систему? Медленно, а что, если новые документы создаются или разрешения изменяются между логинами? Масштабируется ли система на 1 млн документов, 10 млн документов, 100 млн документов?

В примерах моделирования данных Neo4j есть решение с использованием графика.

ACL

Пользователи имеют прямые права доступа к документам (или папкам) или через группы. Папки могут иметь много дочерних документов, которые могут иметь много дочерних документов и «черепах на всем пути».

Наличие их на графике позволяет легко:

  • Найти все файлы в структуре каталогов
  • Какие файлы принадлежат кому?
  • У кого есть доступ к файлу?
  • … и так далее

Итак, вернемся к нашему вопросу. Что если поисковая система вернула 1000 записей, и нам нужно было узнать, к какому из них пользователь имеет доступ. Как мы можем сделать это на графике?

Не начиная с пользователя и обращаясь ко всем возможным документам, к которым у него есть доступ (так как это может занять очень много времени в графе документа 100M). Вместо этого мы начнем с документов и посмотрим, имеют ли они прямое право доступа к пользователю или группам, к которым принадлежит пользователь. Мы делаем это для всех документов, а для тех, с которыми у нас нет прямых отношений, мы перемещаемся в родительскую папку и пробуем снова. Мы будем умны и объединяем любые документы, имеющие одного и того же родителя, чтобы уменьшить количество проверок. Делая это таким образом, мы будем пересекать тысячи отношений (вместо миллионов) в зависимости от глубины структуры документа.

Я собираюсь пройтись по коду с помощью Neo4j Core API внутри неуправляемого расширения, чтобы к нему можно было получить доступ через REST, и я покажу вам, как я написал пару тестов производительности (один случайный и один статический), а также как я генерировал наши тестовые данные. Этот проект с открытым исходным кодом и доступен на GitHub .

Давайте углубимся. Нам нужно создать тест для нашего неуправляемого расширения, но мы не можем тестировать без некоторых примеров данных, поэтому давайте начнем с создания ImpermanentGraphDatabase в MyServiceTest.java :

private ImpermanentGraphDatabase db;
private MyService service;
private ObjectMapper objectMapper = new ObjectMapper();
private static final RelationshipType KNOWS = DynamicRelationshipType.withName("KNOWS");
private static final RelationshipType SECURITY = DynamicRelationshipType.withName("SECURITY");
private static final RelationshipType IS_MEMBER_OF = DynamicRelationshipType.withName("IS_MEMBER_OF");
private static final RelationshipType HAS_CHILD_CONTENT = DynamicRelationshipType.withName("HAS_CHILD_CONTENT");

Перед выполнением наших тестов мы хотим заполнить наш график, а затем разобрать его:

@Before
public void setUp() {
    db = new ImpermanentGraphDatabase();
    populateDb(db);
    service = new MyService();
}
 
@After
public void tearDown() throws Exception {
    db.shutdown();
}

Мы создадим несколько пользователей, группы, документы, а затем свяжем их друг с другом:

private void populateDb(GraphDatabaseService db) {
    Transaction tx = db.beginTx();
    try
    {
        Node personA = createPerson(db, "A");
        Node personB = createPerson(db, "B");
        Node personC = createPerson(db, "C");
        Node personD = createPerson(db, "D");
        Node doc1 = createDocument(db, "DOC1");
        Node doc2 = createDocument(db, "DOC2");
        Node doc3 = createDocument(db, "DOC3");
        Node doc4 = createDocument(db, "DOC4");
        Node doc5 = createDocument(db, "DOC5");
        Node doc6 = createDocument(db, "DOC6");
        Node doc7 = createDocument(db, "DOC7");
        Node g1 = createGroup(db, "G1");
        Node g2 = createGroup(db, "G2");
 
        personA.createRelationshipTo(personB, KNOWS);
        personB.createRelationshipTo(personC, KNOWS);
        personC.createRelationshipTo(personD, KNOWS);
 
        personA.createRelationshipTo(g1, IS_MEMBER_OF);
        personB.createRelationshipTo(g2, IS_MEMBER_OF);
 
        doc1.createRelationshipTo(doc4, HAS_CHILD_CONTENT);
        doc2.createRelationshipTo(doc5, HAS_CHILD_CONTENT);
        doc5.createRelationshipTo(doc7, HAS_CHILD_CONTENT);
 
        Relationship secA1 = createPermission(personA, doc1, "R");
        Relationship secA3 = createPermission(personA, doc3, "RW");
        Relationship secB4 = createPermission(personB, doc4, "R");
        Relationship secG2 = createPermission(g1, doc2, "R");
        Relationship secG6 = createPermission(g2, doc6, "R");
 
        tx.success();
    }
    finally
    {
        tx.finish();
    }
}

Методы CreatePerson, CreateDocument и CreateGroup выглядят так:

private Node createPerson(GraphDatabaseService db, String uid) {
        Index<Node> people = db.index().forNodes("Users");
        Node node = db.createNode();
        node.setProperty("unique_id", uid);
        people.add(node, "unique_id", uid);
        return node;
    }

Метод CreatePermission немного отличается, так как мы создаем отношения.

private Relationship createPermission(Node person, Node doc, String permission) {
    Relationship sec = person.createRelationshipTo(doc, SECURITY);
    sec.setProperty("flags", permission);
    return sec;
}

Наконец, мы напишем пару тестов, которые, как только мы получим метод «permissions», должны пройти:

@Test
public void shouldRespondToPermissions() throws BadInputException, IOException {
    String ids = "A,DOC1 DOC2 DOC3 DOC4 DOC5 DOC6 DOC7";
    Response response =  service.permissions(ids, db);
    List list = objectMapper.readValue((String) response.getEntity(), List.class);
    assertEquals(new HashSet<String>(Arrays.asList("DOC1", "DOC2", "DOC3", "DOC4", "DOC5", "DOC7")), new HashSet<String>(list));
}
 
@Test
public void shouldRespondToPermissions2() throws BadInputException, IOException {
    String ids = "B,DOC1 DOC2 DOC3 DOC4 DOC5 DOC6 DOC7";
    Response response =  service.permissions(ids, db);
    List list = objectMapper.readValue((String) response.getEntity(), List.class);
    assertEquals(new HashSet<String>(Arrays.asList("DOC4", "DOC6")), new HashSet<String>(list));
}

Отсюда мы переходим к фактическому алгоритму графа в MyService.java :

Сначала мы определяем наши типы отношений:

private ObjectMapper objectMapper = new ObjectMapper();
private static enum RelTypes implements RelationshipType
{
    IS_MEMBER_OF,
    SECURITY,
    HAS_CHILD_CONTENT
}

Наш метод разрешений принимает строковый параметр, который содержит идентификатор пользователя, запятую и разделенный пробелами список идентификаторов документов. Мы берем входные данные и инициализируем некоторые переменные, которые будут содержать наши данные при прохождении графика.

@POST
@Path("/permissions")
public Response permissions(String body, @Context GraphDatabaseService db) throws IOException {
    String[] splits = body.split(",");
    PermissionRequest ids = new PermissionRequest(splits[0], splits[1]);
    Set<String> documents = new HashSet<String>();
    Set<Node> documentNodes = new HashSet<Node>();
    List<Node> groupNodes = new ArrayList<Node>();
    Set<Node> parentNodes = new HashSet<Node>();
    HashMap<Node, ArrayList<Node>> foldersAndDocuments = new HashMap<Node, ArrayList<Node>>();

Нам нужно найти пользователя, и все документы, переданные в качестве поисковых запросов из нашей поисковой системы:

IndexHits<Node> uid = db.index().forNodes("Users").get("unique_id", ids.userAccountUid);
IndexHits<Node> docids = db.index().forNodes("Documents").query("unique_id:(" + ids.documentUids + ")");
try
{
    for ( Node node : docids )
    {
        documentNodes.add(node);
    }
}
finally
{
    docids.close();
}

Нам также понадобятся все группы, к которым принадлежит пользователь:

if ( uid.size() > 0 && documentNodes.size() > 0)
    {
        Node user = uid.getSingle();
        for ( Relationship relationship : user.getRelationships(
                RelTypes.IS_MEMBER_OF, Direction.OUTGOING ) )
        {
            groupNodes.add(relationship.getEndNode());
        }

Мы собираемся перебрать эти узлы документа. В цикле мы проверяем, имеет ли пользовательский узел прямую связь с документом, и, если да, добавляют его (и любые возможные дочерние документы в список «документов», который мы вернем в конце.

Iterator listIterator ;
do {
    listIterator = documentNodes.iterator();
    Node document = (Node) listIterator.next();
    listIterator.remove();
 
    //Check against user
    Node found = getAllowed(document, user);
    if (found != null) {
        if (foldersAndDocuments.get(found) != null) {
            for(Node docs : foldersAndDocuments.get(found)) {
                documents.add(docs.getProperty("unique_id").toString());
            }
        } else {
            documents.add(found.getProperty("unique_id").toString());
        }
    }

Если пользователь не имеет прямого подключения к документу, возможно, одна из групп, к которым он принадлежит, делает:

//Check against user Groups
for (Node group : groupNodes){
    found = getAllowed(document, group);
    if (found != null) {
        if (foldersAndDocuments.get(found) != null) {
            for(Node docs : foldersAndDocuments.get(found)) {
                documents.add(docs.getProperty("unique_id").toString());
            }
        } else {
            documents.add(found.getProperty("unique_id").toString());
        }
    }
}

Если у нас не получилось настроить родительский узел этого документа:

// Did not find a security relationship, go up the folder chain
Relationship parentRelationship = document.getSingleRelationship(RelTypes.HAS_CHILD_CONTENT,Direction.INCOMING);
if (parentRelationship != null){
    Node parent = parentRelationship.getStartNode();
    ArrayList<Node> myDocs = foldersAndDocuments.get(document);
    if(myDocs == null) myDocs = new ArrayList<Node>();
 
    ArrayList<Node> existingDocs = foldersAndDocuments.get(parent);
    if(existingDocs == null) existingDocs = new ArrayList<Node>();
 
    for (Node myDoc:myDocs) {
        existingDocs.add(myDoc);
    }
    if (myDocs.isEmpty()) existingDocs.add(document);
    foldersAndDocuments.put(parent, existingDocs);
    parentNodes.add(parent);
}

Если мы добираемся до конца наших документов, мы переключаемся к родителям наших документов и перебираем их, пока не останется больше родителей (не достигли корневой папки нашего дерева документов).

    if(listIterator.hasNext() == false){
        documentNodes.clear();
 
        for( Node parentNode : parentNodes){
            documentNodes.add(parentNode);
        }
 
        parentNodes.clear();
        listIterator = documentNodes.iterator();
    }
 
} while (listIterator.hasNext());

Если мы не столкнулись с ошибкой, мы возвращаем список документов:

    } else {documents.add("Error: User or Documents not found");}
 
    uid.close();
 
    return Response.ok().entity(objectMapper.writeValueAsString(documents)).build();
}

Метод getAllowed просто проверяет, найдены ли отношения безопасности с флагом свойства, содержащим «R»:

private Node getAllowed(Node from, Node to){
        ConnectedResult connectedResult = isConnected(from, to, Direction.INCOMING, RelTypes.SECURITY);
        if (connectedResult.isConnected){
            if (connectedResult.connectedRelationship.getProperty("flags").toString().contains("R")) {
                return connectedResult.connectedRelationship.getEndNode();
            }
        }
        return null;
    }

Метод isConnected позволяет нам узнать, связаны ли два узла друг с другом какими-то направленными отношениями:

private ConnectedResult isConnected(Node from, Node to, Direction dir, RelationshipType type) {
        for (Relationship r : from.getRelationships(dir, type)) {
            if (r.getOtherNode(from).equals(to)) return new ConnectedResult(true, r);
        }
        return new ConnectedResult(false, null);
    }

Теперь, когда мы запускаем наши тесты:

прохождение испытаний

Но как быстро это … и это масштабируется? Чтобы выяснить это, нам нужно создать увеличенный график и набор тестов производительности .
Оставайтесь с нами для второй части.