from google.appengine.ext.webapp.template import render, register_template_library from google.appengine.ext import ndb from google.appengine.api import users, memcache from googleapiclient.errors import HttpError from urlparse import urlparse import logging import webapp2 import config import gsuite from third_party import xsrfutil register_template_library('third_party.xsrfutil') class Link(ndb.Model): url = ndb.StringProperty() owner_id = ndb.StringProperty() owner_name = ndb.StringProperty() viewcount = ndb.IntegerProperty() public = ndb.BooleanProperty() visibility = ndb.TextProperty() def errorPage(response, code, message): context = {'code': code, 'message': message, 'corpname': config.CORP_NAME} response.write(render('template/error.html', context)) response.set_status(code) def check_redirect(func): def decorate(self, *args, **kwargs): if config.ALWAYS_REDIRECT_TO_FQDN and self.request.host != config.GOLINKS_FQDN: url = self.request.url.replace(self.request.host, config.GOLINKS_FQDN, 1) return self.redirect(url) return func(self, *args, **kwargs) return decorate def isValidUrl(url): o = urlparse(url) return o.scheme in config.URL_ALLOWED_SCHEMAS class ShowLinks(webapp2.RequestHandler): @check_redirect def get(self, param): user = users.get_current_user() if not user: self.redirect(users.create_login_url(self.request.path)) return sign_out_link = users.create_logout_url('/') is_admin = users.is_current_user_admin() if param == "all" and is_admin: links = Link.query().fetch() else: links = Link.query(Link.owner_id == user.user_id()).fetch() context = { "links": links, "is_admin": is_admin, "sign_out_link": sign_out_link, "fqdn": config.GOLINKS_FQDN, "hostname": config.GOLINKS_HOSTNAME } self.response.write(render("template/list.html", context)) class DeleteLink(webapp2.RequestHandler): @check_redirect @xsrfutil.xsrf_protect def post(self, link): user = users.get_current_user() if not user: self.redirect(users.create_login_url(self.request.path)) return key = link.rstrip("/") l = Link.get_by_id(key) if l.owner_id: if l.owner_id != user.user_id() and not users.is_current_user_admin(): logging.info("%s tried to delete /%s but doesn't have permission" % (user.email(), key)) errorPage(self.response, 403, "Access denied") return l.key.delete() logging.info("%s deleted /%s" % (user.email(), key)) self.redirect("/links/my") class EditLink(webapp2.RequestHandler): @check_redirect @xsrfutil.xsrf_protect def post(self, link): user = users.get_current_user() if not user: self.redirect(users.create_login_url(self.request.path)) return key = self.request.get("key", "").rstrip("/") url = self.request.get("url", None) public = self.request.get("public", 0) visibility = self.request.get("visibility", "") if not key: errorPage(self.response, 400, "Shortened URL required") return blacklist = ["edit", "links", "delete"] for word in blacklist: if key.startswith(word + '/') or key == word: logging.info("%s tried to add forbidden URL /%s" % (user.email(), key)) errorPage(self.response, 400, "Shortened URL forbidden") return if link: if key != link: logging.info( "%s tried to change /%s to %s but such request is forbidden" % (user.email(), link, key)) errorPage(self.response, 400, "Cannot change shortened URL") return if not isValidUrl(url): logging.info("%s tried to set /%s to illegal URL: %s" % (user.email(), key, url)) errorPage(self.response, 400, "URL Illegal") return l = Link.get_or_insert(key) if l.owner_id: if not link: logging.info("%s tried to overwrite /%s" % (user.email(), key)) errorPage( self.response, 500, "Link already exists... Please update existing one or change url.") return if l.owner_id != user.user_id() and not users.is_current_user_admin(): logging.info("%s tried to modify /%s but doesn't have permission" % (user.email(), key)) errorPage(self.response, 403, "Access denied") return else: l.owner_id = user.user_id() l.owner_name = user.nickname() if config.ENABLE_GOOGLE_GROUPS_INTEGRATION: groups = map(lambda x: x.strip(), visibility.split(';')) for group in groups: if group: try: logging.info("Checking if %s is a valid group" % group) gsuite.directory_service.groups().get(groupKey=group).execute() except HttpError: logging.info("%s tried to add invalid group %s to /%s" % (user.email(), group, key)) errorPage(self.response, 400, "Invalid group: " + group) return l.visibility = visibility l.url = url if public: l.public = True else: l.public = False if not l.viewcount: l.viewcount = 0 l.put() logging.info("%s created or updated /%s to %s" % (user.email(), key, url)) self.redirect("/edit/" + key) @check_redirect def get(self, link): user = users.get_current_user() if not user: self.redirect(users.create_login_url(self.request.path)) return sign_out_link = users.create_logout_url('/') is_admin = users.is_current_user_admin() context = { "sign_out_link": sign_out_link, "is_admin": is_admin, "show_visibility": config.ENABLE_GOOGLE_GROUPS_INTEGRATION, 'hostname': config.GOLINKS_HOSTNAME } if link: link = link.rstrip("/") context.update({'key': link}) l = Link.get_by_id(link) if l: if l.owner_id: if l.owner_id != user.user_id() and not is_admin: logging.info( "%s tried to check details page of /%s but doesn't have permission" % (user.email(), link)) errorPage(self.response, 403, "Access denied") return context.update({ 'url': l.url, 'viewcount': l.viewcount, 'public': l.public, 'visibility': l.visibility or '', 'can_delete': 1, 'owner': l.owner_name }) logging.info("%s checked details page of /%s" % (user.email(), link)) self.response.write(render("template/edit.html", context)) class RedirectLink(webapp2.RequestHandler): @check_redirect def get(self, link): user = users.get_current_user() if link: link = link.rstrip("/") l = Link.get_by_id(link) if l: if l.public: username = "public-user" else: if not user: self.redirect(users.create_login_url(self.request.path)) return username = user.email() if l.visibility: if config.ENABLE_GOOGLE_GROUPS_INTEGRATION: memcacheKey = "v_%s_%s" % (user.user_id(), link) if not config.USE_MEMCACHE or not memcache.get(memcacheKey): groups = map(lambda x: x.strip(), l.visibility.split(';')) no_access = True for group in groups: if group: try: # NOTES: this does support nested group members but doesn't support external users # even though we don't currently allow external users to log in, this is worth # noting if we decide to support logging.info("Checking if %s is a member of %s" % (username, group)) if gsuite.directory_service.members().hasMember( groupKey=group, memberKey=user.email()).execute()['isMember']: no_access = False break except HttpError: pass if no_access: # no caching for 403 so that user can gain access immediately logging.info( "%s tried to access /%s but failed visibilitty check" % (username, link)) errorPage(self.response, 403, "You do not have access to the requested resource") return if config.USE_MEMCACHE: memcache.set(memcacheKey, 1, config.MEMCACHE_TTL) else: logging.info("%s has access to /%s by cache" % (username, link)) l.viewcount += 1 l.put() logging.info("%s accessed /%s and redirected to %s" % (username, link, str(l.url))) self.redirect(str(l.url)) return if not user: # we don't want external to know if url exists self.redirect(users.create_login_url(self.request.path)) return if not link: self.redirect('/links/my') return logging.info("%s accessed non-existent URL /%s" % (user.email(), link)) errorPage(self.response, 404, "Not Found!") app = webapp2.WSGIApplication([ ('/edit/([-\/\w]*)', EditLink), ('/delete/([-\/\w]+)', DeleteLink), ('/links/(\w*)', ShowLinks), ('/([-\/\w]*)', RedirectLink), ], debug=True)