summaryrefslogtreecommitdiff
path: root/macaroonbakery/checkers/_caveat.py
blob: 5732f43e63cc378190709d0fa59ed1002bbd050e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
import collections

import pyrfc3339
from ._conditions import (
    COND_ALLOW,
    COND_DECLARED,
    COND_DENY,
    COND_ERROR,
    COND_TIME_BEFORE,
    STD_NAMESPACE,
)


class Caveat(collections.namedtuple('Caveat', 'condition location namespace')):
    '''Represents a condition that must be true for a check to complete
    successfully.

    If location is provided, the caveat must be discharged by
    a third party at the given location (a URL string).

    The namespace parameter holds the namespace URI string of the
    condition - if it is provided, it will be converted to a namespace prefix
    before adding to the macaroon.
    '''
    __slots__ = ()

    def __new__(cls, condition, location=None, namespace=None):
        return super(Caveat, cls).__new__(cls, condition, location, namespace)


def declared_caveat(key, value):
    '''Returns a "declared" caveat asserting that the given key is
    set to the given value.

    If a macaroon has exactly one first party caveat asserting the value of a
    particular key, then infer_declared will be able to infer the value, and
    then the check will allow the declared value if it has the value
    specified here.

    If the key is empty or contains a space, it will return an error caveat.
    '''
    if key.find(' ') >= 0 or key == '':
        return error_caveat('invalid caveat \'declared\' key "{}"'.format(key))
    return _first_party(COND_DECLARED, key + ' ' + value)


def error_caveat(f):
    '''Returns a caveat that will never be satisfied, holding f as the text of
    the caveat.

    This should only be used for highly unusual conditions that are never
    expected to happen in practice, such as a malformed key that is
    conventionally passed as a constant. It's not a panic but you should
    only use it in cases where a panic might possibly be appropriate.

    This mechanism means that caveats can be created without error
    checking and a later systematic check at a higher level (in the
    bakery package) can produce an error instead.
    '''
    return _first_party(COND_ERROR, f)


def allow_caveat(ops):
    ''' Returns a caveat that will deny attempts to use the macaroon to perform
    any operation other than those listed. Operations must not contain a space.
    '''
    if ops is None or len(ops) == 0:
        return error_caveat('no operations allowed')
    return _operation_caveat(COND_ALLOW, ops)


def deny_caveat(ops):
    '''Returns a caveat that will deny attempts to use the macaroon to perform
    any of the listed operations. Operations must not contain a space.
    '''
    return _operation_caveat(COND_DENY, ops)


def _operation_caveat(cond, ops):
    ''' Helper for allow_caveat and deny_caveat.

    It checks that all operation names are valid before creating the caveat.
    '''
    for op in ops:
        if op.find(' ') != -1:
            return error_caveat('invalid operation name "{}"'.format(op))
    return _first_party(cond, ' '.join(ops))


def time_before_caveat(t):
    '''Return a caveat that specifies that the time that it is checked at
    should be before t.
    :param t is a a UTC date in - use datetime.utcnow, not datetime.now
    '''

    return _first_party(COND_TIME_BEFORE,
                        pyrfc3339.generate(t, accept_naive=True,
                                           microseconds=True))


def parse_caveat(cav):
    ''' Parses a caveat into an identifier, identifying the checker that should
    be used, and the argument to the checker (the rest of the string).

    The identifier is taken from all the characters before the first
    space character.
    :return two string, identifier and arg
    '''
    if cav == '':
        raise ValueError('empty caveat')
    try:
        i = cav.index(' ')
    except ValueError:
        return cav, ''
    if i == 0:
        raise ValueError('caveat starts with space character')
    return cav[0:i], cav[i + 1:]


def _first_party(name, arg):
    condition = name
    if arg != '':
        condition += ' ' + arg

    return Caveat(condition=condition,
                  namespace=STD_NAMESPACE)