개요
회사에서 Redash를 Visualization Tool로 사용하고 있는데 특정 대시보드에 대해서 회사 외부에 공개할 일이 생겨 공개를 결정한 대시보드를 제외한 대시보드는 외부에는 노출되지 않도록 하고 싶다는 needs가 있었다. 하지만 Redash는 Web UI에서 권한 관리나 설정 등을 수행하는데 해당 UI를 통해서는 권한 관리에 취약점이 많았고 한계가 많아서 아예 Redash 서버 자체를 분리하여 둘의 서버를 sync 하는 방식으로 진행하자는 의견이 나왔었다. 그래서 나는 해당 Job을 받았고 그대로 수행하려고 했으나 Redash에 대해서 조사한 결과 서버를 분리하지 않아도 가능할 것 같다는 생각이 들었고 그 방안에 대해서 고민한 과정과 결과를 포스팅하려고 한다.
해당 포스팅은 Redash에 대해 이해하고 있다는 가정하에 진행한다.
Redash 요소
Redash를 구성하는 요소들이 있는데 해당 요소들에 대해서 먼저 설명하겠다.
Data Source
대상 데이터 소스를 정의하는 부분으로 쿼리를 실행할 대상 데이터 소스(ex. MySQL, BigQuery, Elasticsearch 등)에 대한 연결 정보를 정의한다. 같은 데이터 소스를 바라볼 지라도 다르게 정의(ex. BigQuery에 대하여 bigquery-1, bigquery-2라고 지정)하여 선언할 수 있다.
Query
위의 Data Source를 지정하여 실행할 Query 문을 정의하는 부분이다.
Widget
대시보드를 구성하는 부분적인 요소들을 의미한다. Query를 통해 Widget을 정의할 수도 있고 그 외에도 Markdown으로 Textbox를 만들 수도 있다.
Dashboard
여러 개의 Widget들이 모여서 이루어진다.
Redash 권한 관리
Redash는 Group과 User로 이루어져 있는데 특정 Group에 대해서 권한 관리가 이루어지고 해당 Group에 User들을 추가하면 해당 User들이 Group의 권한 가지게 되는 방식이다.
Redash 권한 구성 요소
Redash는 Group별로 권한을 제어하는데 Data Source 별로 권한을 제어합니다. 해당 Group에 특정 Data Source 들을 추가할 수 있는데 각각의 Data Source는 Full Access와 View Only 두 가지의 권한 중 하나를 가지게 됩니다.
그리고 해당 Group에 속한 User들은 추가된 Data Source들에 대해서만 접근할 수 있는 권한을 가지게 됩니다. 그렇기 때문에 User 별로 대시보드의 구성 요소들에 대해서 접근을 제어하려면 User가 속한 Group에서 접근할 수 있는 Data Source들에 대해서 관리가 필요합니다.
글로 설명하면 이해하기 어려울 수 있으니 아래 예시를 살펴보자.
- 위의 예시는 한 Group에 User가 속해 있고 해당 User가 대시보드를 보고 있는 상황이다.
- 해당 대시보드는 3개의 Widget과 Textbox로 이루어져 있고 현재 모든 Widget을 볼 수 있는 상황이다.
- 그림과 같이 각 Widget은 각 Query에 연결되어 있는 모양이고 각 Query는 각 Data Source와 연결되어 있는 상황이다.
그렇다면 여기서 Query 3과 연결된 Widget을 Dashboard에서 보여지는 것을 제한하고 싶다면 어떻게 해야 할까?
위에서 Group의 권한을 살펴보면 Data Source 1과 Data Source 2-public에 대해서 권한을 가지고 있는 것을 볼 수 있다. 그렇다면 Query 3의 데이터 소스를 Data Source 2-private으로 바꾸면 해당 Data Source에 대해 권한이 없는 Group의 User는 볼 수 없을 것이다. 결과는 아래 그림과 같다.
그렇다면 내부 User와 외부 User에 대해서 하나의 대시보드를 같이 바라보지만 특정 Widget에 대해 제한할 수 있는 대시보드 구성이 가능하다.
위의 그림에서는 External 유저는 Widget 3은 볼 수가 없다.
Textbox는 연결된 Query가 없기 때문에 권한 상관없이 보여지고 만약 모든 Query가 있는 Widget에 대해 권한이 없으면 해당 대시보드 자체를 볼 수가 없다.
위의 방식을 통해 하나의 대시보드에 대해서 Widget 별로 권한이 관리가 가능한 것이 증명되었다. 하지만 한 가지 문제가 또 있다. 아래 그림을 보자.
위의 그림은 External 유저의 입장에서 바라봤을 때 Redash의 화면이다. 대시보드에 대해서 권한을 제어할 수 있지만 다른 요소들에 대해서는 제한이 불가능하다.
- Redash에 속한 User
- 해당 User들이 속한 Group
- Query Snippets
나는 해당 요소들에 대해서도 제한을 하고 싶었는데 Redash의 Web UI에서 해당 권한을 제어하는 부분이 존재하지 않았고 그래서 방법을 찾아보다가 해당 문서도 없어 직접 Redash 오픈소스를 살펴보았다. 그러다가 Redash의 동작 과정을 알게 되었다.
Redash permission
Redash는 Frontend(react) + Backend(Flask)로 이루어진 Web Application 이다. Web UI에서 동작이 이루어질 때, Frontend가 Dynamic Interact에 대해서 Backend로 API 호출을 통해 데이터를 fetch하는 방식으로 동작한다.
여기서 권한 검사를 하는 Logic을 살펴 보았는데 특정 요소에 대해서 Backend에 검사를 하는 API를 요청할 때, 각 User들의 특정 ID값과 함께 Backend로 요청을 하게 되고 Backend에서 User들을 식별하는 ID값을 토대로 API의 로직을 실행하기 이전에 DB에 해당 User가 속한 Group의 권한(permissions)를 검사한다.
아래는 ORM Model로 정의된 Group에 대한 코드이다.
class Group(db.Model, BelongsToOrgMixin):
DEFAULT_PERMISSIONS = [
"create_dashboard",
"create_query",
"edit_dashboard",
"edit_query",
"view_query",
"view_source",
"execute_query",
"list_users",
"schedule_query",
"list_dashboards",
"list_alerts",
"list_data_sources",
]
ADMIN_PERMISSIONS = ["admin", "super_admin"]
BUILTIN_GROUP = "builtin"
REGULAR_GROUP = "regular"
id = primary_key("Group")
data_sources = db.relationship("DataSourceGroup", back_populates="group", cascade="all")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship("Organization", back_populates="groups")
type = Column(db.String(255), default=REGULAR_GROUP)
name = Column(db.String(100))
permissions = Column(postgresql.ARRAY(db.String(255)), default=DEFAULT_PERMISSIONS)
created_at = Column(db.DateTime(True), default=db.func.now())
__tablename__ = "groups"
그리고 Group을 생성하는 API 부분이다.
class GroupListResource(BaseResource):
@require_admin
def post(self):
name = request.json["name"]
group = models.Group(name=name, org=self.current_org)
models.db.session.add(group)
models.db.session.commit()
self.record_event({"action": "create", "object_id": group.id, "object_type": "group"})
return group.to_dict()
실제로 Redash에서 UI로 Group을 생성할 때, Group의 이름을 받게 되어 있는데 API에서는 이름만 값으로 받아와서 DB에 Group을 생성하게 된다. 여기서 permissions라는 Column이 존재하지만 DEFAULT_PERMISSIONS라는 값을 미리 정의하여 기본적으로 Group을 생성할 때, 해당하는 값들이 들어가게 된다.
@require_permission("list_users")
def get(self, group_id):
if not (self.current_user.has_permission("admin") or int(group_id) in self.current_user.group_ids):
abort(403)
members = models.Group.members(group_id)
return [m.to_dict() for m in members]
실제로 API 호출을 할 때, 각 요청 별로 require_permission({permission}) 라는 데코레이터가 달려 있는데, 이는 위에서 말한 DB의 permissions column에 해당하는 값들이 있는 지 검사한다는 의미이다.
그래서 해당 권한들만 골라서 부여하면 되겠다고 생각했지만 알아본 결과 특정 권한만 부여하고 싶어도 Redash 에서는 해당 기능을 지원하지 않는다.
그래서 생각한 방안은 직접 DB에서 Group에 UPDATE를 하는 방법으로 "list_dashboards"와 "execute_query"만 권한으로 부여하게 되면 우리가 원하는 대시보드만 볼 수 있는 Redash를 완성할 수 있다.