示例#1
0
class Config(BaseModel):
    """
    Represents and defines config for PassTheSalt.
    """

    owner: fields.Optional(fields.Str)
    master: fields.Optional(Master)
示例#2
0
class Algorithm(BaseModel):
    """
    A secret generation algorithm.
    """

    version: fields.Optional(fields.Int, default=1)
    length: fields.Optional(fields.Int)
示例#3
0
class Author(Model):
    """Contains information about an author.
    """
    name: fields.Str()
    surname: fields.Str()
    affiliation: fields.Optional(fields.List(fields.Str()))

    def __init__(self, soup):
        """Creates an `Author` by parsing a `soup` of type
        `BeautifulSoup`.
        """
        if not soup.persname:
            self.name = ""
            self.surname = ""
        else:
            self.name = text(soup.persname.forename)
            self.surname = text(soup.persname.surname)
        # TODO: better affiliation parsing.
        self.affiliation = list(map(text, soup.find_all("affiliation")))

    def __str__(self):
        s = ""
        if self.name:
            s += self.name + " "
        if self.surname:
            s += self.surname

        return s.strip()
示例#4
0
class Generatable(Secret):
    """
    A generatable Secret.
    """

    salt: fields.Str()
    algorithm: fields.Optional(Algorithm, default=Algorithm)

    def display(self):
        """
        A display tuple for this tabulating this secret.

        Returns:
            (str, str, str): the label, the kind, and the salt.
        """
        return super().display() + (self.salt,)

    def get(self):
        """
        Generate the secret value for this Secret.

        Returns:
            str: the secret value.
        """
        return generate(
            self.salt,
            self._pts.master_key,
            version=self.algorithm.version,
            length=self.algorithm.length,
        )
示例#5
0
class Login(Generatable):
    """
    An account login Secret.
    """

    domain: fields.Domain()
    username: fields.Str()
    iteration: fields.Optional(fields.Int)

    @property
    def salt(self):
        """
        The salt for this Generatable secret.

        Returns:
            str: the salt.
        """
        return '|'.join((self.domain, self.username, str(self.iteration or 0)))
示例#6
0
class Config(Model):
    # misc
    mode: fields.Str()
    log_level: fields.Str()
    logger_name: fields.Optional(fields.Str)
    sleep_interval: fields.Float(normalizers=[float])
    keys_base_path: fields.Str()
    path_to_keys: fields.Optional(fields.Str)  # Only used in tests

    # db stuff
    db_name: fields.Str()
    db_uri: fields.Optional(fields.Str)
    db_host: fields.Optional(fields.Str)
    db_password: fields.Optional(fields.Str)
    db_username: fields.Optional(fields.Str)

    # multisig stuff
    signatures_threshold: fields.Int(normalizers=[int])
    signatures_threshold_eth: fields.Optional(fields.Int(normalizers=[int]))
    multisig_wallet_address: fields.Str()  # Ethereum address
    multisig_acc_addr: fields.Str()  # SN address
    multisig_key_name: fields.Str()
    secret_signers: fields.Str()

    # ethereum stuff
    eth_node: fields.Str()
    network: fields.Str()
    eth_start_block: fields.Int(normalizers=[int])
    eth_confirmations: fields.Int(normalizers=[int])

    # eth account stuff
    eth_address: fields.Optional(fields.Str)
    eth_private_key: fields.Optional(fields.Str)
    pkcs11_module: fields.Optional(fields.Str)
    token: fields.Optional(fields.Str)
    user_pin: fields.Optional(fields.Str)
    label: fields.Optional(fields.Str)

    # oracle stuff
    ethgastation_api_key: fields.Optional(fields.Str)

    # secret network stuff
    secretcli_home: fields.Str()
    secret_node: fields.Str()
    enclave_key: fields.Str()
    chain_id: fields.Str()
    scrt_swap_address: fields.Str()
    swap_code_hash: fields.Str()

    # scrt account stuff
    secret_key_file: fields.Str()
    secret_key_name: fields.Str()
    secret_key_password: fields.Optional(fields.Str)

    # warnings
    eth_funds_warning_threshold: fields.Float(normalizers=[float])
    scrt_funds_warning_threshold: fields.Float(normalizers=[float])
示例#7
0
 class Version(Model):
     major = fields.Int()
     minor = fields.Int()
     patch = fields.Optional(fields.Int, default=0)
示例#8
0
 class User(Model):
     name = fields.Str(rename='username', serializers=[lambda s: s.strip()])
     age = fields.Optional(fields.Int)
     addresses = fields.Optional(fields.List(Address))
