diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 3038df4ab8..ddab210718 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -13,22 +13,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""This module contains classes for authenticating the user."""
+import logging
+
+import pymacaroons
from canonicaljson import encode_canonical_json
from signedjson.key import decode_verify_key_bytes
from signedjson.sign import verify_signed_json, SignatureVerifyException
-
from twisted.internet import defer
+from unpaddedbase64 import decode_base64
+import synapse.types
from synapse.api.constants import EventTypes, Membership, JoinRules
from synapse.api.errors import AuthError, Codes, SynapseError, EventSizeError
-from synapse.types import Requester, RoomID, UserID, EventID
-from synapse.util.logutils import log_function
+from synapse.types import UserID, get_domain_from_id
from synapse.util.logcontext import preserve_context_over_fn
-from unpaddedbase64 import decode_base64
-
-import logging
-import pymacaroons
+from synapse.util.logutils import log_function
+from synapse.util.metrics import Measure
logger = logging.getLogger(__name__)
@@ -39,23 +39,34 @@ AuthEventTypes = (
EventTypes.ThirdPartyInvite,
)
+# guests always get this device id.
+GUEST_DEVICE_ID = "guest_device"
-class Auth(object):
+class Auth(object):
+ """
+ FIXME: This class contains a mix of functions for authenticating users
+ of our client-server API and authenticating events added to room graphs.
+ """
def __init__(self, hs):
self.hs = hs
+ self.clock = hs.get_clock()
self.store = hs.get_datastore()
self.state = hs.get_state_handler()
self.TOKEN_NOT_FOUND_HTTP_STATUS = 401
- self._KNOWN_CAVEAT_PREFIXES = set([
- "gen = ",
- "guest = ",
- "type = ",
- "time < ",
- "user_id = ",
- ])
-
- def check(self, event, auth_events):
+
+ @defer.inlineCallbacks
+ def check_from_context(self, event, context, do_sig_check=True):
+ auth_events_ids = yield self.compute_auth_events(
+ event, context.prev_state_ids, for_verification=True,
+ )
+ auth_events = yield self.store.get_events(auth_events_ids)
+ auth_events = {
+ (e.type, e.state_key): e for e in auth_events.values()
+ }
+ self.check(event, auth_events=auth_events, do_sig_check=do_sig_check)
+
+ def check(self, event, auth_events, do_sig_check=True):
""" Checks if this event is correctly authed.
Args:
@@ -66,11 +77,35 @@ class Auth(object):
Returns:
True if the auth checks pass.
"""
- self.check_size_limits(event)
+ with Measure(self.clock, "auth.check"):
+ self.check_size_limits(event)
- try:
if not hasattr(event, "room_id"):
raise AuthError(500, "Event has no room_id: %s" % event)
+
+ if do_sig_check:
+ sender_domain = get_domain_from_id(event.sender)
+ event_id_domain = get_domain_from_id(event.event_id)
+
+ is_invite_via_3pid = (
+ event.type == EventTypes.Member
+ and event.membership == Membership.INVITE
+ and "third_party_invite" in event.content
+ )
+
+ # Check the sender's domain has signed the event
+ if not event.signatures.get(sender_domain):
+ # We allow invites via 3pid to have a sender from a different
+ # HS, as the sender must match the sender of the original
+ # 3pid invite. This is checked further down with the
+ # other dedicated membership checks.
+ if not is_invite_via_3pid:
+ raise AuthError(403, "Event not signed by sender's server")
+
+ # Check the event_id's domain has signed the event
+ if not event.signatures.get(event_id_domain):
+ raise AuthError(403, "Event not signed by sending server")
+
if auth_events is None:
# Oh, we don't know what the state of the room was, so we
# are trusting that this is allowed (at least for now)
@@ -78,6 +113,12 @@ class Auth(object):
return True
if event.type == EventTypes.Create:
+ room_id_domain = get_domain_from_id(event.room_id)
+ if room_id_domain != sender_domain:
+ raise AuthError(
+ 403,
+ "Creation event's room_id domain does not match sender's"
+ )
# FIXME
return True
@@ -89,8 +130,8 @@ class Auth(object):
"Room %r does not exist" % (event.room_id,)
)
- creating_domain = RoomID.from_string(event.room_id).domain
- originating_domain = UserID.from_string(event.sender).domain
+ creating_domain = get_domain_from_id(event.room_id)
+ originating_domain = get_domain_from_id(event.sender)
if creating_domain != originating_domain:
if not self.can_federate(event, auth_events):
raise AuthError(
@@ -100,6 +141,22 @@ class Auth(object):
# FIXME: Temp hack
if event.type == EventTypes.Aliases:
+ if not event.is_state():
+ raise AuthError(
+ 403,
+ "Alias event must be a state event",
+ )
+ if not event.state_key:
+ raise AuthError(
+ 403,
+ "Alias event must have non-empty state_key"
+ )
+ sender_domain = get_domain_from_id(event.sender)
+ if event.state_key != sender_domain:
+ raise AuthError(
+ 403,
+ "Alias event's state_key does not match sender's domain"
+ )
return True
logger.debug(
@@ -118,6 +175,24 @@ class Auth(object):
return allowed
self.check_event_sender_in_room(event, auth_events)
+
+ # Special case to allow m.room.third_party_invite events wherever
+ # a user is allowed to issue invites. Fixes
+ # https://github.com/vector-im/vector-web/issues/1208 hopefully
+ if event.type == EventTypes.ThirdPartyInvite:
+ user_level = self._get_user_power_level(event.user_id, auth_events)
+ invite_level = self._get_named_level(auth_events, "invite", 0)
+
+ if user_level < invite_level:
+ raise AuthError(
+ 403, (
+ "You cannot issue a third party invite for %s." %
+ (event.content.display_name,)
+ )
+ )
+ else:
+ return True
+
self._can_send_event(event, auth_events)
if event.type == EventTypes.PowerLevels:
@@ -127,13 +202,6 @@ class Auth(object):
self.check_redaction(event, auth_events)
logger.debug("Allowing! %s", event)
- except AuthError as e:
- logger.info(
- "Event auth check failed on event %s with msg: %s",
- event, e.msg
- )
- logger.info("Denying! %s", event)
- raise
def check_size_limits(self, event):
def too_big(field):
@@ -219,21 +287,17 @@ class Auth(object):
@defer.inlineCallbacks
def check_host_in_room(self, room_id, host):
- curr_state = yield self.state.get_current_state(room_id)
+ with Measure(self.clock, "check_host_in_room"):
+ latest_event_ids = yield self.store.get_latest_event_ids_in_room(room_id)
- for event in curr_state.values():
- if event.type == EventTypes.Member:
- try:
- if UserID.from_string(event.state_key).domain != host:
- continue
- except:
- logger.warn("state_key not user_id: %s", event.state_key)
- continue
-
- if event.content["membership"] == Membership.JOIN:
- defer.returnValue(True)
+ entry = yield self.state.resolve_state_groups(
+ room_id, latest_event_ids
+ )
- defer.returnValue(False)
+ ret = yield self.store.is_host_joined(
+ room_id, host, entry.state_group, entry.state
+ )
+ defer.returnValue(ret)
def check_event_sender_in_room(self, event, auth_events):
key = (EventTypes.Member, event.user_id, )
@@ -271,8 +335,8 @@ class Auth(object):
target_user_id = event.state_key
- creating_domain = RoomID.from_string(event.room_id).domain
- target_domain = UserID.from_string(target_user_id).domain
+ creating_domain = get_domain_from_id(event.room_id)
+ target_domain = get_domain_from_id(target_user_id)
if creating_domain != target_domain:
if not self.can_federate(event, auth_events):
raise AuthError(
@@ -328,6 +392,10 @@ class Auth(object):
if Membership.INVITE == membership and "third_party_invite" in event.content:
if not self._verify_third_party_invite(event, auth_events):
raise AuthError(403, "You are not invited to this room.")
+ if target_banned:
+ raise AuthError(
+ 403, "%s is banned from the room" % (target_user_id,)
+ )
return True
if Membership.JOIN != membership:
@@ -432,6 +500,9 @@ class Auth(object):
if not invite_event:
return False
+ if invite_event.sender != event.sender:
+ return False
+
if event.user_id != invite_event.user_id:
return False
@@ -512,33 +583,38 @@ class Auth(object):
return default
@defer.inlineCallbacks
- def get_user_by_req(self, request, allow_guest=False):
+ def get_user_by_req(self, request, allow_guest=False, rights="access"):
""" Get a registered user's ID.
Args:
request - An HTTP request with an access_token query parameter.
Returns:
- tuple of:
- UserID (str)
- Access token ID (str)
+ defer.Deferred: resolves to a ``synapse.types.Requester`` object
Raises:
AuthError if no user by that token exists or the token is invalid.
"""
# Can optionally look elsewhere in the request (e.g. headers)
try:
- user_id = yield self._get_appservice_user_id(request.args)
+ user_id, app_service = yield self._get_appservice_user_id(request)
if user_id:
request.authenticated_entity = user_id
defer.returnValue(
- Requester(UserID.from_string(user_id), "", False)
+ synapse.types.create_requester(user_id, app_service=app_service)
)
- access_token = request.args["access_token"][0]
- user_info = yield self.get_user_by_access_token(access_token)
+ access_token = get_access_token_from_request(
+ request, self.TOKEN_NOT_FOUND_HTTP_STATUS
+ )
+
+ user_info = yield self.get_user_by_access_token(access_token, rights)
user = user_info["user"]
token_id = user_info["token_id"]
is_guest = user_info["is_guest"]
+ # device_id may not be present if get_user_by_access_token has been
+ # stubbed out.
+ device_id = user_info.get("device_id")
+
ip_addr = self.hs.get_ip_from_request(request)
user_agent = request.requestHeaders.getRawHeaders(
"User-Agent",
@@ -550,7 +626,8 @@ class Auth(object):
user=user,
access_token=access_token,
ip=ip_addr,
- user_agent=user_agent
+ user_agent=user_agent,
+ device_id=device_id,
)
if is_guest and not allow_guest:
@@ -560,7 +637,9 @@ class Auth(object):
request.authenticated_entity = user.to_string()
- defer.returnValue(Requester(user, token_id, is_guest))
+ defer.returnValue(synapse.types.create_requester(
+ user, token_id, is_guest, device_id, app_service=app_service)
+ )
except KeyError:
raise AuthError(
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token.",
@@ -568,19 +647,21 @@ class Auth(object):
)
@defer.inlineCallbacks
- def _get_appservice_user_id(self, request_args):
- app_service = yield self.store.get_app_service_by_token(
- request_args["access_token"][0]
+ def _get_appservice_user_id(self, request):
+ app_service = self.store.get_app_service_by_token(
+ get_access_token_from_request(
+ request, self.TOKEN_NOT_FOUND_HTTP_STATUS
+ )
)
if app_service is None:
- defer.returnValue(None)
+ defer.returnValue((None, None))
- if "user_id" not in request_args:
- defer.returnValue(app_service.sender)
+ if "user_id" not in request.args:
+ defer.returnValue((app_service.sender, app_service))
- user_id = request_args["user_id"][0]
+ user_id = request.args["user_id"][0]
if app_service.sender == user_id:
- defer.returnValue(app_service.sender)
+ defer.returnValue((app_service.sender, app_service))
if not app_service.is_interested_in_user(user_id):
raise AuthError(
@@ -592,10 +673,10 @@ class Auth(object):
403,
"Application service has not registered this user"
)
- defer.returnValue(user_id)
+ defer.returnValue((user_id, app_service))
@defer.inlineCallbacks
- def get_user_by_access_token(self, token):
+ def get_user_by_access_token(self, token, rights="access"):
""" Get a registered user's ID.
Args:
@@ -606,46 +687,62 @@ class Auth(object):
AuthError if no user by that token exists or the token is invalid.
"""
try:
- ret = yield self.get_user_from_macaroon(token)
+ ret = yield self.get_user_from_macaroon(token, rights)
except AuthError:
# TODO(daniel): Remove this fallback when all existing access tokens
# have been re-issued as macaroons.
+ if self.hs.config.expire_access_token:
+ raise
ret = yield self._look_up_user_by_access_token(token)
+
defer.returnValue(ret)
@defer.inlineCallbacks
- def get_user_from_macaroon(self, macaroon_str):
+ def get_user_from_macaroon(self, macaroon_str, rights="access"):
try:
macaroon = pymacaroons.Macaroon.deserialize(macaroon_str)
- self.validate_macaroon(macaroon, "access", False)
- user_prefix = "user_id = "
- user = None
+ user_id = self.get_user_id_from_macaroon(macaroon)
+ user = UserID.from_string(user_id)
+
+ self.validate_macaroon(
+ macaroon, rights, self.hs.config.expire_access_token,
+ user_id=user_id,
+ )
+
guest = False
for caveat in macaroon.caveats:
- if caveat.caveat_id.startswith(user_prefix):
- user = UserID.from_string(caveat.caveat_id[len(user_prefix):])
- elif caveat.caveat_id == "guest = true":
+ if caveat.caveat_id == "guest = true":
guest = True
- if user is None:
- raise AuthError(
- self.TOKEN_NOT_FOUND_HTTP_STATUS, "No user caveat in macaroon",
- errcode=Codes.UNKNOWN_TOKEN
- )
-
if guest:
ret = {
"user": user,
"is_guest": True,
"token_id": None,
+ # all guests get the same device id
+ "device_id": GUEST_DEVICE_ID,
+ }
+ elif rights == "delete_pusher":
+ # We don't store these tokens in the database
+ ret = {
+ "user": user,
+ "is_guest": False,
+ "token_id": None,
+ "device_id": None,
}
else:
- # This codepath exists so that we can actually return a
- # token ID, because we use token IDs in place of device
- # identifiers throughout the codebase.
- # TODO(daniel): Remove this fallback when device IDs are
- # properly implemented.
+ # This codepath exists for several reasons:
+ # * so that we can actually return a token ID, which is used
+ # in some parts of the schema (where we probably ought to
+ # use device IDs instead)
+ # * the only way we currently have to invalidate an
+ # access_token is by removing it from the database, so we
+ # have to check here that it is still in the db
+ # * some attributes (notably device_id) aren't stored in the
+ # macaroon. They probably should be.
+ # TODO: build the dictionary from the macaroon once the
+ # above are fixed
ret = yield self._look_up_user_by_access_token(macaroon_str)
if ret["user"] != user:
logger.error(
@@ -665,31 +762,67 @@ class Auth(object):
errcode=Codes.UNKNOWN_TOKEN
)
- def validate_macaroon(self, macaroon, type_string, verify_expiry):
+ def get_user_id_from_macaroon(self, macaroon):
+ """Retrieve the user_id given by the caveats on the macaroon.
+
+ Does *not* validate the macaroon.
+
+ Args:
+ macaroon (pymacaroons.Macaroon): The macaroon to validate
+
+ Returns:
+ (str) user id
+
+ Raises:
+ AuthError if there is no user_id caveat in the macaroon
+ """
+ user_prefix = "user_id = "
+ for caveat in macaroon.caveats:
+ if caveat.caveat_id.startswith(user_prefix):
+ return caveat.caveat_id[len(user_prefix):]
+ raise AuthError(
+ self.TOKEN_NOT_FOUND_HTTP_STATUS, "No user caveat in macaroon",
+ errcode=Codes.UNKNOWN_TOKEN
+ )
+
+ def validate_macaroon(self, macaroon, type_string, verify_expiry, user_id):
"""
validate that a Macaroon is understood by and was signed by this server.
Args:
macaroon(pymacaroons.Macaroon): The macaroon to validate
- type_string(str): The kind of token this is (e.g. "access", "refresh")
+ type_string(str): The kind of token required (e.g. "access",
+ "delete_pusher")
verify_expiry(bool): Whether to verify whether the macaroon has expired.
- This should really always be True, but no clients currently implement
- token refresh, so we can't enforce expiry yet.
+ user_id (str): The user_id required
"""
v = pymacaroons.Verifier()
+
+ # the verifier runs a test for every caveat on the macaroon, to check
+ # that it is met for the current request. Each caveat must match at
+ # least one of the predicates specified by satisfy_exact or
+ # specify_general.
v.satisfy_exact("gen = 1")
v.satisfy_exact("type = " + type_string)
- v.satisfy_general(lambda c: c.startswith("user_id = "))
+ v.satisfy_exact("user_id = %s" % user_id)
v.satisfy_exact("guest = true")
+
+ # verify_expiry should really always be True, but there exist access
+ # tokens in the wild which expire when they should not, so we can't
+ # enforce expiry yet (so we have to allow any caveat starting with
+ # 'time < ' in access tokens).
+ #
+ # On the other hand, short-term login tokens (as used by CAS login, for
+ # example) have an expiry time which we do want to enforce.
+
if verify_expiry:
v.satisfy_general(self._verify_expiry)
else:
v.satisfy_general(lambda c: c.startswith("time < "))
- v.verify(macaroon, self.hs.config.macaroon_secret_key)
+ # access_tokens include a nonce for uniqueness: any value is acceptable
+ v.satisfy_general(lambda c: c.startswith("nonce = "))
- v = pymacaroons.Verifier()
- v.satisfy_general(self._verify_recognizes_caveats)
v.verify(macaroon, self.hs.config.macaroon_secret_key)
def _verify_expiry(self, caveat):
@@ -700,15 +833,6 @@ class Auth(object):
now = self.hs.get_clock().time_msec()
return now < expiry
- def _verify_recognizes_caveats(self, caveat):
- first_space = caveat.find(" ")
- if first_space < 0:
- return False
- second_space = caveat.find(" ", first_space + 1)
- if second_space < 0:
- return False
- return caveat[:second_space + 1] in self._KNOWN_CAVEAT_PREFIXES
-
@defer.inlineCallbacks
def _look_up_user_by_access_token(self, token):
ret = yield self.store.get_user_by_access_token(token)
@@ -718,18 +842,23 @@ class Auth(object):
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Unrecognised access token.",
errcode=Codes.UNKNOWN_TOKEN
)
+ # we use ret.get() below because *lots* of unit tests stub out
+ # get_user_by_access_token in a way where it only returns a couple of
+ # the fields.
user_info = {
"user": UserID.from_string(ret.get("name")),
"token_id": ret.get("token_id", None),
"is_guest": False,
+ "device_id": ret.get("device_id"),
}
defer.returnValue(user_info)
- @defer.inlineCallbacks
def get_appservice_by_req(self, request):
try:
- token = request.args["access_token"][0]
- service = yield self.store.get_app_service_by_token(token)
+ token = get_access_token_from_request(
+ request, self.TOKEN_NOT_FOUND_HTTP_STATUS
+ )
+ service = self.store.get_app_service_by_token(token)
if not service:
logger.warn("Unrecognised appservice access token: %s" % (token,))
raise AuthError(
@@ -738,7 +867,7 @@ class Auth(object):
errcode=Codes.UNKNOWN_TOKEN
)
request.authenticated_entity = service.sender
- defer.returnValue(service)
+ return defer.succeed(service)
except KeyError:
raise AuthError(
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token."
@@ -749,7 +878,7 @@ class Auth(object):
@defer.inlineCallbacks
def add_auth_events(self, builder, context):
- auth_ids = self.compute_auth_events(builder, context.current_state)
+ auth_ids = yield self.compute_auth_events(builder, context.prev_state_ids)
auth_events_entries = yield self.store.add_event_hashes(
auth_ids
@@ -757,30 +886,32 @@ class Auth(object):
builder.auth_events = auth_events_entries
- def compute_auth_events(self, event, current_state):
+ @defer.inlineCallbacks
+ def compute_auth_events(self, event, current_state_ids, for_verification=False):
if event.type == EventTypes.Create:
- return []
+ defer.returnValue([])
auth_ids = []
key = (EventTypes.PowerLevels, "", )
- power_level_event = current_state.get(key)
+ power_level_event_id = current_state_ids.get(key)
- if power_level_event:
- auth_ids.append(power_level_event.event_id)
+ if power_level_event_id:
+ auth_ids.append(power_level_event_id)
key = (EventTypes.JoinRules, "", )
- join_rule_event = current_state.get(key)
+ join_rule_event_id = current_state_ids.get(key)
key = (EventTypes.Member, event.user_id, )
- member_event = current_state.get(key)
+ member_event_id = current_state_ids.get(key)
key = (EventTypes.Create, "", )
- create_event = current_state.get(key)
- if create_event:
- auth_ids.append(create_event.event_id)
+ create_event_id = current_state_ids.get(key)
+ if create_event_id:
+ auth_ids.append(create_event_id)
- if join_rule_event:
+ if join_rule_event_id:
+ join_rule_event = yield self.store.get_event(join_rule_event_id)
join_rule = join_rule_event.content.get("join_rule")
is_public = join_rule == JoinRules.PUBLIC if join_rule else False
else:
@@ -789,15 +920,21 @@ class Auth(object):
if event.type == EventTypes.Member:
e_type = event.content["membership"]
if e_type in [Membership.JOIN, Membership.INVITE]:
- if join_rule_event:
- auth_ids.append(join_rule_event.event_id)
+ if join_rule_event_id:
+ auth_ids.append(join_rule_event_id)
if e_type == Membership.JOIN:
- if member_event and not is_public:
- auth_ids.append(member_event.event_id)
+ if member_event_id and not is_public:
+ auth_ids.append(member_event_id)
else:
- if member_event:
- auth_ids.append(member_event.event_id)
+ if member_event_id:
+ auth_ids.append(member_event_id)
+
+ if for_verification:
+ key = (EventTypes.Member, event.state_key, )
+ existing_event_id = current_state_ids.get(key)
+ if existing_event_id:
+ auth_ids.append(existing_event_id)
if e_type == Membership.INVITE:
if "third_party_invite" in event.content:
@@ -805,26 +942,26 @@ class Auth(object):
EventTypes.ThirdPartyInvite,
event.content["third_party_invite"]["signed"]["token"]
)
- third_party_invite = current_state.get(key)
- if third_party_invite:
- auth_ids.append(third_party_invite.event_id)
- elif member_event:
+ third_party_invite_id = current_state_ids.get(key)
+ if third_party_invite_id:
+ auth_ids.append(third_party_invite_id)
+ elif member_event_id:
+ member_event = yield self.store.get_event(member_event_id)
if member_event.content["membership"] == Membership.JOIN:
auth_ids.append(member_event.event_id)
- return auth_ids
+ defer.returnValue(auth_ids)
- @log_function
- def _can_send_event(self, event, auth_events):
+ def _get_send_level(self, etype, state_key, auth_events):
key = (EventTypes.PowerLevels, "", )
send_level_event = auth_events.get(key)
send_level = None
if send_level_event:
send_level = send_level_event.content.get("events", {}).get(
- event.type
+ etype
)
if send_level is None:
- if hasattr(event, "state_key"):
+ if state_key is not None:
send_level = send_level_event.content.get(
"state_default", 50
)
@@ -838,6 +975,13 @@ class Auth(object):
else:
send_level = 0
+ return send_level
+
+ @log_function
+ def _can_send_event(self, event, auth_events):
+ send_level = self._get_send_level(
+ event.type, event.get("state_key", None), auth_events
+ )
user_level = self._get_user_power_level(event.user_id, auth_events)
if user_level < send_level:
@@ -855,16 +999,6 @@ class Auth(object):
403,
"You are not allowed to set others state"
)
- else:
- sender_domain = UserID.from_string(
- event.user_id
- ).domain
-
- if sender_domain != event.state_key:
- raise AuthError(
- 403,
- "You are not allowed to set others state"
- )
return True
@@ -888,8 +1022,8 @@ class Auth(object):
if user_level >= redact_level:
return False
- redacter_domain = EventID.from_string(event.event_id).domain
- redactee_domain = EventID.from_string(event.redacts).domain
+ redacter_domain = get_domain_from_id(event.event_id)
+ redactee_domain = get_domain_from_id(event.redacts)
if redacter_domain == redactee_domain:
return True
@@ -982,3 +1116,108 @@ class Auth(object):
"You don't have permission to add ops level greater "
"than your own"
)
+
+ @defer.inlineCallbacks
+ def check_can_change_room_list(self, room_id, user):
+ """Check if the user is allowed to edit the room's entry in the
+ published room list.
+
+ Args:
+ room_id (str)
+ user (UserID)
+ """
+
+ is_admin = yield self.is_server_admin(user)
+ if is_admin:
+ defer.returnValue(True)
+
+ user_id = user.to_string()
+ yield self.check_joined_room(room_id, user_id)
+
+ # We currently require the user is a "moderator" in the room. We do this
+ # by checking if they would (theoretically) be able to change the
+ # m.room.aliases events
+ power_level_event = yield self.state.get_current_state(
+ room_id, EventTypes.PowerLevels, ""
+ )
+
+ auth_events = {}
+ if power_level_event:
+ auth_events[(EventTypes.PowerLevels, "")] = power_level_event
+
+ send_level = self._get_send_level(
+ EventTypes.Aliases, "", auth_events
+ )
+ user_level = self._get_user_power_level(user_id, auth_events)
+
+ if user_level < send_level:
+ raise AuthError(
+ 403,
+ "This server requires you to be a moderator in the room to"
+ " edit its room list entry"
+ )
+
+
+def has_access_token(request):
+ """Checks if the request has an access_token.
+
+ Returns:
+ bool: False if no access_token was given, True otherwise.
+ """
+ query_params = request.args.get("access_token")
+ auth_headers = request.requestHeaders.getRawHeaders("Authorization")
+ return bool(query_params) or bool(auth_headers)
+
+
+def get_access_token_from_request(request, token_not_found_http_status=401):
+ """Extracts the access_token from the request.
+
+ Args:
+ request: The http request.
+ token_not_found_http_status(int): The HTTP status code to set in the
+ AuthError if the token isn't found. This is used in some of the
+ legacy APIs to change the status code to 403 from the default of
+ 401 since some of the old clients depended on auth errors returning
+ 403.
+ Returns:
+ str: The access_token
+ Raises:
+ AuthError: If there isn't an access_token in the request.
+ """
+
+ auth_headers = request.requestHeaders.getRawHeaders("Authorization")
+ query_params = request.args.get("access_token")
+ if auth_headers:
+ # Try the get the access_token from a "Authorization: Bearer"
+ # header
+ if query_params is not None:
+ raise AuthError(
+ token_not_found_http_status,
+ "Mixing Authorization headers and access_token query parameters.",
+ errcode=Codes.MISSING_TOKEN,
+ )
+ if len(auth_headers) > 1:
+ raise AuthError(
+ token_not_found_http_status,
+ "Too many Authorization headers.",
+ errcode=Codes.MISSING_TOKEN,
+ )
+ parts = auth_headers[0].split(" ")
+ if parts[0] == "Bearer" and len(parts) == 2:
+ return parts[1]
+ else:
+ raise AuthError(
+ token_not_found_http_status,
+ "Invalid Authorization header.",
+ errcode=Codes.MISSING_TOKEN,
+ )
+ else:
+ # Try to get the access_token from the query params.
+ if not query_params:
+ raise AuthError(
+ token_not_found_http_status,
+ "Missing access token.",
+ errcode=Codes.MISSING_TOKEN
+ )
+
+ return query_params[0]
|