# Copyright 2025 New Vector Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
# Please see LICENSE files in the repository root for full details.

# METADATA
# schemas:
#   - input: schema["authorization_grant_input"]
package authorization_grant

import rego.v1

import data.common

default allow := false

allow if {
	count(violation) == 0
}

# Users can request admin scopes if either:
# 1. They are in the admin_users list
can_request_admin(user) if {
	some admin_user in data.admin_users
	user.username == admin_user
}

# 2. They have the can_request_admin flag set to true
can_request_admin(user) if {
	user.can_request_admin
}

interactive_grant_type("authorization_code") := true

interactive_grant_type("urn:ietf:params:oauth:grant-type:device_code") := true

# Special case to make empty scope work
allowed_scope("") := true

allowed_scope("openid") := true

allowed_scope("email") := true

# This grants access to Synapse's admin API endpoints
allowed_scope("urn:synapse:admin:*") if {
	# Synapse doesn't support user-less tokens yet, so access to the admin API
	# can only be used with an authorization_code grant or a device code grant
	# as the user is present
	interactive_grant_type(input.grant_type)
	can_request_admin(input.user)
}

# This grants access to the /graphql API endpoint
allowed_scope("urn:mas:graphql:*") := true

# This makes it possible to query and do anything in the GraphQL API as an admin
allowed_scope("urn:mas:admin") if {
	interactive_grant_type(input.grant_type)
	can_request_admin(input.user)
}

# This makes it possible to get the admin scope for clients that are allowed
allowed_scope("urn:mas:admin") if {
	input.grant_type == "client_credentials"
	some client in data.admin_clients
	input.client.id == client
}

allowed_scope(scope) if {
	# Grant access to the C-S API only if there is a user
	interactive_grant_type(input.grant_type)
	regex.match(`^urn:matrix:org.matrix.msc2967.client:device:[A-Za-z0-9._~!$&'()*+,;=:@/-]{10,}$`, scope)
}

allowed_scope(scope) if {
	# Grant access to the C-S API only if there is a user
	interactive_grant_type(input.grant_type)
	regex.match(`^urn:matrix:client:device:[A-Za-z0-9._~!$&'()*+,;=:@/-]{10,}$`, scope)
}

allowed_scope("urn:matrix:client:api:*") if {
	# Grant access to the C-S API only if there is a user
	interactive_grant_type(input.grant_type)
}

allowed_scope("urn:matrix:org.matrix.msc2967.client:api:*") if {
	# Grant access to the C-S API only if there is a user
	interactive_grant_type(input.grant_type)
}

uses_unstable_scopes if {
	scope_list := split(input.scope, " ")
	count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:")}) > 0
}

uses_stable_scopes if {
	scope_list := split(input.scope, " ")
	count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:")}) > 0
}

has_device_scope if {
	scope_list := split(input.scope, " ")
	count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:device:")}) > 0
}

has_device_scope if {
	scope_list := split(input.scope, " ")
	count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:device:")}) > 0
}

has_cs_api_scope if {
	scope_list := split(input.scope, " ")
	count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:api:")}) > 0
}

has_cs_api_scope if {
	scope_list := split(input.scope, " ")
	count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:api:")}) > 0
}

# METADATA
# entrypoint: true
violation contains {"msg": msg} if {
	some scope in split(input.scope, " ")
	not allowed_scope(scope)
	msg := sprintf("scope '%s' not allowed", [scope])
}

violation contains {"msg": "only one device scope is allowed at a time"} if {
	scope_list := split(input.scope, " ")
	count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:device:")}) > 1
}

violation contains {"msg": "only one device scope is allowed at a time"} if {
	scope_list := split(input.scope, " ")
	count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:device:")}) > 1
}

# Prevent the creation of C-S API devices for sessions that don't have C-S API access.
violation contains {"msg": "device scopes are only allowed when the client-server API scope is requested"} if {
	has_device_scope
	not has_cs_api_scope
}

violation contains {"msg": "request cannot mix unstable and stable scopes"} if {
	uses_stable_scopes
	uses_unstable_scopes
}

violation contains {"msg": sprintf(
	"Requester [%s] isn't allowed to do this action",
	[common.format_requester(input.requester)],
)} if {
	common.requester_banned(input.requester, data.requester)
}

violation contains {
	"code": "too-many-sessions",
	"msg": "user has too many active sessions",
} if {
	# Only apply if session limits are enabled in the config
	data.session_limit != null

	# Only apply if it's a user logging in (who therefore has countable sessions)
	input.session_counts != null

	# For OAuth 2 login, a violation occurs when the soft limit has already been
	# reached or exceeded.
	# We use the soft limit because the user will be able to interactively remove
	# sessions to return under the limit.
	data.session_limit.soft_limit <= input.session_counts.total
}