示例#9
0
class Article(_Article):
    """Represents an academic article or a reference contained in
    an article.

    The data is parsed from a TEI XML file (`from_file()`) or
    directly from a `BeautifulSoup` object.
    """
    title: fields.Str()
    text: fields.Str()
    authors: fields.List(Author)
    year: fields.Optional(fields.Date())
    references: fields.Optional(fields.List(_Article))

    def __init__(self, soup, is_reference=False):
        """Create a new `Article` by parsing a `soup: BeautifulSoup`
        instance.
        The parameter `is_reference` specifies if the `soup` contains
        an entire article or just the content of a reference.
        """
        self.title = text(soup.title)
        self.doi = text(soup.idno)
        self.abstract = text(soup.abstract)
        self.text = soup.text.strip() if soup.text else ""
        # FIXME
        self.year = None

        if is_reference:
            self.authors = list(map(Author, soup.find_all("author")))
            self.references = []
        else:
            self.authors = list(map(Author, soup.analytic.find_all("author")))
            self.references = self._parse_biblio(soup)

    @staticmethod
    def from_file(tei_file):
        """Creates an `Article` by parsing a TEI XML file.
        """
        with open(tei_file) as f:
            soup = BeautifulSoup(f, "lxml")

        return Article(soup)

    def _parse_biblio(self, soup):
        """Parses the bibliography from an article.
        """
        references = []
        # NOTE: we could do this without the regex.
        bibs = soup.find_all("biblstruct", {"xml:id": re.compile(r"b[0-9]*")})

        for bib in bibs:
            if bib.analytic:
                references.append(Article(bib.analytic, is_reference=True))
                # NOTE: in this case, bib.monogr contains more info
                # about the manuscript where the paper was published.
                # Not parsing for now.
            elif bib.monogr:
                references.append(Article(bib.monogr, is_reference=True))
            else:
                print(f"Could not parse reference from {bib}")

        return references

    def __str__(self):
        return f"'{self.title}' - {' '.join(map(str, self.authors))}"

    def summary(self):
        """Prints a human-readable summary.
        """
        print(f"Title: {self.title}")
        print("Authors: " + ", ".join(map(str, self.authors)))
        if self.references:
            print("References:")
            for r in self.references:
                r.summary()
                print("-------------------")
示例#10
0
 class Example(Model):
     a = fields.Optional(fields.Int)
示例#11
0
 class Example(Model):
     a = fields.Optional(fields.Int, default=5)
示例#12
0
 class Example(Model):
     a = fields.Int()
     b = fields.Optional(fields.Bool)
