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ł, które dopasowują wszystko – od wszystkich zapisów do bazy 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ć Twoim danym, a co najważniejsze, nie trzeba będzie używać 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 mieć strukturę 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ą wyrażone za pomocą prostych instrukcji 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 taka:

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

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

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

Struktura reguł powinna wyglądać podobnie. Oto zestaw reguł dotyczących zabezpieczeń tylko do odczytu, które mogą sprawdzić się 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 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 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ć prawidłowo sformatowana wartość, czy ma atrybuty podrzędne, oraz 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ć zmiennych przechwytywania symbolu wieloznacznego $, aby wskazywać zbiory węzłów na 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 więcej 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 nazwami ścieżek stałych. 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 spowodowałby utworzenie dodatkowych elementów podrzędnych, koń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 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 związana z zasadą /foo/bar/ nie ma tutaj żadnego wpływu, ponieważ ścieżka podrzędna nie może anulować dostępu.

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. Wyjaśnimy to, gdy w dalszej części tego przewodnika zajmiemy się zabezpieczeniami opartymi 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 nie są filtrami

Reguły są stosowane w sposób atomowy. Oznacza to, że operacja odczytu lub zapisu kończy się niepowodzeniem natychmiast, jeśli w tej lokalizacji lub w lokalizacji nadrzędnej nie ma reguły przyznającej dostęp. Nawet jeśli wszystkie ścieżki podrzędne są dostępne, odczyt w miejscu nadrzędnym zakończy się całkowitym niepowodzeniem. Weźmy pod uwagę 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 /records/ jest operacją atomową i nie ma reguły odczytu, która zezwalałaby na dostęp do wszystkich danych w /records/, więc zostanie zwrócony błąd PERMISSION_DENIED. Jeśli ocenimy tę regułę w symulatorze zabezpieczeń w konsoli Firebase, zobaczymy, że operacja odczytu została odrzucona, ponieważ żadna reguła odczytu nie zezwala 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ć plik 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. W przypadku, gdy 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 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.

  • Przejrzyj typowe przypadki użycia zabezpieczeń i odpowiadające im definicje reguł zabezpieczeń Firebase.