Role Based Access Control in Flutter
Tags: flutter, developer, tutorial, mobile
Published: 2023-12-17 12:54:53

When I started #30DaysOfFlutter on my [Twitter](https://twitter.com/Sparsh_M_) (sorry for the shameless plugin xD), I never thought I would go more than 4 days. But it's Day 8 today and I feel the progress is really good. 

While I regularly share my Flutter learnings on Twitter, today's topic is too good not to have its own dedicated post. So I am actually building a side project on Flutter that has different user roles - Admin, Manager and User. These roles demand distinct UIs, functionalities, and data access! So I decided not to drown in if-else statements within every widget and instead craft a robust system to seamlessly control access. And this is what we are gonna learn today - How to implement Role Based Access Control (RBAC) in Flutter.

## Starting Point


We're starting simple! To explore role management, we'll build a basic app with just two files, `app.dart` and `main.dart`. Think of it as a blank canvas. Forget fancy UIs for now, we'll add complexity as we go. This way, we can focus on the core concepts of access control, step-by-step. 

`app.dart`
```
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RBAC Demo',
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('User model app'),
      ),
      body: Container(),
    );
  }
}
```

`main.dart`

```
import 'package:flutter/material.dart';
import 'package:userrole/app.dart';

void main() {
  runApp(const MyApp());
}
```

Now let’s introduce some functionality. First, we begin by introducing a simple button that basically creates a snack bar with a message:

```
import 'package:flutter/material.dart';

class MessageButton extends StatefulWidget {
  const MessageButton({Key? key}) : super(key: key);

  @override
  State<MessageButton> createState() => _MessageButtonState();
}

class _MessageButtonState extends State<MessageButton> {
  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: _showMessage,
      child: const Text('Show me'),
    );
  }

  _showMessage() {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('I am role based'),
      ),
    );
  }
}
```
> The best practice when handling roles is not managing them from UI, but every feature call should be checked for roles and permission on the server side. UI role management is great for user experience.

To mimic real-world authentication, imagine we get user data (including roles) after a successful login. While we could use secure JWT tokens for this, let's simplify things and define the plain JSON format we'll use for storing this data in local storage.

```
{
  "username": "Sparsh",
  "email": "sparsh@gmail.com",
  "role": {
    "name": "admin",
    "level": 3
  }
}
```

We need to create a proper user data container in `model.dart`.

```
import 'package:flutter/foundation.dart';

@immutable
class UserData {
  final String username;
  final String email;
  final UserRole role;

  const UserData({
    required this.username,
    required this.email,
    required this.role,
  });

  factory UserData.fromJson(Map<String, dynamic> json) {
    return UserData(
      username: json['username']!,
      email: json['email'],
      role: UserRole.fromJson(json['role']),
    );
  }
}

@immutable
class UserRole {
  final String name;
  final int level;

  const UserRole({
    required this.name,
    required this.level,
  });

  factory UserRole.fromJson(Map<String, dynamic> json) {
    return UserRole(
      name: json['name'],
      level: json['level'] as int,
    );
  }
}
```
To make accessing user data a breeze, we'll have a dedicated file where it lives, readily available from anywhere in the app.

`core.dart`
```
import 'dart:convert' show jsonDecode;
import 'package:userrole/mdoel.dart';
import 'package:flutter/services.dart' show rootBundle;

class Core {
  static UserData? _user;

  UserData? get user => _user;

  Future<void> setUserData() async {
    // loads user data from shared preferences and sets it to
    // [user]
  }
}
```
`main.dart` will be changed into the following:

```
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  Core core = Core();
  await core.setUserData();
  runApp(const MyApp());
}
```
To avoid hardcoded role checks in every widget, we'll define a stateful `WidgetWithRole` class in `role_handlers.dart`. This class interacts with the Core class for data and centralizes role verification, streamlining our code.