示例#13
0
class PassTheSalt(Model):
    """
    An object to store and manage Secrets.

    A PassTheSalt represents and defines a deterministic password generation and
    password storage system.
    """

    config: fields.Optional(Config, default=Config)
    secrets: fields.Optional(fields.Dict(fields.Str, Secret), default=dict)
    secrets_encrypted: fields.Optional(fields.Str)
    version: fields.Optional(
        fields.Literal(MAJOR_VERSION),
        default=MAJOR_VERSION,
        normalizers=[major_version],
    )

    def __init__(self, *args, **kwargs):
        """
        Create a new PassTheSalt.
        """
        super().__init__(*args, **kwargs)
        self._master = None

    @classmethod
    def from_dict(cls, d):
        """
        Create a PassTheSalt object from a dictionary.

        Args:
            d (dict): the input dictionary.

        Returns:
            PassTheSalt: a new PassTheSalt object.
        """
        pts = super().from_dict(d)

        # Add the current context to each Secret.
        for label, secret in pts.secrets.items():
            secret.add_context(label, pts)

        return pts

    def save(self, dict=None, **kwargs):
        """
        Write this PassTheSalt store to the configured path.

        Args:
            dict (type): the class of the deserialized dictionary. This defaults
                to an `OrderedDict` so that the fields will be returned in the
                order they were defined on the Model.
            **kwargs: extra keyword arguments passed directly to `json.dumps()`.
        """
        self.to_path(self.path, dict=dict, **kwargs)

    def with_master(self, master):
        """
        Configure PassTheSalt with a master password.

        Args:
            master: the master password for generating and encrypting secrets.
                This can be a callback for getting the password (for example
                through user input), or the actual master password as a string.

        Returns:
            PassTheSalt: this object.
        """
        self._master = master
        return self

    def with_path(self, path):
        """
        Configure PassTheSalt with a default path.

        Args:
            path (str): the default path to read and write to.

        Returns:
            PassTheSalt: this object.
        """
        self._path = path
        return self

    @property
    def master_key(self):
        """
        Return the master key.

        This is  constructed from the master password and the configured owner.

        Returns:
            str: the master key.
        """
        if self._master is None:
            raise ConfigurationError('no master password is configured')

        if callable(self._master):
            self._master = self._master(self)

        key = []

        if self.config.owner:
            key.append(self.config.owner)

        key.append(self._master)

        return '|'.join(key)

    @property
    def path(self):
        """
        Return the configured path if it is set.

        Returns:
            str: the configured path.

        Raises:
            `ConfigurationError`: when there is no configured path.
        """
        try:
            return self._path
        except AttributeError:
            raise ConfigurationError('no default path is configured')

    def labels(self, pattern=None):
        """
        Return the list of labels for secrets.

        This list can be optionally filtered with a regex pattern.

        Args:
            pattern (str): filter labels with a regex pattern.

        Returns:
            list: a list of labels matching the given pattern and prefix.

        Raises:
            LabelError: when the given pattern is an invalid regex expression.
        """
        labels = self.secrets.keys()

        if pattern:
            try:
                regex = re.compile(pattern)
            except re.error:
                raise LabelError(f'{pattern!r} is an invalid regex expression')

            labels = filter(regex.match, labels)

        return list(labels)

    def resolve(self, pattern):
        """
        Resolve a pattern and prefix to a single label.

        Args:
            pattern (str): filter labels with a regex pattern.

        Returns:
            str: the actual label of the secret.

        Raises:
            LabelError: if the pattern does not match any labels or multiple
                labels are matched.
        """
        if self.contains(pattern):
            return pattern

        matches = self.labels(pattern=pattern)

        if len(matches) == 1:
            return matches[0]
        elif not matches:
            raise LabelError(f'unable to resolve pattern {pattern!r}')
        else:
            raise LabelError(f'pattern {pattern!r} matches multiple secrets')

    def contains(self, label):
        """
        Whether the label exists.

        Args:
            label (str): the label for the secret.

        Returns:
            bool: True if the label exists else False.
        """
        return label in self.secrets

    def add(self, label, secret):
        """
        Add a secret to PassTheSalt.

        Args:
            label (str): the label for the secret.
            secret (Secret): the secret to add.
        """
        if self.contains(label):
            raise LabelError(f'{label!r} already exists')

        secret.add_context(label, self)
        secret.add()
        self.secrets[label] = secret
        self.touch()

    def get(self, label):
        """
        Retrieve a secret.

        Args:
            label (str): the label for the secret.

        Returns:
            Secret: the secret corresponding to the label.
        """
        return self.secrets[label]

    def pop(self, label):
        """
        Remove a secret and return the removed secret.

        Args:
            label (str): the label for the secret.

        Returns:
            Secret: the secret corresponding to the label.
        """
        try:
            secret = self.secrets.pop(label)
        except KeyError:
            raise LabelError(f'{label!r} does not exist')

        secret.remove()
        secret.remove_context()
        self.touch()

        return secret

    def remove(self, label):
        """
        Remove a secret.

        Args:
            label (str): the label for the secret.
        """
        self.pop(label)

    def move(self, label, new_label):
        """
        Rename a secret.

        Args:
            label (str): the label for the secret.
            new_label (str): the new label for the secret.
        """
        if self.contains(new_label):
            raise LabelError(f'{new_label!r} already exists')

        self.add(new_label, self.pop(label))

    def update(self, label, secret):
        """
        Update secret.

        Args:
            label (str): the label for the secret.
            secret (Secret): the secret to update with.
        """
        if self.contains(label):
            self.remove(label)

        self.add(label, secret)

    def _diff(self, other):
        """
        Return the difference between this store and the other.

        The returned PassTheSalt store contains everything in the current store
        that is not present in the other, and anything that is not equal.

        Warning: this method is private API for a reason. The returned
        PassTheSalt store is not usable as a store.

        Args:
            other (PassTheSalt): the store to compare with.

        Returns:
            PassTheSalt: a store with all the extra / missing secrets.
        """
        diff = PassTheSalt()

        for label in self.labels():
            if label not in other.labels() or self.get(label) != other.get(label):
                diff.secrets[label] = self.get(label)

        return diff
