Poznaj podstawową składnię języka reguł bezpieczeństwa bazy danych czasu rzeczywistego

Reguły bezpieczeństwa bazy danych Firebase Realtime pozwalają kontrolować dostęp do danych przechowywanych w bazie danych. Elastyczna składnia reguł umożliwia tworzenie reguł pasujących do wszystkiego, od wszystkich zapisów do bazy danych po operacje na poszczególnych węzłach.

Reguły zabezpieczeń bazy danych w czasie rzeczywistym są deklaratywną konfiguracją bazy danych. Oznacza to, że reguły definiowane są odrębnie od logiki produktu. Ma to wiele zalet: klienci nie są odpowiedzialni za egzekwowanie bezpieczeństwa, błędne implementacje nie naruszą bezpieczeństwa danych, a co być może najważniejsze, nie ma potrzeby korzystania z pośrednika, takiego jak serwer, w celu ochrony danych przed światem.

W tym temacie opisano podstawową składnię i strukturę Reguły zabezpieczeń bazy danych czasu rzeczywistego używane do tworzenia kompletnych zestawów reguł.

Struktura reguł bezpieczeństwa

Reguły bezpieczeństwa bazy danych w czasie rzeczywistym składają się z wyrażeń przypominających JavaScript zawartych w dokumencie JSON. Struktura reguł powinna być zgodna ze strukturą danych przechowywanych w bazie danych.

Podstawowe reguły identyfikują zbiór węzłów, które mają być zabezpieczone, stosowane metody dostępu (np. odczyt, zapis) oraz warunki , pod którymi dostęp jest dozwolony lub zabroniony. W poniższych przykładach nasze warunki będą prostymi stwierdzeniami true i false , ale w następnym temacie omówimy bardziej dynamiczne sposoby wyrażania warunków.

Na przykład, jeśli próbujemy zabezpieczyć child_node pod węzłem parent_node , ogólna składnia, którą należy zastosować, jest następująca:

{
  "rules": {
    "parent_node": {
      "child_node": {
        ".read": <condition>,
        ".write": <condition>,
        ".validate": <condition>,
      }
    }
  }
}

Zastosujmy ten wzór. Załóżmy na przykład, że śledzisz listę wiadomości i masz dane wyglądające tak:

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

Twoje zasady powinny być skonstruowane w podobny sposób. Oto zestaw reguł zabezpieczeń tylko do odczytu, które mogą mieć sens w przypadku tej struktury danych. Ten przykład ilustruje, jak określamy węzły bazy danych, do których mają zastosowanie reguły, oraz warunki oceny reguł w tych węzłach.

{
  "rules": {
    // For requests to access the 'messages' node...
    "messages": {
      // ...and the individual wildcarded 'message' nodes beneath
      // (we'll cover wildcarding variables more a bit later)....
      "$message": {

        // For each message, allow a read operation if <condition>. In this
        // case, we specify our condition as "true", so read access is always granted.
        ".read": "true",

        // For read-only behavior, we specify that for write operations, our
        // condition is false.
        ".write": "false"
      }
    }
  }
}

Podstawowe zasady operacji

Istnieją trzy typy reguł wymuszania bezpieczeństwa w zależności od rodzaju operacji wykonywanej na danych: .write , .read i .validate . Oto krótkie podsumowanie ich celów:

Typy reguł
.Czytać Opisuje, czy i kiedy dane mogą być odczytywane przez użytkowników.
.pisać Opisuje, czy i kiedy można zapisywać dane.
.uprawomocnić Określa, jak będzie wyglądać poprawnie sformatowana wartość, czy ma atrybuty podrzędne i typ danych.

Zmienne przechwytywania symboli wieloznacznych

Wszystkie instrukcje reguł wskazują na węzły. Instrukcja może wskazywać konkretny węzeł lub używać zmiennych przechwytujących symbole wieloznaczne $ , aby wskazywać zestawy węzłów na poziomie hierarchii. Użyj tych zmiennych przechwytujących do przechowywania wartości kluczy węzłów do wykorzystania w kolejnych instrukcjach reguł. Ta technika umożliwia pisanie bardziej złożonych warunków Reguł, co omówimy bardziej szczegółowo w następnym temacie.

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

Dynamiczne zmienne $ mogą być również używane równolegle ze stałymi nazwami ścieżek. W tym przykładzie używamy zmiennej $other do zadeklarowania reguły .validate , która gwarantuje, że widget nie będzie miał żadnych dzieci poza title i color . Jakikolwiek zapis, który spowodowałby utworzenie dodatkowych dzieci, zakończy się niepowodzeniem.

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}

Odczytuj i zapisuj reguły kaskadowo

