| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- import dayjs from 'dayjs'
- import { IPv4CidrRange, IPv6CidrRange } from 'ip-num'
- import type {
- JitExpiryMode,
- JitIpRangeDraft,
- JitMemberOption,
- JitRoleGrantDraft,
- JitRoleOption,
- JitStatus,
- JitStatusBadge,
- JitUserRule,
- JitUserRuleDraft,
- } from './JitDbAccess.types'
- import { type DatabaseRolesData, type PgRole } from '@/data/database-roles/database-roles-query'
- import type { JitDbAccessMembersData } from '@/data/jit-db-access/jit-db-access-members-query'
- import type { OrganizationMembersData } from '@/data/organizations/organization-members-query'
- import type { ProjectMembersData } from '@/data/projects/project-members-query'
- export function getRelativeDatetimeByMode(mode: JitExpiryMode) {
- if (mode === '1h') return dayjs().add(1, 'hour').toISOString()
- if (mode === '1d') return dayjs().add(1, 'day').toISOString()
- if (mode === '7d') return dayjs().add(7, 'day').toISOString()
- if (mode === '30d') return dayjs().add(30, 'day').toISOString()
- return ''
- }
- function inferExpiryMode(grant: Pick<JitRoleGrantDraft, 'hasExpiry'>): JitExpiryMode {
- if (!grant.hasExpiry) return 'never'
- return 'custom'
- }
- export function createEmptyGrant(roleId: string): JitRoleGrantDraft {
- return {
- roleId,
- enabled: false,
- branchesOnly: false,
- expiryMode: '1h',
- hasExpiry: true,
- expiry: getRelativeDatetimeByMode('1h'),
- ipRanges: [createEmptyIpRange()],
- }
- }
- export function createEmptyIpRange(): JitIpRangeDraft {
- return { value: '' }
- }
- function parseIpRangeRows(value: JitIpRangeDraft[]) {
- return value.map((item) => item.value.trim()).filter((item) => item.length > 0)
- }
- function cloneIpRanges(ipRanges: JitIpRangeDraft[]) {
- return ipRanges.map((ipRange) => ({ ...ipRange }))
- }
- function cloneGrants(grants: JitRoleGrantDraft[]) {
- return grants.map((grant) => ({ ...grant, ipRanges: cloneIpRanges(grant.ipRanges) }))
- }
- export function createDraft(roleIds: string[]): JitUserRuleDraft {
- return { memberId: '', grants: roleIds.map((roleId) => createEmptyGrant(roleId)) }
- }
- function mergeRoleIds(baseRoleIds: string[], extraRoleIds: string[]) {
- const seen = new Set<string>()
- const merged: string[] = []
- for (const roleId of [...baseRoleIds, ...extraRoleIds]) {
- if (seen.has(roleId)) continue
- seen.add(roleId)
- merged.push(roleId)
- }
- return merged
- }
- export function draftFromRule(rule: JitUserRule, baseRoleIds: string[]): JitUserRuleDraft {
- const byRoleId = new Map(rule.grants.map((grant) => [grant.roleId, grant]))
- const mergedRoleIds = mergeRoleIds(
- baseRoleIds,
- rule.grants.map((grant) => grant.roleId)
- )
- return {
- memberId: rule.memberId,
- grants: mergedRoleIds.map((roleId) => {
- const nextGrant = {
- ...createEmptyGrant(roleId),
- ...(byRoleId.get(roleId) ?? {}),
- }
- return {
- ...nextGrant,
- expiryMode: inferExpiryMode(nextGrant),
- ipRanges: cloneIpRanges(nextGrant.ipRanges),
- }
- }),
- }
- }
- export function computeStatusFromGrants(grants: JitRoleGrantDraft[]): JitStatus {
- const enabledGrants = grants.filter((grant) => grant.enabled)
- let active = 0
- let expired = 0
- let activeIp = 0
- let expiredIp = 0
- enabledGrants.forEach((grant) => {
- const hasIp = parseIpRangeRows(grant.ipRanges).length > 0
- if (!grant.hasExpiry || !grant.expiry) {
- active += 1
- if (hasIp) activeIp += 1
- return
- }
- const isExpired = dayjs(grant.expiry).isValid() && dayjs(grant.expiry).isBefore(dayjs())
- if (isExpired) {
- expired += 1
- if (hasIp) expiredIp += 1
- return
- }
- active += 1
- if (hasIp) activeIp += 1
- })
- return { active, expired, activeIp, expiredIp }
- }
- function formatBadgeLabel(raw: string, showCount: boolean): string {
- if (showCount) return raw
- // If only one in count:
- // Strip leading "N " and "· N " count segments, then capitalize first letter
- return raw
- .replace(/^\d+\s/, '')
- .replace(/·\s*\d+\s/g, '· ')
- .replace(/^./, (c) => c.toUpperCase())
- }
- export function getJitStatusDisplay(status: JitStatus): { badges: JitStatusBadge[] } {
- const { active, expired, activeIp } = status
- const badges: JitStatusBadge[] = []
- const showCount = (active > 0 ? 1 : 0) + (expired > 0 ? 1 : 0) > 1
- if (active > 0) {
- const raw = activeIp > 0 ? `${active} active · ${activeIp} IP` : `${active} active`
- badges.push({ label: formatBadgeLabel(raw, showCount), variant: 'success' })
- }
- if (expired > 0) {
- const raw = `${expired} expired`
- badges.push({ label: formatBadgeLabel(raw, showCount), variant: 'default' })
- }
- return { badges }
- }
- function toUnixSeconds(datetimeIso: string) {
- const value = dayjs(datetimeIso)
- if (!value.isValid()) return undefined
- return value.unix()
- }
- function isValidCidr(value: string) {
- try {
- if (value.includes(':')) {
- IPv6CidrRange.fromCidr(value)
- return true
- }
- IPv4CidrRange.fromCidr(value)
- return true
- } catch {
- return false
- }
- }
- export function getInvalidIpRangeRows(value: JitIpRangeDraft[]) {
- return parseIpRangeRows(value).filter((cidr) => !isValidCidr(cidr))
- }
- function isAssignableJitRole(role: PgRole) {
- return (
- role.canLogin &&
- !role.isSuperuser &&
- !role.name.startsWith('pg_') &&
- (!role.name.startsWith('briven_') || role.name === 'briven_read_only_user') &&
- !['pgbouncer', 'authenticator'].includes(role.name)
- )
- }
- function serializeAllowedNetworks(roleObj: {
- allowed_networks?: {
- allowed_cidrs?: Array<{ cidr: string }>
- allowed_cidrs_v6?: Array<{ cidr: string }>
- }
- }) {
- const cidrs = roleObj.allowed_networks?.allowed_cidrs?.map((item) => item.cidr) ?? []
- const cidrsV6 = roleObj.allowed_networks?.allowed_cidrs_v6?.map((item) => item.cidr) ?? []
- return [...cidrs, ...cidrsV6]
- }
- export function getAssignableJitRoleOptions(
- databaseRoles?: DatabaseRolesData | null
- ): JitRoleOption[] {
- return (
- databaseRoles
- ?.filter(isAssignableJitRole)
- .map((role) => ({ id: role.name, label: role.name }))
- .sort((a, b) => a.label.localeCompare(b.label)) ?? []
- )
- }
- export function getJitMemberOptions(
- organizationMembers?: OrganizationMembersData | null,
- projectMembers?: ProjectMembersData | null
- ): JitMemberOption[] {
- const byId = new Map<string, JitMemberOption>()
- for (const member of organizationMembers ?? []) {
- // JIT rules should only target accepted org members (invites can be expired/pending).
- if (!member.gotrue_id) continue
- const id = member.gotrue_id ?? member.primary_email
- if (!id) continue
- byId.set(id, {
- id,
- email: member.primary_email ?? id,
- name: member.username || undefined,
- })
- }
- for (const member of projectMembers ?? []) {
- const id = member.user_id ?? member.primary_email
- if (!id) continue
- byId.set(id, {
- id,
- email: member.primary_email ?? byId.get(id)?.email ?? id,
- name: member.username ?? byId.get(id)?.name,
- })
- }
- return Array.from(byId.values()).sort((a, b) => a.email.localeCompare(b.email))
- }
- export function mapJitMembersToUserRules(
- jitMembers: JitDbAccessMembersData | undefined,
- projectMembers: ProjectMembersData | undefined,
- roleOptions: JitRoleOption[]
- ): JitUserRule[] {
- const memberMap = new Map((projectMembers ?? []).map((member) => [member.user_id, member]))
- const baseRoleIds = roleOptions.map((role) => role.id)
- return (jitMembers ?? []).map((item) => {
- const mappedMember = memberMap.get(item.user_id)
- const assignedRoles: JitRoleGrantDraft[] = (item.user_roles ?? []).map((roleObj) => {
- const roleWithBranchRestriction = roleObj as typeof roleObj & { branches_only?: boolean }
- const expiresAt = typeof roleObj.expires_at === 'number' ? roleObj.expires_at : undefined
- const hasExpiry = typeof expiresAt === 'number'
- const allowedNetworks = serializeAllowedNetworks(roleObj)
- return {
- ...createEmptyGrant(roleObj.role),
- roleId: roleObj.role,
- enabled: true,
- branchesOnly: roleWithBranchRestriction.branches_only ?? false,
- hasExpiry,
- expiryMode: hasExpiry ? 'custom' : 'never',
- expiry: hasExpiry ? new Date(expiresAt * 1000).toISOString() : '',
- ipRanges:
- allowedNetworks.length > 0
- ? allowedNetworks.map((cidr) => ({ value: cidr }))
- : [createEmptyIpRange()],
- }
- })
- const assignedByRoleId = new Map(assignedRoles.map((grant) => [grant.roleId, grant]))
- const allRoleIds = mergeRoleIds(
- baseRoleIds,
- assignedRoles.map((grant) => grant.roleId)
- )
- const grants = allRoleIds.map((roleId) => ({
- ...createEmptyGrant(roleId),
- ...(assignedByRoleId.get(roleId) ?? {}),
- roleId,
- }))
- const email = mappedMember?.primary_email ?? item.user_id
- const name = mappedMember?.username ?? undefined
- return {
- id: item.user_id,
- memberId: item.user_id,
- email,
- name,
- grants: cloneGrants(grants),
- status: computeStatusFromGrants(grants),
- }
- })
- }
- export function serializeDraftRolesForGrantMutation(draft: JitUserRuleDraft) {
- const serializeAllowedNetworks = (value: JitIpRangeDraft[]) => {
- const cidrs = parseIpRangeRows(value)
- if (cidrs.length === 0) return undefined
- const allowed_cidrs = cidrs.filter((cidr) => !cidr.includes(':')).map((cidr) => ({ cidr }))
- const allowed_cidrs_v6 = cidrs.filter((cidr) => cidr.includes(':')).map((cidr) => ({ cidr }))
- return {
- ...(allowed_cidrs.length > 0 ? { allowed_cidrs } : {}),
- ...(allowed_cidrs_v6.length > 0 ? { allowed_cidrs_v6 } : {}),
- }
- }
- return draft.grants
- .filter((grant) => grant.enabled)
- .map((grant) => {
- const expires_at = grant.hasExpiry ? toUnixSeconds(grant.expiry) : undefined
- const allowed_networks = serializeAllowedNetworks(grant.ipRanges)
- return {
- role: grant.roleId,
- ...(grant.branchesOnly ? { branches_only: true } : {}),
- ...(typeof expires_at === 'number' ? { expires_at } : {}),
- ...(allowed_networks ? { allowed_networks } : {}),
- }
- })
- }
|