示例#14
0
class Model(BaseModel):
    """
    A custom Model that has a modified Field.
    """

    modified: fields.Optional(DateTime, default=datetime.datetime.utcnow)

    def to_base64(self, **kwargs):
        """
        Dump the model as a JSON string and base64 encode it.

        Args:
            **kwargs: extra keyword arguments to pass directly to `json.dumps`.

        Returns:
            str: a base64 encoded representation of this model.
        """
        return b64encode(self.to_json(**kwargs).encode()).decode('ascii')

    def to_toml(self, **kwargs):
        """
        Dump the model as a TOML string.

        Args:
            **kwargs: extra keyword arguments to pass directly to `toml.dumps`.

        Returns:
            str: a TOML representation of this model.
        """
        return toml.dumps(self.to_dict(), **kwargs)

    def to_path(self, p, dict=None, **kwargs):
        """
        Dump the model to a file path.

        Args:
            p (str): the file path to write to.
            **kwargs: extra keyword arguments to pass directly to `json.dumps`.
        """
        with open(p, 'w') as f:
            f.write(self.to_json(**kwargs))

    @classmethod
    def from_base64(cls, s, **kwargs):
        """
        Load the model from a base64 encoded string.

        Args:
            s (str): the JSON string.
            **kwargs: extra keyword arguments to pass directly to `json.loads`.

        Returns:
            Model: an instance of this model.            .
        """
        return cls.from_json(b64decode(s.encode()).decode('utf-8'), **kwargs)

    @classmethod
    def from_toml(cls, s, **kwargs):
        """
        Load the model from a TOML string.

        Args:
            s (str): the TOML string.
            **kwargs: extra keyword arguments to pass directly to `toml.loads`.

        Returns:
            Model: an instance of this model.
        """
        return cls.from_dict(toml.loads(s, **kwargs))

    @classmethod
    def from_path(cls, p, **kwargs):
        """
        Load the model from a file path.

        Args:
            p (str): the file path to read from.
            **kwargs: extra keyword arguments to pass directly to `json.loads`.

        Returns:
            Model: an instance of this model.            .
        """
        with open(p) as f:
            return cls.from_json(f.read(), **kwargs)

    def touch(self):
        """
        Update the modified time of this object to the current UTC time.
        """
        self.modified = datetime.datetime.utcnow()
示例#15
0
class Stow(Remote):
    """
    Stow configuration for a remote store.

    See https://github.com/rossmacarthur/stow.
    """

    token: fields.Optional(fields.Str)
    token_location: fields.Url()

    @property
    def headers(self):
        """
        General headers to use when making requests to the remote server.

        Returns:
            dict: the headers.
        """
        return {'Content-Type': 'application/json'}

    def validate_response(self, response):
        """
        Validate the given response.

        Args:
            response (Response): the requests response.

        Raises:
            UnauthorizedAccess: when the response code is 401.
            ConflictingTimestamps: when the response code is 409.
        """
        try:
            message = response.json().get('message')
        except json.decoder.JSONDecodeError:
            message = None

        if response.status_code == 401:
            raise UnauthorizedAccess(message)
        elif response.status_code == 409:
            raise ConflictingTimestamps(message)

        super().validate_response(response)

    def renew(self):
        """
        Renew the internal token with the configured authentication.
        """
        data = self.request('GET',
                            self.token_location,
                            headers=self.headers,
                            auth=self.auth).json()
        self.token = data['token']
        self.touch()

    def handle_renew(self, f):
        """
        Decorator to recall a function if the token needs to be renewed.

        If the given function raises UnauthorizedAccess then renew the token and
        recall the function.

        Args:
            f (callable): the function that accesses some secure resource.

        Returns:
            the result of the function.
        """
        def decorated_function(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except UnauthorizedAccess:
                self.renew()
                return f(*args, **kwargs)

        return decorated_function

    def get(self):
        """
        Retrieve the Remote store.

        Returns:
            PassTheSalt: a PassTheSalt instance.
        """
        @self.handle_renew
        def get():
            return self.request('GET',
                                self.location,
                                headers=self.headers,
                                auth=(self.token, 'unused')).json()

        return PassTheSalt.from_base64(get()['value'])

    def put(self, pts, force=False):
        """
        Upload the given PassTheSalt to the Remote store.

        Args:
            pts (PassTheSalt): a PassTheSalt instance.
            force (bool): whether to ignore any conflicts.

        Returns:
            dict: the message from the server.
        """
        @self.handle_renew
        def put(pts):
            payload = {'value': pts.to_base64()}

            if not force:
                payload['modified'] = pts.modified.isoformat()

            data = json.dumps(payload)
            return self.request(
                'PUT',
                self.location,
                headers=self.headers,
                auth=(self.token, 'unused'),
                data=data,
            )

        try:
            message = put(pts).json().get('message')
        except json.decoder.JSONDecodeError:
            message = None

        return message