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

Reguły zabezpieczeń Bazy danych czasu rzeczywistego Firebase umożliwiają kontrolowanie dostępu do danych przechowywanych w bazie danych. Elastyczna składnia reguł umożliwia tworzenie reguł pasujących do dowolnych operacji, od wszystkich zapisów w bazie danych po operacje na poszczególnych węzłach.

Reguły zabezpieczeń Bazy danych czasu rzeczywistego to deklaratywna konfiguracja bazy danych. Oznacza to, że reguły są definiowane oddzielnie od logiki produktu. Ma to wiele zalet: klienci nie są odpowiedzialni za egzekwowanie zabezpieczeń, wadliwe implementacje nie zagrażają Twoim danym, a co najważniejsze, nie ma potrzeby korzystania z pośrednika, takiego jak serwer, aby chronić dane przed światem.

W tym artykule opisujemy podstawową składnię i strukturę reguł zabezpieczeń Bazy danych czasu rzeczywistego, które służą do tworzenia pełnych zestawów reguł.

Struktura reguł zabezpieczeń

Reguły zabezpieczeń Bazy danych czasu rzeczywistego składają się z wyrażeń podobnych do JavaScriptu, które znajdują się w dokumencie JSON. Struktura reguł powinna być zgodna ze strukturą danych przechowywanych w bazie danych.

Reguły podstawowe określają zestaw węzłów, które mają być zabezpieczone, metody dostępu (np. odczyt, zapis) oraz warunki, w których dostęp jest dozwolony lub zabroniony. W poniższych przykładach nasze warunki będą prostymi stwierdzeniami truefalse, ale w następnym temacie omówimy bardziej dynamiczne sposoby wyrażania warunków.

Jeśli na przykład próbujemy zabezpieczyć child_node w ramach parent_node, ogólna składnia, której należy użyć, to:

{
  "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 w takiej postaci:

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

Twoje reguły powinny być skonstruowane w podobny sposób. Oto zestaw reguł zabezpieczeń tylko do odczytu, które mogą być odpowiednie dla tej struktury danych. Ten przykład pokazuje, 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"
      }
    }
  }
}

Działania dotyczące reguł podstawowych

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

Typy reguł
.read Określa, czy i kiedy użytkownicy mogą odczytywać dane.
.write Określa, czy i kiedy można zapisywać dane.
.validate Określa, jak będzie wyglądać prawidłowo sformatowana wartość, czy ma atrybuty podrzędne i typ danych.

Zmienne przechwytywania symboli wieloznacznych

Wszystkie instrukcje reguł wskazują węzły. Instrukcja może wskazywać konkretny węzeł lub używać $ symbolu wieloznacznego zmiennych przechwytywania, aby wskazywać zbiory węzłów na poziomie hierarchii. Użyj tych zmiennych przechwytywania, aby przechowywać wartość kluczy węzłów do użycia w kolejnych instrukcjach reguł. Ta technika umożliwia pisanie bardziej złożonych Rules warunków, 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')"
        }
      }
    }
  }
}

Zmiennych dynamicznych $ można też używać równolegle ze stałymi nazwami ścieżek. W tym przykładzie używamy zmiennej $other, aby zadeklarować regułę .validate, która zapewnia, że element widget nie ma innych elementów podrzędnych niż title i color. Każdy zapis, który spowodowałby utworzenie dodatkowych elementów podrzędnych, 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 }
    }
  }
}

Kaskadowe reguły odczytu i zapisu

Reguły .read.write działają od góry do dołu, przy czym reguły na wyższych poziomach zastępują reguły na niższych poziomach. Jeśli reguła przyznaje uprawnienia do odczytu lub zapisu w określonej ścieżce, przyznaje też dostęp do wszystkich węzłów podrzędnych. Możesz zastosować taką 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 odczytywanie elementu /bar/, gdy element /foo/ zawiera element podrzędny baz o wartości true. Reguła ".read": false w sekcji /foo/bar/ nie ma tu żadnego wpływu, ponieważ dostępu nie można cofnąć za pomocą ścieżki podrzędnej.

Choć może się to nie wydawać intuicyjne, jest to ważna część języka reguł, która pozwala z minimalnym wysiłkiem wdrażać bardzo złożone uprawnienia dostępu. Zostanie to zilustrowane w dalszej części tego przewodnika, gdy przejdziemy do kwestii bezpieczeństwa opartego na użytkownikach.

Pamiętaj, że reguły .validate nie są kaskadowe. Aby można było zapisać dane, wszystkie reguły weryfikacji muszą być spełnione na wszystkich poziomach hierarchii.

Reguły nie są filtrami

Reguły są stosowane w sposób niepodzielny. Oznacza to, że operacja odczytu lub zapisu zostanie natychmiast przerwana, jeśli w danej lokalizacji lub w lokalizacji nadrzędnej nie ma reguły, która przyznaje dostęp. Nawet jeśli każda ścieżka podrzędna, której dotyczy problem, jest dostępna, odczyt w lokalizacji nadrzędnej całkowicie się nie powiedzie. Rozważ taką strukturę:

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

Jeśli nie wiesz, że reguły są oceniane niepodzielnie, może Ci się wydawać, że pobranie ścieżki /records/ zwróci rec1, ale nie rec2. Rzeczywisty wynik to jednak błąd:

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
});
Objective-C
Uwaga: ta usługa Firebase nie jest dostępna w przypadku klipów z 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
}];
Swift
Uwaga: ta usługa Firebase nie jest dostępna w przypadku klipów z 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
})
Java
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
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

Ponieważ operacja odczytu w lokalizacji /records/ jest niepodzielna i nie ma reguły odczytu, która przyznaje dostęp do wszystkich danych w lokalizacji /records/, zostanie zgłoszony błąd PERMISSION_DENIED. Jeśli ocenimy tę regułę w symulatorze zabezpieczeń w naszej Firebasekonsoli, zobaczymy, że operacja odczytu została odrzucona, ponieważ żadna reguła odczytu nie zezwalała na dostęp do ścieżki /records/. Pamiętaj jednak, że reguła dla rec1 nie została nigdy oceniona, ponieważ nie znajdowała się na ścieżce, o którą prosiliśmy. Aby pobrać rec1, musimy 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
});
Objective-C
Uwaga: ta usługa Firebase nie jest dostępna w przypadku klipów z aplikacji.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
Uwaga: ta usługa Firebase nie jest dostępna w przypadku klipów z aplikacji.
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
Java
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
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Nakładające się instrukcje

Do węzła może być zastosowana więcej niż 1 reguła. Jeśli wiele wyrażeń reguł identyfikuje węzeł, metoda dostępu jest odrzucana, jeśli którykolwiek z warunków ma wartość 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 z węzła message1 zostaną odrzucone, ponieważ druga reguła zawsze ma wartość false, mimo że pierwsza reguła zawsze ma wartość true.

Dalsze kroki

Możesz lepiej poznać reguły zabezpieczeń Bazy danych czasu rzeczywistego Firebase:

  • Poznaj kolejne ważne pojęcie języka Rules, czyli dynamiczne warunki, które umożliwiają sprawdzanie autoryzacji użytkownika przez Rules, porównywanie istniejących i przychodzących danych, weryfikowanie przychodzących danych, sprawdzanie struktury zapytań pochodzących od klienta i nie tylko.

  • Zapoznaj się z typowymi przypadkami użycia dotyczącymi bezpieczeństwa i odpowiadającymi im definicjami reguł zabezpieczeń Firebase.