DEV Community

Cover image for Role Based Access Control in Flutter
Sparsh Malhotra
Sparsh Malhotra

Posted on

Role Based Access Control in Flutter

When I started #30DaysOfFlutter on my Twitter (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(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

main.dart

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

void main() {
  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

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'),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
  }
}
Enter fullscreen mode Exit fullscreen mode

main.dart will be changed into the following:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  Core core = Core();
  await core.setUserData();
  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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(),
      ),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

enums.dart

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

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

  final String name;
  final int level;

  @override
  String toString() => name;
}
Enter fullscreen mode Exit fullscreen mode

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(),
      ),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

role_handlers.dart

final List<UserRole> allowedRoles;

// ...

bool get isAllowed => widget.allowedRoles.contains(core.user?.role);
Enter fullscreen mode Exit fullscreen mode

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(),
),
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)