Reguły .read i .write działają od góry do dołu, przy czym płytsze reguły zastępują głębsze. Jeśli reguła przyznaje uprawnienia do odczytu lub zapisu w określonej ścieżce, wówczas zapewnia także dostęp do wszystkich znajdujących się pod nią węzłów podrzędnych. Rozważ następującą strukturę:

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          /* ignored, since read was allowed already */
          ".read": false
        }
     }
  }
}

Ta struktura zabezpieczeń umożliwia odczyt /bar/ za każdym razem, gdy /foo/ zawiera baz podrzędną o wartości true . ".read": false w /foo/bar/ nie ma tutaj żadnego efektu, ponieważ ścieżka podrzędna nie może odwołać dostępu.

Chociaż może nie wydawać się to od razu intuicyjne, jest to potężna część języka reguł i pozwala na wdrożenie bardzo złożonych uprawnień dostępu przy minimalnym wysiłku. Zostanie to zilustrowane, gdy zajmiemy się bezpieczeństwem opartym na użytkownikach w dalszej części tego przewodnika.

Należy pamiętać, że reguły .validate nie łączą się kaskadowo. Aby zapis był dozwolony, wszystkie reguły sprawdzania poprawności muszą być spełnione na wszystkich poziomach hierarchii.

Reguły to nie filtry

Reguły są stosowane w sposób atomowy. Oznacza to, że operacja odczytu lub zapisu kończy się natychmiastowym niepowodzeniem, jeśli w tej lokalizacji lub w lokalizacji nadrzędnej nie ma reguły udzielającej dostępu. Nawet jeśli dostępna jest każda ścieżka podrzędna, której dotyczy problem, odczyt w lokalizacji nadrzędnej zakończy się całkowitym niepowodzeniem. Rozważ tę strukturę:

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

Bez zrozumienia, że ​​reguły są oceniane atomowo, może się wydawać, że pobranie ścieżki /records/ zwróci rec1 , ale nie rec2 . Rzeczywisty wynik jest jednak błędem:

JavaScript
var db = firebase.database();
db.ref("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Cel C
Uwaga: ten produkt Firebase nie jest dostępny w docelowym klipie aplikacji.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  // success block is not called
} withCancelBlock:^(NSError * _Nonnull error) {
  // cancel block triggered with PERMISSION_DENIED
}];
Szybki
Uwaga: ten produkt Firebase nie jest dostępny w docelowym klipie aplikacji.
var ref = FIRDatabase.database().reference()
ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
Jawa
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
ODPOCZYNEK
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

Ponieważ operacja odczytu w /records/ jest niepodzielna i nie ma reguły odczytu, która zapewniałaby dostęp do wszystkich danych w /records/ , spowoduje to wyświetlenie błędu PERMISSION_DENIED . Jeśli ocenimy tę regułę w symulatorze bezpieczeństwa w naszej konsoli Firebase , zobaczymy, że operacja odczytu została odrzucona, ponieważ żadna reguła odczytu nie zezwalała na dostęp do ścieżki /records/ . Należy jednak pamiętać, że reguła rec1 nigdy nie została oceniona, ponieważ nie znajdowała się w żądanej ścieżce. Aby pobrać rec1 , musielibyśmy uzyskać do niego bezpośredni dostęp:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Cel C
Uwaga: ten produkt Firebase nie jest dostępny w docelowym klipie aplikacji.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Szybki
Uwaga: ten produkt Firebase nie jest dostępny w docelowym klipie aplikacji.
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
Jawa
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records/rec1");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
ODPOCZYNEK
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Pokrywające się stwierdzenia

Do węzła można zastosować więcej niż jedną regułę. W przypadku, gdy węzeł identyfikuje wiele wyrażeń reguł, metoda dostępu zostaje odrzucona, jeśli którykolwiek z warunków jest false :

{
  "rules": {
    "messages": {
      // A rule expression that applies to all nodes in the 'messages' node
      "$message": {
        ".read": "true",
        ".write": "true"
      },
      // A second rule expression applying specifically to the 'message1` node
      "message1": {
        ".read": "false",
        ".write": "false"
      }
    }
  }
}

W powyższym przykładzie odczyty do węzła message1 zostaną odrzucone, ponieważ druga reguła jest zawsze false , mimo że pierwsza reguła jest zawsze true .

Następne kroki

Możesz pogłębić swoją wiedzę na temat reguł bezpieczeństwa bazy danych Firebase Realtime:

  • Poznaj kolejną ważną koncepcję języka Reguł, warunków dynamicznych, które pozwalają Regułom sprawdzać autoryzację użytkownika, porównywać istniejące i przychodzące dane, weryfikować przychodzące dane, sprawdzać strukturę zapytań przychodzących od klienta i nie tylko.

  • Przejrzyj typowe przypadki użycia zabezpieczeń i definicje reguł zabezpieczeń Firebase, które je dotyczą .