```
import 'package:flutter/material.dart';
import 'package:userrole/core.dart';

class WidgetWithRole extends StatefulWidget {
  const WidgetWithRole({Key? key, required this.child}) : super(key: key);

  final Widget child;

  @override
  State<WidgetWithRole> createState() => _WidgetWithRoleState();
}

class _WidgetWithRoleState extends State<WidgetWithRole> {
  late Core core;

  @override
  void initState() {
    core = Core();

    super.initState();
  }

  bool get isAdmin => core.user?.role.name == "admin";

  @override
  Widget build(BuildContext context) {
    if (isAdmin) {
      return widget.child;
    }

    return Container();
  }
}
```
Now, `app.dart` can leverage this functionality by wrapping MessageButton with `WidgetWithRole`, seamlessly implementing access control.

```
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('User model app'),
    ),
    body: const Center(
      child: WidgetWithRole(
        child: MessageButton(),
      ),
    ),
  );
}
```
We've got a fantastic role-checking wrapper, but the "admin" hardcode feels limiting. Let's make it even better with an `allowedRole` parameter! Pass in any role as a string, but wouldn't enums be cleaner? We'll dedicate an `enums.dart` file for them, convert `UserRole` to an enum, and update `WidgetWithRole` calls accordingly.

`model.dart`
```
import 'package:flutter/foundation.dart';
import 'package:userrole/enums.dart';

@immutable
class UserData {
  final String username;
  final String email;
  final UserRole role;

  const UserData({
    required this.username,
    required this.email,
    required this.role,
  });

  factory UserData.fromJson(Map<String, dynamic> json) {
    return UserData(
      username: json['username']!,
      email: json['email'],
      role: UserRole.admin,
    );
  }
}
```
`enums.dart`

```
enum UserRole {
  admin('admin', 3);

  const UserRole(this.name, this.level);

  final String name;
  final int level;

  @override
  String toString() => name;
}
```
Now in `MyHomePage` widget we will pass `allowedRole` argument to `WidgetWIthRole`.

`app.dart`

```
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('User model app'),
    ),
    body: const Center(
      child: WidgetWithRole(
        allowedRole: UserRole.admin,
        child: MessageButton(),
      ),
    ),
  );
}
```
Now we will modify the check in `role_handlers.dart` file:

`role_handlers.dart`

```
class WidgetWithRole extends StatefulWidget {
  const WidgetWithRole({
    Key? key,
    required this.child,
    required this.allowedRole,
  }) : super(key: key);

  final Widget child;
  final UserRole allowedRole;

  @override
  State<WidgetWithRole> createState() => _WidgetWithRoleState();
}

class _WidgetWithRoleState extends State<WidgetWithRole> {
  late Core core;

  @override
  void initState() {
    core = Core();

    super.initState();
  }

  bool get isAllowed => core.user?.role == widget.allowedRole;

  @override
  Widget build(BuildContext context) {
    if (isAllowed) {
      return widget.child;
    }

    return Container();
  }
}
```
Now, we have refactored and refined the version of our role handler, and I think it is time to introduce multiple roles and everything that comes to it.

First of all, let’s update `model.dart` and `enum.dart` to support multiple roles and their deserialization,

`enum.dart`

```
enum UserRole {
  admin('admin', 3),
  manager('manager', 2),
  user('user', 1);

  const UserRole(this.name, this.level);

  final String name;
  final int level;

  @override
  String toString() => name;

  factory UserRole.fromJson(String? role) {
    switch (role) {
      case "admin":
        return UserRole.admin;
      case "manager":
        return UserRole.manager;
      default:
        return UserRole.user;
    }
  }
}
```

`role_handlers.dart`

```
final List<UserRole> allowedRoles;

// ...

bool get isAllowed => widget.allowedRoles.contains(core.user?.role);
```
Now in `app.dart`, we will modify the code to give access to multiple roles : 

```
child: WidgetWithRole(
  allowedRoles: [
    UserRole.admin,
    UserRole.manager,
    UserRole.user,
  ],
  child: MessageButton(),
),
```

## Conclusion

This way you can introduce RBAC in your flutter app. There are some more approaches too - like adding a hierarchy in roles and only specifying lowest role so that all the roles above it will have the access to the widget. You can use your own approach whichever is best suited for your needs and complexity.