Cannot make custom metadata in a Firebase Cloud Storage security rule work as expected

Viewed 28

Issue is effectively identical to this issue which was left unresolved and I'm new to SO so couldn't comment to ask if a resolution was ever found (apologies for posting a redundant question). My specific case:

    match /profilePhotos/{pathAsUserUid}/{photoFileName} {
      allow create: if request.resource.metadata.userUid == pathAsUserUid;
    }

returns false when I believe it should return true. The metadata has been written correctly to Storage and appears in https://console.cloud.google.com/storage as expected e.g., (key 2) userUid has a value of LjCZ3ONBTqPEIhNTXt2pxMDWkvJ2.

custom metadata as it appears in the console

Forcing a result via specific test cases clearly shows request.resource.metadata.userUid simply isn't working as expected. For example, (for testing only)

      allow create: if 'LjCZ3ONBTqPEIhNTXt2pxMDWkvJ2' == pathAsUserUid;

works as expected (returns true) however,

      allow create: if request.resource.metadata.userUid == 'LjCZ3ONBTqPEIhNTXt2pxMDWkvJ2';

doesn't work as expected (returns false).

ADDITION 1

Screen snapshot of console including path to image

ADDITION 2 Here's the code for uploading the profile photo. PUser is a straightforward user-data class with a uid set (not shown below but thoroughly tested) from the FirebaseAuth.instance.currentUser.uid.

  void onSubmitted(String? value) async {
    bool success = true;

    // pUSer contains everything *except* the profile photo (held by pUser.profilePhotoXFile)
    // upload the photo to cloud storage then get the photo's url
    XFile profilePhotoXFile = widget.pUser.profilePhotoXFile as XFile;
    final profilePhotoMetadata = SettableMetadata(
      contentType: profilePhotoXFile.mimeType,
      customMetadata: {
        'user': widget.pUser.fullName!,
        'userUid': widget.pUser.uid! // <-- @Dharmaraj this is where the userUid metadata is set
      },
    );

    final storageRef = FirebaseStorage.instance.ref();
    final profilePhotoRef = storageRef
        .child('profilePhotos/${widget.pUser.uid}/${profilePhotoXFile.name}');

    setState(() {
      screenMode = ScreenMode.wait;
      screenMessage = 'Uploading profile photo...';
    });

    try {
      if (kIsWeb) {
        Uint8List profilePhotoBytes = await profilePhotoXFile.readAsBytes();
        TaskSnapshot upload = await profilePhotoRef.putData(
          profilePhotoBytes,
          profilePhotoMetadata,
        );

        widget.pUser.profilePhotoURL = await upload.ref.getDownloadURL();
      } else {
        throw const PlainliException('Only web is (currently) supported for image upload.');
      }
    } catch (e) {
      success = false;
      plainliBanner(
          context: context,
          message: 'Error uploading profile photo. error = $e');
    }

    if (!success) {
      setState(() {
        screenMode = ScreenMode.message;
        screenMessage = 'There was an error uploading your photo.';
      });
      return;
    }

    // *merge* data{} into Firestore
    setState(() {
      screenMode = ScreenMode.wait;
      screenMessage = 'Registering...';
    });

    Map<String, dynamic> data = widget.pUser.toMap();
    CollectionReference<Map<String, dynamic>> collectionRef =
        FirebaseFirestore.instance.collection('users');

    try {
      await collectionRef
          .doc(widget.pUser.uid)
          .set(data, SetOptions(merge: true));
    } catch (e) {
      success = false;
      plainliBanner(
          context: context,
          message: 'Error merging data into Firestore. Error: $e');
    }

    if (!success) {
      setState(() {
        screenMode = ScreenMode.message;
        screenMessage =
            'There was an error uploading your profile information.';
      });
      return;
    }

    if (!mounted) return;
    plainliSnackbar(context: context, message: 'Registration complete');

    Navigator.of(context).popUntil((route) => route.isFirst);

    // this allows the wrapper to get notified that all the user data has
    // been successfully collected
    Provider.of<PUserProvider>(context, listen: false)
        .allRequiredUserDataExists = true;
  }

And here are the Cloud Storage rules.

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
  
    match /{allPaths=**} {
      allow read, write: if false;
    }
    
    match /profilePhotos/{pathAsUserUid}/{photo} {
      allow read, write: if isAdmin();
      allow get, create, update: if profilePhotoBelongsToUser(pathAsUserUid);
    }
    
    function isSignedin() {
      return request.auth != null;
    }   
    
    function isAdmin() {
      let isUserSignedin = isSignedin();
      let isUserRoleAdmin = request.auth.token.isAdmin;
      return isUserSignedin && isUserRoleAdmin;
    }

    function profilePhotoBelongsToUser(pathAsUserUid) {
      let isUserSignedin = isSignedin();
      //let metadataUidMatchesPath = request.resource.metadata.userUid == pathAsUserUid;
      let metadataUidMatchesPath = true; // <-- @Dharmaraj this is a temp get-around until we can sort out why the line above fails
      let authUidMatchesPath = request.auth.uid == pathAsUserUid;
      return isUserSignedin && metadataUidMatchesPath && authUidMatchesPath;
    }   
    
  }
}

0 Answers
Related