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 elementów, od wszystkich operacji zapisu 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 niezależnie od logiki usługi. Ma to wiele zalet: klienci nie są odpowiedzialni za egzekwowanie zabezpieczeń, wadliwe implementacje nie będą zagrażać danym, a co najważniejsze, nie trzeba będzie stosować pośredniego arbitra, takiego jak serwer, do ochrony danych przed światem zewnętrznym.

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

Struktura reguł zabezpieczeń

Reguły zabezpieczeń Bazy danych czasu rzeczywistego składają się z wyrażeń podobnych do wyrażeń JavaScript zawartych w dokumencie JSON. Struktura reguł powinna odpowiadać strukturze danych przechowywanych w bazie danych.

Podstawowe reguły określają zestaw węzłów, które mają być chronione, metody dostępu (np. odczyt, zapis) oraz warunki, na jakich dostęp ma być udzielany lub odmawiany. W następujących przykładach warunki będą proste truefalse, ale w następnym temacie omówimy bardziej dynamiczne sposoby wyrażania warunków.

Jeśli na przykład chcemy zabezpieczyć child_node w ramach parent_node, ogólna składnia będzie wyglądać tak:

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

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

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

Twoje reguły powinny być sformułowane w podobny sposób. Oto zestaw reguł zabezpieczeń tylko do odczytu, które mogą być przydatne w przypadku tej struktury danych. Ten przykład pokazuje, jak określamy węzły bazy danych, do których mają zastosowanie reguły, oraz warunki ich oceny 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 operacje dotyczące reguł

Istnieją 3 typy reguł dotyczących egzekwowania zabezpieczeń w zależności od typu 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ć poprawnie sformatowana wartość, czy ma atrybuty podrzędne i typ danych.

Zmienne obrazu z symbolem wieloznacznym

Wszystkie instrukcje reguł odwołują się do węzłów. Instrukcja może wskazywać konkretny węzeł lub używać $ symboli wieloznacznych zmiennych przechwytywania, aby wskazywać zbiory węzłów na określonym poziomie hierarchii. Używaj tych zmiennych przechwytywania do przechowywania wartości kluczy węzłów na potrzeby korzystania z nich w kolejnych instrukcjach reguł. Ta technika umożliwia tworzenie bardziej złożonych Rules warunków, o których opowiemy 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 z stałymi nazwami ścieżek. W tym przykładzie używamy zmiennej $other, aby zadeklarować regułę .validate, która zapewnia, że widget nie ma żadnych elementów podrzędnych poza titlecolor. Każdy zapis, który spowoduje 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 }
    }
  }
}

Reguły odczytu i zapisu w kaskadowym trybie odczytu i zapisu

Reguły .read.write działają od góry do dołu, a reguły z poziomu wyższego poziomu zastępują te z poziomu niższego. Jeśli reguła przyznaje uprawnienia do odczytu lub zapisu w konkretnej ścieżce, to także przyznaje dostęp do wszystkich podrzędnych węzłów na tej ścieżce. Rozważ 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 odczyt elementu /bar/, gdy element /foo/ zawiera element podrzędny baz o wartości true. Reguła ".read": false w poziomie /foo/bar/ nie ma tu zastosowania, ponieważ dostęp nie może zostać cofnięty przez ścieżkę podrzędną.

Chociaż może się to wydawać nieintuicyjne, jest to potężna część języka reguł, która pozwala na implementowanie bardzo złożonych uprawnień dostępu przy minimalnym wysiłku. Przykłady tego omówimy w dalszej części tego przewodnika, gdy przejdziemy do sekcji poświęconej bezpieczeństwu opartym na użytkownikach.

Pamiętaj, że .validate nie działają kaskadowo. Aby zapis był dozwolony, wszystkie reguły walidacyjne muszą być spełnione na wszystkich poziomach hierarchii.

Reguły a 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, która przyznaje dostęp. Nawet jeśli wszystkie ścieżki podrzędne są dostępne, odczyt w miejscu nadrzędnym zakończy się całkowitym niepowodzeniem. Rozważ tę strukturę:

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

Jeśli nie rozumiesz, że reguły są oceniane indywidualnie, możesz sądzić, że pobranie ścieżki /records/ zwróci wartość rec1, a 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 celu typu App Clip.
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 celu typu App Clip.
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

Operacja odczytu w przypadku /records/ jest atomowa i nie ma reguły odczytu, która przyznaje dostęp do wszystkich danych w przypadku /records/, więc zostanie zwrócony błąd PERMISSION_DENIED. Jeśli sprawdzimy tę regułę w symulatorze zabezpieczeń w konsoli Firebase, 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 rec1 nigdy nie została oceniona, ponieważ nie znajdowała się na ścieżce, której dotyczyło nasze żądanie. 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 celu typu App Clip.
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 celu typu App Clip.
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!

Oświadczenia nakładające się

Do węzła może być stosowanych więcej niż 1 reguła. Jeśli węzeł jest identyfikowany przez wiele wyrażeń reguł, metoda dostępu jest odrzucana, jeśli co najmniej jeden z tych warunków jest spełniony: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 przykładzie powyżej odczyty z węzła message1 zostaną odrzucone, ponieważ druga reguła zawsze zwraca wartość false, mimo że pierwsza reguła zawsze zwraca wartość true.

Dalsze kroki

Aby dowiedzieć się więcej o regułach zabezpieczeń Bazy danych czasu rzeczywistego Firebase:

  • Poznaj kolejną ważną koncepcję języka Rules, dynamiczne warunki, które umożliwiają Rules sprawdzanie autoryzacji użytkownika, porównywanie istniejących i przychodzących danych, weryfikowanie danych przychodzących, sprawdzanie struktury zapytań otrzymywanych od klienta i nie tylko.

  • Zapoznaj się z typowymi przypadkami użycia reguł zabezpieczeń i definicjami reguł zabezpieczeń Firebase, które je obsługują.