##// END OF EJS Templates
wireproto: declare permissions requirements in @wireprotocommand (API)...
Gregory Szorc -
r36818:0b18604d default
parent child Browse files
Show More
@@ -164,21 +164,18 b' def uisetup(ui):'
164 overrides.openlargefile)
164 overrides.openlargefile)
165
165
166 # create the new wireproto commands ...
166 # create the new wireproto commands ...
167 wireproto.wireprotocommand('putlfile', 'sha')(proto.putlfile)
167 wireproto.wireprotocommand('putlfile', 'sha', permission='push')(
168 wireproto.wireprotocommand('getlfile', 'sha')(proto.getlfile)
168 proto.putlfile)
169 wireproto.wireprotocommand('statlfile', 'sha')(proto.statlfile)
169 wireproto.wireprotocommand('getlfile', 'sha', permission='pull')(
170 wireproto.wireprotocommand('lheads', '')(wireproto.heads)
170 proto.getlfile)
171 wireproto.wireprotocommand('statlfile', 'sha', permission='pull')(
172 proto.statlfile)
173 wireproto.wireprotocommand('lheads', '', permission='pull')(
174 wireproto.heads)
171
175
172 # ... and wrap some existing ones
176 # ... and wrap some existing ones
173 wireproto.commands['heads'].func = proto.heads
177 wireproto.commands['heads'].func = proto.heads
174
178
175 # make putlfile behave the same as push and {get,stat}lfile behave
176 # the same as pull w.r.t. permissions checks
177 wireproto.permissions['putlfile'] = 'push'
178 wireproto.permissions['getlfile'] = 'pull'
179 wireproto.permissions['statlfile'] = 'pull'
180 wireproto.permissions['lheads'] = 'pull'
181
182 extensions.wrapfunction(webcommands, 'decodepath', overrides.decodepath)
179 extensions.wrapfunction(webcommands, 'decodepath', overrides.decodepath)
183
180
184 extensions.wrapfunction(wireproto, '_capabilities', proto._capabilities)
181 extensions.wrapfunction(wireproto, '_capabilities', proto._capabilities)
@@ -37,7 +37,6 b' from .. import ('
37 templater,
37 templater,
38 ui as uimod,
38 ui as uimod,
39 util,
39 util,
40 wireproto,
41 wireprotoserver,
40 wireprotoserver,
42 )
41 )
43
42
@@ -47,9 +46,6 b' from . import ('
47 wsgicgi,
46 wsgicgi,
48 )
47 )
49
48
50 # Aliased for API compatibility.
51 perms = wireproto.permissions
52
53 archivespecs = util.sortdict((
49 archivespecs = util.sortdict((
54 ('zip', ('application/zip', 'zip', '.zip', None)),
50 ('zip', ('application/zip', 'zip', '.zip', None)),
55 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
@@ -592,10 +592,12 b' def supportedcompengines(ui, role):'
592
592
593 class commandentry(object):
593 class commandentry(object):
594 """Represents a declared wire protocol command."""
594 """Represents a declared wire protocol command."""
595 def __init__(self, func, args='', transports=None):
595 def __init__(self, func, args='', transports=None,
596 permission='push'):
596 self.func = func
597 self.func = func
597 self.args = args
598 self.args = args
598 self.transports = transports or set()
599 self.transports = transports or set()
600 self.permission = permission
599
601
600 def _merge(self, func, args):
602 def _merge(self, func, args):
601 """Merge this instance with an incoming 2-tuple.
603 """Merge this instance with an incoming 2-tuple.
@@ -605,7 +607,8 b' class commandentry(object):'
605 data not captured by the 2-tuple and a new instance containing
607 data not captured by the 2-tuple and a new instance containing
606 the union of the two objects is returned.
608 the union of the two objects is returned.
607 """
609 """
608 return commandentry(func, args=args, transports=set(self.transports))
610 return commandentry(func, args=args, transports=set(self.transports),
611 permission=self.permission)
609
612
610 # Old code treats instances as 2-tuples. So expose that interface.
613 # Old code treats instances as 2-tuples. So expose that interface.
611 def __iter__(self):
614 def __iter__(self):
@@ -643,7 +646,8 b' class commanddict(dict):'
643 else:
646 else:
644 # Use default values from @wireprotocommand.
647 # Use default values from @wireprotocommand.
645 v = commandentry(v[0], args=v[1],
648 v = commandentry(v[0], args=v[1],
646 transports=set(wireprototypes.TRANSPORTS))
649 transports=set(wireprototypes.TRANSPORTS),
650 permission='push')
647 else:
651 else:
648 raise ValueError('command entries must be commandentry instances '
652 raise ValueError('command entries must be commandentry instances '
649 'or 2-tuples')
653 'or 2-tuples')
@@ -672,12 +676,8 b" POLICY_V2_ONLY = 'v2-only'"
672
676
673 commands = commanddict()
677 commands = commanddict()
674
678
675 # Maps wire protocol name to operation type. This is used for permissions
679 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL,
676 # checking. All defined @wireiprotocommand should have an entry in this
680 permission='push'):
677 # dict.
678 permissions = {}
679
680 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL):
681 """Decorator to declare a wire protocol command.
681 """Decorator to declare a wire protocol command.
682
682
683 ``name`` is the name of the wire protocol command being provided.
683 ``name`` is the name of the wire protocol command being provided.
@@ -688,6 +688,12 b" def wireprotocommand(name, args='', tran"
688 ``transportpolicy`` is a POLICY_* constant denoting which transports
688 ``transportpolicy`` is a POLICY_* constant denoting which transports
689 this wire protocol command should be exposed to. By default, commands
689 this wire protocol command should be exposed to. By default, commands
690 are exposed to all wire protocol transports.
690 are exposed to all wire protocol transports.
691
692 ``permission`` defines the permission type needed to run this command.
693 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
694 respectively. Default is to assume command requires ``push`` permissions
695 because otherwise commands not declaring their permissions could modify
696 a repository that is supposed to be read-only.
691 """
697 """
692 if transportpolicy == POLICY_ALL:
698 if transportpolicy == POLICY_ALL:
693 transports = set(wireprototypes.TRANSPORTS)
699 transports = set(wireprototypes.TRANSPORTS)
@@ -701,14 +707,18 b" def wireprotocommand(name, args='', tran"
701 raise error.Abort(_('invalid transport policy value: %s') %
707 raise error.Abort(_('invalid transport policy value: %s') %
702 transportpolicy)
708 transportpolicy)
703
709
710 if permission not in ('push', 'pull'):
711 raise error.Abort(_('invalid wire protocol permission; got %s; '
712 'expected "push" or "pull"') % permission)
713
704 def register(func):
714 def register(func):
705 commands[name] = commandentry(func, args=args, transports=transports)
715 commands[name] = commandentry(func, args=args, transports=transports,
716 permission=permission)
706 return func
717 return func
707 return register
718 return register
708
719
709 # TODO define a more appropriate permissions type to use for this.
720 # TODO define a more appropriate permissions type to use for this.
710 permissions['batch'] = 'pull'
721 @wireprotocommand('batch', 'cmds *', permission='pull')
711 @wireprotocommand('batch', 'cmds *')
712 def batch(repo, proto, cmds, others):
722 def batch(repo, proto, cmds, others):
713 repo = repo.filtered("served")
723 repo = repo.filtered("served")
714 res = []
724 res = []
@@ -725,11 +735,9 b' def batch(repo, proto, cmds, others):'
725 # checking on each batched command.
735 # checking on each batched command.
726 # TODO formalize permission checking as part of protocol interface.
736 # TODO formalize permission checking as part of protocol interface.
727 if util.safehasattr(proto, 'checkperm'):
737 if util.safehasattr(proto, 'checkperm'):
728 # Assume commands with no defined permissions are writes / for
738 perm = commands[op].permission
729 # pushes. This is the safest from a security perspective because
739 assert perm in ('push', 'pull')
730 # it doesn't allow commands with undefined semantics from
740 proto.checkperm(perm)
731 # bypassing permissions checks.
732 proto.checkperm(permissions.get(op, 'push'))
733
741
734 if spec:
742 if spec:
735 keys = spec.split()
743 keys = spec.split()
@@ -758,8 +766,8 b' def batch(repo, proto, cmds, others):'
758
766
759 return bytesresponse(';'.join(res))
767 return bytesresponse(';'.join(res))
760
768
761 permissions['between'] = 'pull'
769 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY,
762 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY)
770 permission='pull')
763 def between(repo, proto, pairs):
771 def between(repo, proto, pairs):
764 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
772 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
765 r = []
773 r = []
@@ -768,8 +776,7 b' def between(repo, proto, pairs):'
768
776
769 return bytesresponse(''.join(r))
777 return bytesresponse(''.join(r))
770
778
771 permissions['branchmap'] = 'pull'
779 @wireprotocommand('branchmap', permission='pull')
772 @wireprotocommand('branchmap')
773 def branchmap(repo, proto):
780 def branchmap(repo, proto):
774 branchmap = repo.branchmap()
781 branchmap = repo.branchmap()
775 heads = []
782 heads = []
@@ -780,8 +787,8 b' def branchmap(repo, proto):'
780
787
781 return bytesresponse('\n'.join(heads))
788 return bytesresponse('\n'.join(heads))
782
789
783 permissions['branches'] = 'pull'
790 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY,
784 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY)
791 permission='pull')
785 def branches(repo, proto, nodes):
792 def branches(repo, proto, nodes):
786 nodes = decodelist(nodes)
793 nodes = decodelist(nodes)
787 r = []
794 r = []
@@ -790,8 +797,7 b' def branches(repo, proto, nodes):'
790
797
791 return bytesresponse(''.join(r))
798 return bytesresponse(''.join(r))
792
799
793 permissions['clonebundles'] = 'pull'
800 @wireprotocommand('clonebundles', '', permission='pull')
794 @wireprotocommand('clonebundles', '')
795 def clonebundles(repo, proto):
801 def clonebundles(repo, proto):
796 """Server command for returning info for available bundles to seed clones.
802 """Server command for returning info for available bundles to seed clones.
797
803
@@ -843,13 +849,12 b' def _capabilities(repo, proto):'
843
849
844 # If you are writing an extension and consider wrapping this function. Wrap
850 # If you are writing an extension and consider wrapping this function. Wrap
845 # `_capabilities` instead.
851 # `_capabilities` instead.
846 permissions['capabilities'] = 'pull'
852 @wireprotocommand('capabilities', permission='pull')
847 @wireprotocommand('capabilities')
848 def capabilities(repo, proto):
853 def capabilities(repo, proto):
849 return bytesresponse(' '.join(_capabilities(repo, proto)))
854 return bytesresponse(' '.join(_capabilities(repo, proto)))
850
855
851 permissions['changegroup'] = 'pull'
856 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY,
852 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY)
857 permission='pull')
853 def changegroup(repo, proto, roots):
858 def changegroup(repo, proto, roots):
854 nodes = decodelist(roots)
859 nodes = decodelist(roots)
855 outgoing = discovery.outgoing(repo, missingroots=nodes,
860 outgoing = discovery.outgoing(repo, missingroots=nodes,
@@ -858,9 +863,9 b' def changegroup(repo, proto, roots):'
858 gen = iter(lambda: cg.read(32768), '')
863 gen = iter(lambda: cg.read(32768), '')
859 return streamres(gen=gen)
864 return streamres(gen=gen)
860
865
861 permissions['changegroupsubset'] = 'pull'
862 @wireprotocommand('changegroupsubset', 'bases heads',
866 @wireprotocommand('changegroupsubset', 'bases heads',
863 transportpolicy=POLICY_V1_ONLY)
867 transportpolicy=POLICY_V1_ONLY,
868 permission='pull')
864 def changegroupsubset(repo, proto, bases, heads):
869 def changegroupsubset(repo, proto, bases, heads):
865 bases = decodelist(bases)
870 bases = decodelist(bases)
866 heads = decodelist(heads)
871 heads = decodelist(heads)
@@ -870,16 +875,15 b' def changegroupsubset(repo, proto, bases'
870 gen = iter(lambda: cg.read(32768), '')
875 gen = iter(lambda: cg.read(32768), '')
871 return streamres(gen=gen)
876 return streamres(gen=gen)
872
877
873 permissions['debugwireargs'] = 'pull'
878 @wireprotocommand('debugwireargs', 'one two *',
874 @wireprotocommand('debugwireargs', 'one two *')
879 permission='pull')
875 def debugwireargs(repo, proto, one, two, others):
880 def debugwireargs(repo, proto, one, two, others):
876 # only accept optional args from the known set
881 # only accept optional args from the known set
877 opts = options('debugwireargs', ['three', 'four'], others)
882 opts = options('debugwireargs', ['three', 'four'], others)
878 return bytesresponse(repo.debugwireargs(one, two,
883 return bytesresponse(repo.debugwireargs(one, two,
879 **pycompat.strkwargs(opts)))
884 **pycompat.strkwargs(opts)))
880
885
881 permissions['getbundle'] = 'pull'
886 @wireprotocommand('getbundle', '*', permission='pull')
882 @wireprotocommand('getbundle', '*')
883 def getbundle(repo, proto, others):
887 def getbundle(repo, proto, others):
884 opts = options('getbundle', gboptsmap.keys(), others)
888 opts = options('getbundle', gboptsmap.keys(), others)
885 for k, v in opts.iteritems():
889 for k, v in opts.iteritems():
@@ -945,14 +949,12 b' def getbundle(repo, proto, others):'
945
949
946 return streamres(gen=chunks, prefer_uncompressed=not prefercompressed)
950 return streamres(gen=chunks, prefer_uncompressed=not prefercompressed)
947
951
948 permissions['heads'] = 'pull'
952 @wireprotocommand('heads', permission='pull')
949 @wireprotocommand('heads')
950 def heads(repo, proto):
953 def heads(repo, proto):
951 h = repo.heads()
954 h = repo.heads()
952 return bytesresponse(encodelist(h) + '\n')
955 return bytesresponse(encodelist(h) + '\n')
953
956
954 permissions['hello'] = 'pull'
957 @wireprotocommand('hello', permission='pull')
955 @wireprotocommand('hello')
956 def hello(repo, proto):
958 def hello(repo, proto):
957 """Called as part of SSH handshake to obtain server info.
959 """Called as part of SSH handshake to obtain server info.
958
960
@@ -967,14 +969,12 b' def hello(repo, proto):'
967 caps = capabilities(repo, proto).data
969 caps = capabilities(repo, proto).data
968 return bytesresponse('capabilities: %s\n' % caps)
970 return bytesresponse('capabilities: %s\n' % caps)
969
971
970 permissions['listkeys'] = 'pull'
972 @wireprotocommand('listkeys', 'namespace', permission='pull')
971 @wireprotocommand('listkeys', 'namespace')
972 def listkeys(repo, proto, namespace):
973 def listkeys(repo, proto, namespace):
973 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
974 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
974 return bytesresponse(pushkeymod.encodekeys(d))
975 return bytesresponse(pushkeymod.encodekeys(d))
975
976
976 permissions['lookup'] = 'pull'
977 @wireprotocommand('lookup', 'key', permission='pull')
977 @wireprotocommand('lookup', 'key')
978 def lookup(repo, proto, key):
978 def lookup(repo, proto, key):
979 try:
979 try:
980 k = encoding.tolocal(key)
980 k = encoding.tolocal(key)
@@ -986,14 +986,12 b' def lookup(repo, proto, key):'
986 success = 0
986 success = 0
987 return bytesresponse('%d %s\n' % (success, r))
987 return bytesresponse('%d %s\n' % (success, r))
988
988
989 permissions['known'] = 'pull'
989 @wireprotocommand('known', 'nodes *', permission='pull')
990 @wireprotocommand('known', 'nodes *')
991 def known(repo, proto, nodes, others):
990 def known(repo, proto, nodes, others):
992 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
991 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
993 return bytesresponse(v)
992 return bytesresponse(v)
994
993
995 permissions['pushkey'] = 'push'
994 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
996 @wireprotocommand('pushkey', 'namespace key old new')
997 def pushkey(repo, proto, namespace, key, old, new):
995 def pushkey(repo, proto, namespace, key, old, new):
998 # compatibility with pre-1.8 clients which were accidentally
996 # compatibility with pre-1.8 clients which were accidentally
999 # sending raw binary nodes rather than utf-8-encoded hex
997 # sending raw binary nodes rather than utf-8-encoded hex
@@ -1014,8 +1012,7 b' def pushkey(repo, proto, namespace, key,'
1014 output = output.getvalue() if output else ''
1012 output = output.getvalue() if output else ''
1015 return bytesresponse('%d\n%s' % (int(r), output))
1013 return bytesresponse('%d\n%s' % (int(r), output))
1016
1014
1017 permissions['stream_out'] = 'pull'
1015 @wireprotocommand('stream_out', permission='pull')
1018 @wireprotocommand('stream_out')
1019 def stream(repo, proto):
1016 def stream(repo, proto):
1020 '''If the server supports streaming clone, it advertises the "stream"
1017 '''If the server supports streaming clone, it advertises the "stream"
1021 capability with a value representing the version and flags of the repo
1018 capability with a value representing the version and flags of the repo
@@ -1023,8 +1020,7 b' def stream(repo, proto):'
1023 '''
1020 '''
1024 return streamres_legacy(streamclone.generatev1wireproto(repo))
1021 return streamres_legacy(streamclone.generatev1wireproto(repo))
1025
1022
1026 permissions['unbundle'] = 'push'
1023 @wireprotocommand('unbundle', 'heads', permission='push')
1027 @wireprotocommand('unbundle', 'heads')
1028 def unbundle(repo, proto, heads):
1024 def unbundle(repo, proto, heads):
1029 their_heads = decodelist(heads)
1025 their_heads = decodelist(heads)
1030
1026
@@ -242,11 +242,7 b' def _callhttp(repo, req, proto, cmd, che'
242 'over HTTP'))
242 'over HTTP'))
243 return []
243 return []
244
244
245 # Assume commands with no defined permissions are writes /
245 checkperm(wireproto.commands[cmd].permission)
246 # for pushes. This is the safest from a security perspective
247 # because it doesn't allow commands with undefined semantics
248 # from bypassing permissions checks.
249 checkperm(wireproto.permissions.get(cmd, 'push'))
250
246
251 rsp = wireproto.dispatch(repo, proto, cmd)
247 rsp = wireproto.dispatch(repo, proto, cmd)
252
248
@@ -21,12 +21,10 b''
21 > @wireproto.wireprotocommand('customwritenoperm')
21 > @wireproto.wireprotocommand('customwritenoperm')
22 > def customwritenoperm(repo, proto):
22 > def customwritenoperm(repo, proto):
23 > return b'write command no defined permissions\n'
23 > return b'write command no defined permissions\n'
24 > wireproto.permissions['customreadwithperm'] = 'pull'
24 > @wireproto.wireprotocommand('customreadwithperm', permission='pull')
25 > @wireproto.wireprotocommand('customreadwithperm')
26 > def customreadwithperm(repo, proto):
25 > def customreadwithperm(repo, proto):
27 > return b'read-only command w/ defined permissions\n'
26 > return b'read-only command w/ defined permissions\n'
28 > wireproto.permissions['customwritewithperm'] = 'push'
27 > @wireproto.wireprotocommand('customwritewithperm', permission='push')
29 > @wireproto.wireprotocommand('customwritewithperm')
30 > def customwritewithperm(repo, proto):
28 > def customwritewithperm(repo, proto):
31 > return b'write command w/ defined permissions\n'
29 > return b'write command w/ defined permissions\n'
32 > EOF
30 > EOF
General Comments 0
You need to be logged in to leave comments. Login now