├── .codecov.yml
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── cmd
├── createsuperuser
│ ├── main.go
│ └── main_test.go
└── management
│ └── main.go
├── config
├── config.go
└── config_test.go
├── crypt
├── crypto.go
└── crypto_test.go
├── datastore
├── datastore.go
├── entity.go
├── memory
│ ├── memory.go
│ ├── noncestore.go
│ ├── noncestore_test.go
│ ├── organization.go
│ ├── organization_test.go
│ ├── user.go
│ └── user_test.go
└── postgres
│ ├── nonce.go
│ ├── nonce_sql.go
│ ├── organization.go
│ ├── organization_sql.go
│ ├── postgres.go
│ ├── user.go
│ └── user_sql.go
├── docs
└── IoTManagement.svg
├── domain
└── entity.go
├── go.mod
├── go.sum
├── identityapi
├── device.go
├── identityapi.go
├── organization.go
├── organization_test.go
└── testing_identityapi.go
├── k8s-management.yaml
├── k8s-postgres.yaml
├── run-checks
├── service
├── factory
│ ├── factory.go
│ └── factory_test.go
└── manage
│ ├── action.go
│ ├── device.go
│ ├── device_test.go
│ ├── group.go
│ ├── group_test.go
│ ├── manage.go
│ ├── organization.go
│ ├── organization_test.go
│ ├── registry.go
│ ├── registry_test.go
│ ├── snap.go
│ ├── snap_test.go
│ ├── testing_manage.go
│ ├── user.go
│ └── user_test.go
├── static
├── app.html
├── css
│ ├── main.c040df10.chunk.css
│ └── main.c040df10.chunk.css.map
├── font-awesome
│ ├── css
│ │ └── all.min.css
│ └── webfonts
│ │ ├── fa-brands-400.eot
│ │ ├── fa-brands-400.svg
│ │ ├── fa-brands-400.ttf
│ │ ├── fa-brands-400.woff
│ │ ├── fa-brands-400.woff2
│ │ ├── fa-regular-400.eot
│ │ ├── fa-regular-400.svg
│ │ ├── fa-regular-400.ttf
│ │ ├── fa-regular-400.woff
│ │ ├── fa-regular-400.woff2
│ │ ├── fa-solid-900.eot
│ │ ├── fa-solid-900.svg
│ │ ├── fa-solid-900.ttf
│ │ ├── fa-solid-900.woff
│ │ └── fa-solid-900.woff2
├── images
│ ├── ajax-loader.gif
│ ├── checkbox_checked_16.png
│ ├── checkbox_unchecked_16.png
│ ├── chevron-down.png
│ ├── chevron-up.png
│ ├── favicon.ico
│ ├── logo-ubuntu-black.svg
│ ├── logo-ubuntu-white.svg
│ └── navigation-menu-plain.svg
└── js
│ ├── 2.3f69199a.chunk.js
│ ├── 2.3f69199a.chunk.js.LICENSE.txt
│ ├── 2.3f69199a.chunk.js.map
│ ├── main.8ca24fda.chunk.js
│ ├── main.8ca24fda.chunk.js.map
│ ├── runtime-main.00ed32f5.js
│ └── runtime-main.00ed32f5.js.map
├── testing
└── memory.yaml
├── twinapi
├── action.go
├── action_test.go
├── device.go
├── device_test.go
├── group.go
├── group_test.go
├── snap.go
├── snap_test.go
├── testing_twinapi.go
└── twinapi.go
├── web
├── auth.go
├── handlers_app.go
├── handlers_app_test.go
├── handlers_devices.go
├── handlers_devices_test.go
├── handlers_groups.go
├── handlers_groups_test.go
├── handlers_organization.go
├── handlers_organization_test.go
├── handlers_registry.go
├── handlers_registry_test.go
├── handlers_snaps.go
├── handlers_snaps_test.go
├── handlers_store.go
├── handlers_store_test.go
├── handlers_users.go
├── handlers_users_test.go
├── login.go
├── login_test.go
├── middleware.go
├── response.go
├── router.go
├── usso
│ ├── constants.go
│ ├── jwt.go
│ ├── jwt_test.go
│ ├── openid.go
│ └── openid_test.go
├── web.go
└── web_test.go
└── webapp
├── .env
├── .gitignore
├── build.sh
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.js
├── App.test.js
├── components
├── AccountEdit.js
├── Accounts.js
├── Actions.js
├── AlertBox.js
├── Constants.js
├── Device.js
├── DeviceSnaps.js
├── Devices.js
├── DialogBox.js
├── Footer.js
├── GroupEdit.js
├── Groups.js
├── Header.js
├── If.js
├── Index.js
├── Messages.js
├── Navigation.js
├── NavigationUser.js
├── Pagination.js
├── Register.js
├── RegisterEdit.js
├── SnapDialogBox.js
├── UserEdit.js
├── Users.js
└── Utils.js
├── index.js
├── models
├── api.js
└── constants.js
└── sass
└── App.scss
/.codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - "testing_*.go"
3 | - "./**/testing_*.go"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | main
3 | vendor/*/
4 | settings.yaml
5 | main
6 | .coverage/
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go_import_path: github.com/canonical/iot-management
3 | go:
4 | - 1.13
5 | env:
6 | matrix:
7 | - TEST_SUITE="--static"
8 | - TEST_SUITE="--unit"
9 |
10 | before_install:
11 | - go get golang.org/x/lint/golint
12 |
13 | install:
14 | - echo $GOPATH
15 | - echo "Remaining install is done by the test script."
16 | - true
17 | script: sh -v ./run-checks $TEST_SUITE
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.13 as builder1
2 | COPY . ./src/github.com/canonical/iot-management
3 | WORKDIR /go/src/github.com/canonical/iot-management
4 | RUN CGO_ENABLED=1 GOOS=linux go build -a -o /go/bin/management -ldflags='-extldflags "-static"' cmd/management/main.go
5 | RUN CGO_ENABLED=1 GOOS=linux go build -a -o /go/bin/createsuperuser -ldflags='-extldflags "-static"' cmd/createsuperuser/main.go
6 |
7 |
8 | FROM node:8-alpine as builder2
9 | COPY webapp .
10 | WORKDIR /
11 | RUN npm install
12 | RUN npm rebuild node-sass
13 | RUN npm run build
14 |
15 | # Set params from the environment variables
16 | ARG DRIVER="postgres"
17 | ARG DATASOURCE="dbname=management sslmode=disable"
18 | ARG HOST="management:8010"
19 | ARG SCHEME="http"
20 | ARG DEVICETWINAPI="http://devicetwin:8040/v1/"
21 | ARG STOREURL="https://api.snapcraft.io/api/v1/"
22 | ENV DRIVER="${DRIVER}"
23 | ENV DATASOURCE="${DATASOURCE}"
24 | ENV HOST="${HOST}"
25 | ENV SCHEME="${SCHEME}"
26 | ENV DEVICETWINAPI="${DEVICETWINAPI}"
27 | ENV STOREURL="${STOREURL}"
28 |
29 | # Copy the built applications to the docker image
30 | FROM ubuntu:18.04
31 | WORKDIR /root/
32 | RUN apt-get update
33 | RUN apt-get install -y ca-certificates
34 | COPY --from=builder1 /go/bin/management .
35 | COPY --from=builder1 /go/bin/createsuperuser .
36 | COPY --from=builder1 /go/src/github.com/canonical/iot-management/static ./static
37 | COPY --from=builder2 build/static/css ./static/css
38 | COPY --from=builder2 build/static/js ./static/js
39 | COPY --from=builder2 build/index.html ./static/app.html
40 | EXPOSE 8010
41 | ENTRYPOINT ./management
42 | CMD ['./createsuperuser']
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![Build Status][travis-image]][travis-url]
2 | [![Go Report Card][goreportcard-image]][goreportcard-url]
3 | [![codecov][codecov-image]][codecov-url]
4 | # IoT Management Service
5 |
6 | The Management service is the end-user web interface to monitor and manage IoT devices.
7 | The service integrates with the [IoT Identity](https://github.com/canonical/iot-identity) and
8 | [IoT Device Twin](https://github.com/canonical/iot-devicetwin) services to provide device management
9 | for Ubuntu devices.
10 |
11 |
12 | ## Design
13 | 
14 |
15 | ## Build
16 | The project uses go module and it is recommended to use go 1.13.
17 | ```bash
18 | $ go get github.com/canonical/iot-management
19 | $ cd iot-management
20 | $ go build ./...
21 | ```
22 |
23 | ## Run
24 | ```bash
25 | go run cmd/management/main.go
26 | ```
27 |
28 | The service uses a settings.yaml file for configuration.
29 |
30 | ## Contributing
31 | Before contributing you should sign [Canonical's contributor agreement](https://www.ubuntu.com/legal/contributors), it’s the easiest way for you to give us permission to use your contributions.
32 |
33 | [travis-image]: https://travis-ci.org/canonical/iot-management.svg?branch=master
34 | [travis-url]: https://travis-ci.org/canonical/iot-management
35 | [goreportcard-image]: https://goreportcard.com/badge/github.com/canonical/iot-management
36 | [goreportcard-url]: https://goreportcard.com/report/github.com/canonical/iot-management
37 | [codecov-url]: https://codecov.io/gh/canonical/iot-management
38 | [codecov-image]: https://codecov.io/gh/canonical/iot-management/branch/master/graph/badge.svg
39 |
--------------------------------------------------------------------------------
/cmd/createsuperuser/main.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package main
21 |
22 | import (
23 | "flag"
24 | "fmt"
25 | "log"
26 | "os"
27 |
28 | "github.com/canonical/iot-management/config"
29 | "github.com/canonical/iot-management/datastore"
30 | "github.com/canonical/iot-management/service/factory"
31 | )
32 |
33 | var username, name, email string
34 |
35 | func main() {
36 | // Parse the command line arguments
37 | settings, err := config.Config(config.GetPath())
38 | if err != nil {
39 | log.Fatalf("Error parsing the config file: %v", err)
40 | }
41 |
42 | // Open the connection to the local database
43 | db, err := factory.CreateDataStore(settings)
44 | if err != nil {
45 | log.Fatalf("Error accessing data store: %v", settings.Driver)
46 | }
47 |
48 | // Get the command line parameters
49 | parseFlags()
50 |
51 | // Create the user
52 | err = run(db, username, name, email)
53 | if err != nil {
54 | fmt.Println("Error creating user:", err.Error())
55 | os.Exit(1)
56 | }
57 | }
58 |
59 | func run(db datastore.DataStore, username, name, email string) error {
60 | if len(username) == 0 {
61 | return fmt.Errorf("the username must be supplied")
62 | }
63 |
64 | // Create the user
65 | user := datastore.User{
66 | Username: username,
67 | Name: name,
68 | Email: email,
69 | Role: datastore.Superuser,
70 | }
71 | _, err := db.CreateUser(user)
72 | return err
73 | }
74 |
75 | var parseFlags = func() {
76 | flag.StringVar(&username, "username", "", "Ubuntu SSO username of the user (https://login.ubuntu.com/)")
77 | flag.StringVar(&name, "name", "Super User", "Full name of the user")
78 | flag.StringVar(&email, "email", "user@example.com", "Email address of the user")
79 | flag.Parse()
80 | }
81 |
--------------------------------------------------------------------------------
/cmd/createsuperuser/main_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package main
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/canonical/iot-management/datastore/memory"
26 | )
27 |
28 | func Test_run_success(t *testing.T) {
29 | tests := []struct {
30 | name string
31 | user string
32 | wantErr bool
33 | }{
34 | {"valid", "john", false},
35 | {"valid-no-user", "", true},
36 | }
37 | for _, tt := range tests {
38 | t.Run(tt.name, func(t *testing.T) {
39 | db := memory.NewStore()
40 | err := run(db, tt.user, "User", "j@example.com")
41 | if (err != nil) != tt.wantErr {
42 | t.Errorf("CreateSuperUser.Run() error = %v, wantErr %v", err, tt.wantErr)
43 | }
44 | })
45 | }
46 | }
47 |
48 | func Test_main(t *testing.T) {
49 | type args struct {
50 | user string
51 | }
52 | tests := []struct {
53 | name string
54 | args args
55 | }{
56 | {"valid", args{"john"}},
57 | }
58 | for _, tt := range tests {
59 | t.Run(tt.name, func(t *testing.T) {
60 | parseFlags = func() {}
61 | username = tt.args.user
62 | main()
63 | })
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/cmd/management/main.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package main
21 |
22 | import (
23 | "log"
24 |
25 | "github.com/canonical/iot-management/config"
26 | "github.com/canonical/iot-management/identityapi"
27 | "github.com/canonical/iot-management/service/factory"
28 | "github.com/canonical/iot-management/service/manage"
29 | "github.com/canonical/iot-management/twinapi"
30 | "github.com/canonical/iot-management/web"
31 | )
32 |
33 | func main() {
34 | // Parse the command line arguments
35 | log.Println("Open config file", config.GetPath())
36 | settings, err := config.Config(config.GetPath())
37 | if err != nil {
38 | log.Fatalf("Error parsing the config file: %v", err)
39 | }
40 |
41 | // Open the connection to the local database
42 | db, err := factory.CreateDataStore(settings)
43 | if err != nil {
44 | log.Fatalf("Error accessing data store: %v", settings.Driver)
45 | }
46 |
47 | // Initialize the device twin client
48 | twinAPI, err := twinapi.NewClientAdapter(settings.DeviceTwinAPIUrl)
49 | if err != nil {
50 | log.Fatalf("Error connecting to the device twin service: %v", err)
51 | }
52 |
53 | // Initialize the identity client
54 | idAPI, err := identityapi.NewClientAdapter(settings.IdentityAPIUrl)
55 | if err != nil {
56 | log.Fatalf("Error connecting to the identity service: %v", err)
57 | }
58 |
59 | // Create the main services
60 | srv := manage.NewManagement(settings, db, twinAPI, idAPI)
61 |
62 | // Start the web service
63 | www := web.NewService(settings, srv)
64 | log.Fatal(www.Run())
65 | }
66 |
--------------------------------------------------------------------------------
/config/config_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package config
21 |
22 | import (
23 | "os"
24 | "testing"
25 | )
26 |
27 | func TestReadConfig(t *testing.T) {
28 | settings, err := Config("../testing/memory.yaml")
29 | if err != nil {
30 | t.Errorf("Error reading config file: %v", err)
31 | }
32 | if len(settings.JwtSecret) == 0 {
33 | t.Errorf("Error generating JWT secret: %v", err)
34 | }
35 | }
36 |
37 | func TestReadConfigNew(t *testing.T) {
38 | settings, err := Config("./settings.yaml")
39 | if err != nil {
40 | t.Errorf("Error reading config file: %v", err)
41 | }
42 | if len(settings.JwtSecret) == 0 {
43 | t.Errorf("Error generating JWT secret: %v", err)
44 | }
45 | _ = os.Remove("./settings.yaml")
46 | }
47 |
--------------------------------------------------------------------------------
/crypt/crypto.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package crypt
21 |
22 | import (
23 | "crypto/rand"
24 | "encoding/base64"
25 | "regexp"
26 | )
27 |
28 | // RegexpAlpha is a regex pattern for alphabetic characters only
29 | var RegexpAlpha = regexp.MustCompile("[^a-zA-Z]+")
30 |
31 | // RegexpAlphanumeric is a regex pattern for alphabetic/numeric characters only
32 | var RegexpAlphanumeric = regexp.MustCompile("[^a-zA-Z0-9]+")
33 |
34 | // CreateSecret generates a secret that can be used for encryption
35 | func CreateSecret(length int) (string, error) {
36 | rb := make([]byte, length)
37 | _, err := rand.Read(rb)
38 | if err != nil {
39 | return "", err
40 | }
41 |
42 | return RegexpAlphanumeric.ReplaceAllString(base64.URLEncoding.EncodeToString(rb), ""), nil
43 | }
44 |
--------------------------------------------------------------------------------
/crypt/crypto_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package crypt
21 |
22 | import "testing"
23 |
24 | func TestCreateSecret(t *testing.T) {
25 | type args struct {
26 | length int
27 | }
28 | tests := []struct {
29 | name string
30 | args args
31 | wantLen int
32 | wantErr bool
33 | }{
34 | {"valid-10", args{10}, 10, false},
35 | {"valid-20", args{20}, 20, false},
36 | }
37 | for _, tt := range tests {
38 | t.Run(tt.name, func(t *testing.T) {
39 | got, err := CreateSecret(tt.args.length)
40 | if (err != nil) != tt.wantErr {
41 | t.Errorf("CreateSecret() error = %v, wantErr %v", err, tt.wantErr)
42 | return
43 | }
44 | if len(got) < tt.wantLen {
45 | t.Errorf("CreateSecret() length = %v, want min. %v", len(got), tt.wantLen)
46 | }
47 | })
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/datastore/datastore.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package datastore
21 |
22 | import (
23 | "github.com/juju/usso/openid"
24 | )
25 |
26 | // DataStore is the interfaces for the data repository
27 | type DataStore interface {
28 | OpenIDNonceStore() openid.NonceStore
29 | CreateUser(user User) (int64, error)
30 | GetUser(username string) (User, error)
31 | UserList() ([]User, error)
32 | UserUpdate(user User) error
33 | UserDelete(username string) error
34 |
35 | OrgUserAccess(orgID, username string, role int) bool
36 | OrganizationsForUser(username string) ([]Organization, error)
37 | OrganizationForUserToggle(orgID, username string) error
38 | OrganizationGet(orgID string) (Organization, error)
39 | OrganizationCreate(org Organization) error
40 | OrganizationUpdate(org Organization) error
41 | }
42 |
--------------------------------------------------------------------------------
/datastore/entity.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package datastore
21 |
22 | import "time"
23 |
24 | // Available user roles:
25 | //
26 | // * Invalid: default value set in case there is no authentication previous process for this user and thus not got a valid role.
27 | // * Standard: role for regular users. This is the less privileged role
28 | // * Admin: role for createsuperuser users, including standard role permissions but not superuser ones
29 | // * Superuser: role for users having all the permissions
30 | const (
31 | Invalid = iota // 0
32 | Standard = 100 * iota // 100
33 | Admin // 200
34 | Superuser // 300
35 | )
36 |
37 | // User holds user personal, authentication and authorization info
38 | type User struct {
39 | ID int64
40 | Username string
41 | Name string
42 | Email string
43 | Role int
44 | }
45 |
46 | // OpenidNonceMaxAge is the maximum age of stored nonces. Any nonces older
47 | // than this will automatically be rejected. Stored nonces older
48 | // than this will periodically be purged from the database.
49 | const OpenidNonceMaxAge = MaxNonceAgeInSeconds * time.Second
50 |
51 | // MaxNonceAgeInSeconds is the nonce age
52 | const MaxNonceAgeInSeconds = 60
53 |
54 | // OpenidNonce holds the details of the nonce, combining a timestamp and random text
55 | type OpenidNonce struct {
56 | ID int64
57 | Nonce string
58 | Endpoint string
59 | TimeStamp int64
60 | }
61 |
62 | // Organization holds details of the organization
63 | type Organization struct {
64 | OrganizationID string
65 | Name string
66 | }
67 |
68 | // OrganizationUser holds links a user and organization
69 | type OrganizationUser struct {
70 | OrganizationID string
71 | Username string
72 | }
73 |
--------------------------------------------------------------------------------
/datastore/memory/memory.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package memory
21 |
22 | import (
23 | "sync"
24 |
25 | "github.com/canonical/iot-management/datastore"
26 | "github.com/juju/usso/openid"
27 | )
28 |
29 | // Store implements an in-memory store for testing
30 | type Store struct {
31 | lock sync.RWMutex
32 | Users []datastore.User
33 | Orgs []datastore.Organization
34 | OrgUsers []datastore.OrganizationUser
35 | }
36 |
37 | // NewStore creates a new memory store
38 | func NewStore() *Store {
39 | return &Store{
40 | Users: []datastore.User{
41 | {Username: "jamesj", Name: "JJ", Role: 300},
42 | },
43 | Orgs: []datastore.Organization{{OrganizationID: "abc", Name: "Example Org"}},
44 | OrgUsers: []datastore.OrganizationUser{{OrganizationID: "abc", Username: "jamesj"}},
45 | }
46 | }
47 |
48 | // OpenIDNonceStore returns an openid nonce store
49 | func (mem *Store) OpenIDNonceStore() openid.NonceStore {
50 | return &NonceStore{DB: mem}
51 | }
52 |
53 | // createOpenidNonce stores a new nonce entry
54 | func (mem *Store) createOpenidNonce(nonce datastore.OpenidNonce) error {
55 | // Delete the expired nonce
56 | // Create the nonce in the database
57 | return nil
58 | }
59 |
--------------------------------------------------------------------------------
/datastore/memory/noncestore.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package memory
21 |
22 | import (
23 | "fmt"
24 | "time"
25 |
26 | "github.com/canonical/iot-management/datastore"
27 | "gopkg.in/errgo.v1"
28 | )
29 |
30 | // NonceStore is a nonce store backed by database
31 | type NonceStore struct {
32 | DB *Store
33 | }
34 |
35 | // Accept implements openid.NonceStore.Accept
36 | func (s *NonceStore) Accept(endpoint, nonce string) error {
37 | return s.accept(endpoint, nonce, time.Now())
38 | }
39 |
40 | // accept is the implementation of Accept. The third parameter is the
41 | // current time, useful for testing.
42 | func (s *NonceStore) accept(endpoint, nonce string, now time.Time) error {
43 | // From the openid specification:
44 | //
45 | // openid.response_nonce
46 | //
47 | // Value: A string 255 characters or less in length, that MUST be
48 | // unique to this particular successful authentication response.
49 | // The nonce MUST start with the current time on the server, and
50 | // MAY contain additional ASCII characters in the range 33-126
51 | // inclusive (printable non-whitespace characters), as necessary
52 | // to make each response unique. The date and time MUST be
53 | // formatted as specified in section 5.6 of [RFC3339], with the
54 | // following restrictions:
55 | //
56 | // + All times must be in the UTC timezone, indicated with a "Z".
57 | //
58 | // + No fractional seconds are allowed
59 | //
60 | // For example: 2005-05-15T17:11:51ZUNIQUE
61 |
62 | if len(nonce) < 20 {
63 | return fmt.Errorf("%q does not contain a valid timestamp", nonce)
64 | }
65 | t, err := time.Parse(time.RFC3339, nonce[:20])
66 | if err != nil {
67 | return fmt.Errorf("%q does not contain a valid timestamp: %v", nonce, err)
68 | }
69 |
70 | // Check if the nonce has expired
71 | diff := now.Sub(t)
72 | if diff > datastore.OpenidNonceMaxAge {
73 | return fmt.Errorf("%q too old", nonce)
74 | }
75 |
76 | openidNonce := datastore.OpenidNonce{Nonce: nonce, Endpoint: endpoint, TimeStamp: t.Unix()}
77 | err = s.DB.createOpenidNonce(openidNonce)
78 | return errgo.Mask(err)
79 | }
80 |
--------------------------------------------------------------------------------
/datastore/memory/noncestore_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package memory
21 |
22 | import (
23 | "testing"
24 | "time"
25 | )
26 |
27 | func TestNonceStore_Accept(t *testing.T) {
28 | t1 := time.Now().UTC().Format(time.RFC3339)
29 | t2 := time.Now().AddDate(-2, 0, 0).UTC().Format(time.RFC3339)
30 | type args struct {
31 | endpoint string
32 | nonce string
33 | }
34 | tests := []struct {
35 | name string
36 | args args
37 | wantErr bool
38 | }{
39 | {"valid", args{"/login", t1}, false},
40 | {"invalid-short", args{"/login", "1234"}, true},
41 | {"invalid-value", args{"/login", "12345678901234567890"}, true},
42 | {"invalid-expired", args{"/login", t2}, true},
43 | }
44 | for _, tt := range tests {
45 | t.Run(tt.name, func(t *testing.T) {
46 | mem := NewStore()
47 | s := mem.OpenIDNonceStore()
48 | if err := s.Accept(tt.args.endpoint, tt.args.nonce); (err != nil) != tt.wantErr {
49 | t.Errorf("NonceStore.Accept() error = %v, wantErr %v", err, tt.wantErr)
50 | }
51 | })
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/datastore/memory/user.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package memory
21 |
22 | import (
23 | "fmt"
24 |
25 | "github.com/canonical/iot-management/datastore"
26 | )
27 |
28 | // UserList lists existing users
29 | func (mem *Store) UserList() ([]datastore.User, error) {
30 | mem.lock.Lock()
31 | defer mem.lock.Unlock()
32 |
33 | return mem.Users, nil
34 | }
35 |
36 | // CreateUser creates a new user
37 | func (mem *Store) CreateUser(user datastore.User) (int64, error) {
38 | mem.lock.Lock()
39 | defer mem.lock.Unlock()
40 |
41 | user.ID = int64(len(mem.Users) + 1)
42 | mem.Users = append(mem.Users, user)
43 | return user.ID, nil
44 | }
45 |
46 | // GetUser gets an existing user
47 | func (mem *Store) GetUser(username string) (datastore.User, error) {
48 | mem.lock.RLock()
49 | defer mem.lock.RUnlock()
50 |
51 | for _, u := range mem.Users {
52 | if u.Username == username {
53 | return u, nil
54 | }
55 | }
56 |
57 | return datastore.User{}, fmt.Errorf("cannot find the user `%s`", username)
58 | }
59 |
60 | // UserUpdate updates a user
61 | func (mem *Store) UserUpdate(user datastore.User) error {
62 | mem.lock.Lock()
63 | defer mem.lock.Unlock()
64 |
65 | var index = -1
66 |
67 | for i, u := range mem.Users {
68 | if u.Username == user.Username {
69 | user.ID = u.ID
70 | index = i
71 | break
72 | }
73 | }
74 |
75 | if index < 0 {
76 | return fmt.Errorf("error finding user")
77 | }
78 | mem.Users[index] = user
79 | return nil
80 | }
81 |
82 | // UserDelete removes a user
83 | func (mem *Store) UserDelete(username string) error {
84 | mem.lock.Lock()
85 | defer mem.lock.Unlock()
86 |
87 | var index = -1
88 |
89 | for i, u := range mem.Users {
90 | if u.Username == username {
91 | index = i
92 | break
93 | }
94 | }
95 |
96 | if index < 0 {
97 | return fmt.Errorf("error finding user")
98 | }
99 |
100 | // Remove the element
101 | mem.Users = append(mem.Users[:index], mem.Users[index+1:]...)
102 | return nil
103 | }
104 |
--------------------------------------------------------------------------------
/datastore/memory/user_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package memory
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/canonical/iot-management/datastore"
26 | )
27 |
28 | func TestStore_UserWorkflow(t *testing.T) {
29 | tests := []struct {
30 | name string
31 | user datastore.User
32 | want int64
33 | find string
34 | count int
35 | wantErr bool
36 | findErr bool
37 | }{
38 | {"valid", datastore.User{Username: "jsmith", Name: "Joseph Smith", Role: 200}, 2, "jsmith", 2, false, false},
39 | {"invalid-find", datastore.User{Username: "jsmith", Name: "Joseph Smith", Role: 200}, 2, "not-exists", 1, false, true},
40 | }
41 | for _, tt := range tests {
42 | t.Run(tt.name, func(t *testing.T) {
43 | mem := NewStore()
44 | got, err := mem.CreateUser(tt.user)
45 | if (err != nil) != tt.wantErr {
46 | t.Errorf("Store.CreateUser() error = %v, wantErr %v", err, tt.wantErr)
47 | return
48 | }
49 | if got != tt.want {
50 | t.Errorf("Store.CreateUser() = %v, want %v", got, tt.want)
51 | }
52 |
53 | u, err := mem.GetUser(tt.find)
54 | if (err != nil) != tt.findErr {
55 | t.Errorf("Store.GetUser() error = %v, findErr %v", err, tt.findErr)
56 | return
57 | }
58 | if tt.findErr {
59 | return
60 | }
61 | if u.Username != tt.find {
62 | t.Errorf("Store.GetUser() = %v, want %v", u.Username, tt.find)
63 | }
64 |
65 | users, err := mem.UserList()
66 | if (err != nil) != tt.findErr {
67 | t.Errorf("Store.UserList() error = %v, findErr %v", err, tt.findErr)
68 | return
69 | }
70 | if len(users) != tt.count {
71 | t.Errorf("Store.UserList() = %v, want %v", len(users), tt.count)
72 | }
73 | })
74 | }
75 | }
76 |
77 | func TestStore_UserUpdate(t *testing.T) {
78 | tests := []struct {
79 | name string
80 | user datastore.User
81 | wantErr bool
82 | }{
83 | {"valid", datastore.User{Username: "jamesj", Name: "James Jones", Role: 200}, false},
84 | {"invalid", datastore.User{Username: "invalid", Name: "James Jones", Role: 200}, true},
85 | }
86 | for _, tt := range tests {
87 | t.Run(tt.name, func(t *testing.T) {
88 | mem := NewStore()
89 | if err := mem.UserUpdate(tt.user); (err != nil) != tt.wantErr {
90 | t.Errorf("Store.UserUpdate() error = %v, wantErr %v", err, tt.wantErr)
91 | }
92 | })
93 | }
94 | }
95 |
96 | func TestStore_UserDelete(t *testing.T) {
97 | tests := []struct {
98 | name string
99 | username string
100 | wantErr bool
101 | }{
102 | {"valid", "jamesj", false},
103 | {"invalid", "invalid", true},
104 | }
105 | for _, tt := range tests {
106 | t.Run(tt.name, func(t *testing.T) {
107 | mem := NewStore()
108 | if err := mem.UserDelete(tt.username); (err != nil) != tt.wantErr {
109 | t.Errorf("Store.UserDelete() error = %v, wantErr %v", err, tt.wantErr)
110 | }
111 | })
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/datastore/postgres/nonce.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package postgres
21 |
22 | import (
23 | "fmt"
24 | "log"
25 | "time"
26 |
27 | "github.com/canonical/iot-management/datastore"
28 | "github.com/juju/usso/openid"
29 | "gopkg.in/errgo.v1"
30 | )
31 |
32 | // NonceStore is a nonce store backed by database
33 | type NonceStore struct {
34 | DB *Store
35 | }
36 |
37 | // createNonceTable creates the database table for the openid nonce
38 | func (db *Store) createNonceTable() error {
39 | _, err := db.Exec(createNonceTableSQL)
40 | return err
41 | }
42 |
43 | // OpenIDNonceStore returns an openid nonce store
44 | func (db *Store) OpenIDNonceStore() openid.NonceStore {
45 | return &NonceStore{DB: db}
46 | }
47 |
48 | // Accept implements openid.NonceStore.Accept
49 | func (s *NonceStore) Accept(endpoint, nonce string) error {
50 | return s.accept(endpoint, nonce, time.Now())
51 | }
52 |
53 | // accept is the implementation of Accept. The third parameter is the
54 | // current time, useful for testing.
55 | func (s *NonceStore) accept(endpoint, nonce string, now time.Time) error {
56 | // From the openid specification:
57 | //
58 | // openid.response_nonce
59 | //
60 | // Value: A string 255 characters or less in length, that MUST be
61 | // unique to this particular successful authentication response.
62 | // The nonce MUST start with the current time on the server, and
63 | // MAY contain additional ASCII characters in the range 33-126
64 | // inclusive (printable non-whitespace characters), as necessary
65 | // to make each response unique. The date and time MUST be
66 | // formatted as specified in section 5.6 of [RFC3339], with the
67 | // following restrictions:
68 | //
69 | // + All times must be in the UTC timezone, indicated with a "Z".
70 | //
71 | // + No fractional seconds are allowed
72 | //
73 | // For example: 2005-05-15T17:11:51ZUNIQUE
74 |
75 | if len(nonce) < 20 {
76 | return fmt.Errorf("%q does not contain a valid timestamp", nonce)
77 | }
78 | t, err := time.Parse(time.RFC3339, nonce[:20])
79 | if err != nil {
80 | return fmt.Errorf("%q does not contain a valid timestamp: %v", nonce, err)
81 | }
82 |
83 | // Check if the nonce has expired
84 | diff := now.Sub(t)
85 | if diff > datastore.OpenidNonceMaxAge {
86 | return fmt.Errorf("%q too old", nonce)
87 | }
88 |
89 | openidNonce := datastore.OpenidNonce{Nonce: nonce, Endpoint: endpoint, TimeStamp: t.Unix()}
90 | err = s.DB.createOpenidNonce(openidNonce)
91 | return errgo.Mask(err)
92 | }
93 |
94 | // createOpenidNonce stores a new nonce entry
95 | func (db *Store) createOpenidNonce(nonce datastore.OpenidNonce) error {
96 | // Delete the expired nonces
97 | err := db.deleteExpiredOpenidNonces()
98 | if err != nil {
99 | log.Printf("Error checking expired openid nonces: %v\n", err)
100 | return err
101 | }
102 |
103 | // Create the nonce in the database
104 | _, err = db.Exec(createOpenidNonceSQL, nonce.Nonce, nonce.Endpoint, nonce.TimeStamp)
105 | if err != nil {
106 | log.Printf("Error creating the openid nonce: %v\n", err)
107 | return err
108 | }
109 |
110 | return nil
111 | }
112 |
113 | // deleteExpiredOpenidNonces removes nonces with timestamp older than max allowed lifetime
114 | func (db *Store) deleteExpiredOpenidNonces() error {
115 | // Remove expired nonces from the table
116 | timestamp := time.Now().Unix() - datastore.MaxNonceAgeInSeconds
117 | _, err := db.Exec(deleteExpiredOpenidNonceSQL, timestamp)
118 | if err != nil {
119 | log.Printf("Error deleting expired openid nonces: %v\n", err)
120 | return fmt.Errorf("error communicating with the database")
121 | }
122 |
123 | return nil
124 | }
125 |
--------------------------------------------------------------------------------
/datastore/postgres/nonce_sql.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package postgres
21 |
22 | const createNonceTableSQL = `
23 | CREATE TABLE IF NOT EXISTS openidnonce (
24 | id serial primary key,
25 | nonce varchar(255) not null,
26 | endpoint varchar(255) not null,
27 | timestamp int not null
28 | )
29 | `
30 |
31 | const createOpenidNonceSQL = "INSERT INTO openidnonce (nonce, endpoint, timestamp) VALUES ($1, $2, $3)"
32 | const deleteExpiredOpenidNonceSQL = "DELETE FROM openidnonce where timestamp<$1"
33 |
--------------------------------------------------------------------------------
/datastore/postgres/organization_sql.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package postgres
21 |
22 | const createOrganizationTableSQL = `
23 | CREATE TABLE IF NOT EXISTS organization (
24 | id serial primary key,
25 | code varchar(200) not null unique,
26 | name varchar(200) not null,
27 | active bool default true
28 | )
29 | `
30 | const createOrganizationUserTableSQL = `
31 | CREATE TABLE IF NOT EXISTS organization_user (
32 | id serial primary key,
33 | org_id varchar(200) not null,
34 | username varchar(200) not null,
35 | UNIQUE(org_id,username)
36 | )
37 | `
38 |
39 | const getOrganizationSQL = "select code, name from organization where code=$1"
40 |
41 | const createOrganizationSQL = "insert into organization (code, name) values ($1, $2) returning id"
42 |
43 | const organizationUserAccessSQL = `
44 | SELECT EXISTS(
45 | select id from organization_user
46 | where org_id=$1 and username=$2
47 | )
48 | `
49 |
50 | const listUserOrganizationsSQL = `
51 | select a.code, a.name
52 | from organization a
53 | inner join organization_user l on a.code = l.org_id
54 | where l.username=$1
55 | `
56 | const listOrganizationsSQL = `
57 | select code, name
58 | from organization
59 | where $1 = $1
60 | `
61 |
62 | const updateOrganizationSQL = `
63 | update organization
64 | set name=$2
65 | where code = $1
66 | `
67 |
68 | const deleteOrganizationUserAccessSQL = `
69 | delete from organization_user
70 | where org_id=$1 and username=$2
71 | `
72 |
73 | const createOrganizationUserAccessSQL = `
74 | insert into organization_user (org_id, username)
75 | values ($1, $2)
76 | `
77 |
--------------------------------------------------------------------------------
/datastore/postgres/postgres.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package postgres
21 |
22 | import (
23 | "database/sql"
24 | _ "github.com/lib/pq" // postgresql driver
25 | "log"
26 | )
27 |
28 | // Store implements an in-memory store for testing
29 | type Store struct {
30 | driver string
31 | *sql.DB
32 | }
33 |
34 | var pgStore *Store
35 |
36 | // OpenStore returns an open database connection
37 | func OpenStore(driver, dataSource string) *Store {
38 | if pgStore != nil {
39 | return pgStore
40 | }
41 |
42 | // Open the database
43 | pgStore = openDatabase(driver, dataSource)
44 |
45 | // Create the tables, if needed
46 | pgStore.createTables()
47 |
48 | return pgStore
49 | }
50 |
51 | // openDatabase return an open database connection for a postgreSQL database
52 | func openDatabase(driver, dataSource string) *Store {
53 | // Open the database connection
54 | db, err := sql.Open(driver, dataSource)
55 | if err != nil {
56 | log.Fatalf("Error opening the database: %v\n", err)
57 | }
58 |
59 | // Check that we have a valid database connection
60 | err = db.Ping()
61 | if err != nil {
62 | log.Fatalf("Error accessing the database: %v\n", err)
63 | }
64 |
65 | return &Store{driver, db}
66 | }
67 |
68 | func (db *Store) createTables() {
69 | _ = db.createUserTable()
70 | _ = db.createNonceTable()
71 | _ = db.createOrganizationTable()
72 | _ = db.createOrganizationUserTable()
73 | }
74 |
--------------------------------------------------------------------------------
/datastore/postgres/user.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package postgres
21 |
22 | import (
23 | "database/sql"
24 | "log"
25 |
26 | "github.com/canonical/iot-management/datastore"
27 | )
28 |
29 | // createUserTable creates the database table for devices with its indexes.
30 | func (db *Store) createUserTable() error {
31 | _, err := db.Exec(createUserTableSQL)
32 | return err
33 | }
34 |
35 | // CreateUser creates a new user
36 | func (db *Store) CreateUser(user datastore.User) (int64, error) {
37 | var createdUserID int64
38 |
39 | err := db.QueryRow(createUserSQL, user.Username, user.Name, user.Email, user.Role).Scan(&createdUserID)
40 | if err != nil {
41 | log.Printf("Error creating user `%s`: %v\n", user.Username, err)
42 | }
43 |
44 | return createdUserID, err
45 | }
46 |
47 | // UserUpdate updates a user
48 | func (db *Store) UserUpdate(user datastore.User) error {
49 | _, err := db.Exec(updateUserSQL, user.Username, user.Name, user.Email, user.Role)
50 | return err
51 | }
52 |
53 | // UserDelete removes a user
54 | func (db *Store) UserDelete(username string) error {
55 | _, err := db.Exec(deleteUserSQL, username)
56 | return err
57 | }
58 |
59 | // UserList lists existing users
60 | func (db *Store) UserList() ([]datastore.User, error) {
61 | rows, err := db.Query(listUsersSQL)
62 | if err != nil {
63 | log.Printf("Error retrieving database users: %v\n", err)
64 | return nil, err
65 | }
66 | defer rows.Close()
67 |
68 | return db.rowsToUsers(rows)
69 | }
70 |
71 | // GetUser gets an existing user
72 | func (db *Store) GetUser(username string) (datastore.User, error) {
73 | row := db.QueryRow(getUserSQL, username)
74 | user, err := db.rowToUser(row)
75 | if err != nil {
76 | log.Printf("Error retrieving user %v: %v\n", username, err)
77 | }
78 | return user, err
79 | }
80 |
81 | func (db *Store) rowToUser(row *sql.Row) (datastore.User, error) {
82 | user := datastore.User{}
83 | err := row.Scan(&user.ID, &user.Username, &user.Name, &user.Email, &user.Role)
84 | if err != nil {
85 | return datastore.User{}, err
86 | }
87 |
88 | return user, nil
89 | }
90 |
91 | func (db *Store) rowsToUser(rows *sql.Rows) (datastore.User, error) {
92 | user := datastore.User{}
93 | err := rows.Scan(&user.ID, &user.Username, &user.Name, &user.Email, &user.Role)
94 | if err != nil {
95 | return datastore.User{}, err
96 | }
97 |
98 | return user, nil
99 | }
100 |
101 | func (db *Store) rowsToUsers(rows *sql.Rows) ([]datastore.User, error) {
102 | users := []datastore.User{}
103 |
104 | for rows.Next() {
105 | user, err := db.rowsToUser(rows)
106 | if err != nil {
107 | return nil, err
108 | }
109 | users = append(users, user)
110 | }
111 |
112 | return users, nil
113 | }
114 |
--------------------------------------------------------------------------------
/datastore/postgres/user_sql.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package postgres
21 |
22 | const createUserTableSQL = `
23 | CREATE TABLE IF NOT EXISTS userinfo (
24 | id serial primary key,
25 | created timestamp default current_timestamp,
26 | modified timestamp default current_timestamp,
27 | username varchar(200) unique not null,
28 | name varchar(200) not null,
29 | email varchar(200) not null,
30 | user_role int not null
31 | )
32 | `
33 |
34 | const createUserSQL = "insert into userinfo (username, name, email, user_role) values ($1,$2,$3,$4) returning id"
35 | const listUsersSQL = "select id, username, name, email, user_role from userinfo order by username"
36 | const getUserSQL = "select id, username, name, email, user_role from userinfo where username=$1"
37 | const updateUserSQL = `
38 | update userinfo
39 | set name=$2, email=$3, user_role=$4
40 | where username=$1`
41 | const deleteUserSQL = "delete from userinfo where username=$1"
42 |
--------------------------------------------------------------------------------
/domain/entity.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package domain
21 |
22 | // User holds user personal, authentication and authorization info
23 | type User struct {
24 | ID int64 `json:"id"`
25 | Username string `json:"username"`
26 | Name string `json:"name"`
27 | Email string `json:"email"`
28 | Role int `json:"role"`
29 | }
30 |
31 | // Organization holds details of the organization
32 | type Organization struct {
33 | OrganizationID string `json:"orgid"`
34 | Name string `json:"name"`
35 | }
36 |
37 | // OrganizationCreate holds details of the organization creation request
38 | type OrganizationCreate struct {
39 | Name string `json:"name"`
40 | Country string `json:"country"`
41 | }
42 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/canonical/iot-management
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/canonical/iot-devicetwin v0.0.0-20210408101321-73fe6113136c
7 | github.com/canonical/iot-identity v0.0.0-20210408072605-83f114f75fbe
8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
9 | github.com/gorilla/csrf v1.7.0
10 | github.com/gorilla/mux v1.8.0
11 | github.com/juju/usso v1.0.1
12 | github.com/lib/pq v1.10.0
13 | gopkg.in/errgo.v1 v1.0.1
14 | gopkg.in/yaml.v2 v2.4.0
15 | )
16 |
--------------------------------------------------------------------------------
/identityapi/device.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package identityapi
21 |
22 | import (
23 | "encoding/json"
24 | "path"
25 |
26 | "github.com/canonical/iot-identity/web"
27 | )
28 |
29 | // RegDeviceList lists the devices for an account
30 | func (a *ClientAdapter) RegDeviceList(orgID string) web.DevicesResponse {
31 | r := web.DevicesResponse{}
32 | p := path.Join("devices", orgID)
33 |
34 | resp, err := get(a.urlPath(p))
35 | if err != nil {
36 | r.StandardResponse.Message = err.Error()
37 | return r
38 | }
39 |
40 | // Parse the response
41 | err = json.NewDecoder(resp.Body).Decode(&r)
42 | if err != nil {
43 | r.StandardResponse.Message = err.Error()
44 | }
45 |
46 | return r
47 | }
48 |
49 | // RegisterDevice registers a new device
50 | func (a *ClientAdapter) RegisterDevice(body []byte) web.RegisterResponse {
51 | r := web.RegisterResponse{}
52 | p := path.Join("device")
53 |
54 | resp, err := post(a.urlPath(p), body)
55 | if err != nil {
56 | r.StandardResponse.Message = err.Error()
57 | return r
58 | }
59 |
60 | // Parse the response
61 | err = json.NewDecoder(resp.Body).Decode(&r)
62 | if err != nil {
63 | r.StandardResponse.Message = err.Error()
64 | }
65 |
66 | return r
67 | }
68 |
69 | // RegDeviceGet fetches a device registration
70 | func (a *ClientAdapter) RegDeviceGet(orgID, deviceID string) web.EnrollResponse {
71 | r := web.EnrollResponse{}
72 | p := path.Join("devices", orgID, deviceID)
73 |
74 | resp, err := get(a.urlPath(p))
75 | if err != nil {
76 | r.StandardResponse.Message = err.Error()
77 | return r
78 | }
79 |
80 | // Parse the response
81 | err = json.NewDecoder(resp.Body).Decode(&r)
82 | if err != nil {
83 | r.StandardResponse.Message = err.Error()
84 | }
85 |
86 | return r
87 | }
88 |
89 | // RegDeviceUpdate updates a device registration
90 | func (a *ClientAdapter) RegDeviceUpdate(orgID, deviceID string, body []byte) web.StandardResponse {
91 | r := web.StandardResponse{}
92 | p := path.Join("devices", orgID, deviceID)
93 |
94 | resp, err := put(a.urlPath(p), body)
95 | if err != nil {
96 | r.Message = err.Error()
97 | return r
98 | }
99 |
100 | // Parse the response
101 | err = json.NewDecoder(resp.Body).Decode(&r)
102 | if err != nil {
103 | r.Message = err.Error()
104 | }
105 |
106 | return r
107 | }
108 |
--------------------------------------------------------------------------------
/identityapi/identityapi.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package identityapi
21 |
22 | import (
23 | "bytes"
24 | "net/http"
25 | "net/url"
26 | "path"
27 |
28 | "github.com/canonical/iot-identity/web"
29 | )
30 |
31 | // Client is a client for the identity API
32 | type Client interface {
33 | RegDeviceList(orgID string) web.DevicesResponse
34 | RegisterDevice(body []byte) web.RegisterResponse
35 | RegDeviceGet(orgID, deviceID string) web.EnrollResponse
36 | RegDeviceUpdate(orgID, deviceID string, body []byte) web.StandardResponse
37 | RegisterOrganization(body []byte) web.RegisterResponse
38 | RegOrganizationList() web.OrganizationsResponse
39 | }
40 |
41 | // ClientAdapter adapts our expectations to device twin API
42 | type ClientAdapter struct {
43 | URL string
44 | }
45 |
46 | var adapter *ClientAdapter
47 |
48 | // NewClientAdapter creates an adapter to access the device twin service
49 | func NewClientAdapter(u string) (*ClientAdapter, error) {
50 | if adapter == nil {
51 | adapter = &ClientAdapter{URL: u}
52 | }
53 | return adapter, nil
54 | }
55 |
56 | func (a *ClientAdapter) urlPath(p string) string {
57 | u, _ := url.Parse(a.URL)
58 | u.Path = path.Join(u.Path, p)
59 | return u.String()
60 | }
61 |
62 | var get = func(p string) (*http.Response, error) {
63 | return http.Get(p)
64 | }
65 |
66 | var post = func(p string, data []byte) (*http.Response, error) {
67 | return http.Post(p, "application/json", bytes.NewReader(data))
68 | }
69 |
70 | var put = func(p string, data []byte) (*http.Response, error) {
71 | client := &http.Client{}
72 | req, err := http.NewRequest(http.MethodPut, p, bytes.NewReader(data))
73 | if err != nil {
74 | return nil, err
75 | }
76 | return client.Do(req)
77 | }
78 |
--------------------------------------------------------------------------------
/identityapi/organization.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package identityapi
21 |
22 | import (
23 | "encoding/json"
24 | "path"
25 |
26 | "github.com/canonical/iot-identity/web"
27 | )
28 |
29 | // RegisterOrganization registers a new organization
30 | func (a *ClientAdapter) RegisterOrganization(body []byte) web.RegisterResponse {
31 | r := web.RegisterResponse{}
32 | p := path.Join("organization")
33 |
34 | resp, err := post(a.urlPath(p), body)
35 | if err != nil {
36 | r.Message = err.Error()
37 | return r
38 | }
39 |
40 | // Parse the response
41 | err = json.NewDecoder(resp.Body).Decode(&r)
42 | if err != nil {
43 | r.Message = err.Error()
44 | }
45 |
46 | return r
47 | }
48 |
49 | // RegOrganizationList lists the organizations for an account
50 | func (a *ClientAdapter) RegOrganizationList() web.OrganizationsResponse {
51 | r := web.OrganizationsResponse{}
52 | p := path.Join("organizations")
53 |
54 | resp, err := get(a.urlPath(p))
55 | if err != nil {
56 | r.StandardResponse.Message = err.Error()
57 | return r
58 | }
59 |
60 | // Parse the response
61 | err = json.NewDecoder(resp.Body).Decode(&r)
62 | if err != nil {
63 | r.StandardResponse.Message = err.Error()
64 | }
65 |
66 | return r
67 | }
68 |
--------------------------------------------------------------------------------
/identityapi/organization_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package identityapi
21 |
22 | import (
23 | "encoding/json"
24 | "testing"
25 |
26 | "github.com/canonical/iot-identity/service"
27 | )
28 |
29 | func TestClientAdapter_RegisterOrganization(t *testing.T) {
30 | b1 := `{"id": "def", "message": ""}`
31 | type fields struct {
32 | URL string
33 | }
34 | type args struct {
35 | name string
36 | country string
37 | body string
38 | }
39 | tests := []struct {
40 | name string
41 | fields fields
42 | args args
43 | want string
44 | wantErr string
45 | }{
46 | {"valid", fields{""}, args{"Test Inc", "GB", b1}, "def", ""},
47 | {"invalid-org", fields{"invalid"}, args{"invalid", "GB", b1}, "", "MOCK error post"},
48 | {"invalid-body", fields{""}, args{"abc", "GB", ""}, "", "EOF"},
49 | }
50 | for _, tt := range tests {
51 | t.Run(tt.name, func(t *testing.T) {
52 | mockHTTP(tt.args.body)
53 | a := &ClientAdapter{
54 | URL: tt.fields.URL,
55 | }
56 | b, _ := json.Marshal(service.RegisterOrganizationRequest{Name: tt.args.name, CountryName: tt.args.country})
57 |
58 | got := a.RegisterOrganization(b)
59 | if got.Message != tt.wantErr {
60 | t.Errorf("ClientAdapter.RegisterOrganization() = %v, want %v", got.Message, tt.wantErr)
61 | }
62 | if got.ID != tt.want {
63 | t.Errorf("ClientAdapter.RegisterOrganization() = %v, want ID %v", got.Code, tt.want)
64 | }
65 | })
66 | }
67 | }
68 |
69 | func TestClientAdapter_RegOrganizationList(t *testing.T) {
70 | b1 := `{"organizations": [{"id":"abc", "name":"Test Org Ltd"}]}`
71 | type fields struct {
72 | URL string
73 | }
74 | tests := []struct {
75 | name string
76 | body string
77 | fields fields
78 | want int
79 | wantErr string
80 | }{
81 | {"valid", b1, fields{""}, 1, ""},
82 | {"invalid-org", "", fields{"invalid"}, 0, "MOCK error get"},
83 | {"invalid-body", "", fields{""}, 0, "EOF"},
84 | }
85 | for _, tt := range tests {
86 | t.Run(tt.name, func(t *testing.T) {
87 | mockHTTP(tt.body)
88 | a := &ClientAdapter{
89 | URL: tt.fields.URL,
90 | }
91 | got := a.RegOrganizationList()
92 | if got.Message != tt.wantErr {
93 | t.Errorf("ClientAdapter.RegisterOrganization() = %v, want %v", got.Message, tt.wantErr)
94 | }
95 | if len(got.Organizations) != tt.want {
96 | t.Errorf("ClientAdapter.RegisterOrganization() = %v, want ID %v", len(got.Organizations), tt.want)
97 | }
98 | })
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/identityapi/testing_identityapi.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package identityapi
21 |
22 | import (
23 | "fmt"
24 | "io/ioutil"
25 | "net/http"
26 | "strings"
27 |
28 | "github.com/canonical/iot-identity/domain"
29 | "github.com/canonical/iot-identity/web"
30 | )
31 |
32 | func mockHTTP(body string) {
33 | // Mock the HTTP methods
34 | get = func(p string) (*http.Response, error) {
35 | if strings.Contains(p, "invalid") {
36 | return nil, fmt.Errorf("MOCK error get")
37 | }
38 | return &http.Response{
39 | Body: ioutil.NopCloser(strings.NewReader(body)),
40 | }, nil
41 | }
42 | post = func(p string, data []byte) (*http.Response, error) {
43 | if strings.Contains(p, "invalid") {
44 | return nil, fmt.Errorf("MOCK error post")
45 | }
46 | return &http.Response{
47 | Body: ioutil.NopCloser(strings.NewReader(body)),
48 | }, nil
49 | }
50 | put = func(p string, data []byte) (*http.Response, error) {
51 | if strings.Contains(p, "invalid") {
52 | return nil, fmt.Errorf("MOCK error put")
53 | }
54 | return &http.Response{
55 | Body: ioutil.NopCloser(strings.NewReader(body)),
56 | }, nil
57 | }
58 | }
59 |
60 | // MockIdentity mocks the identity API client
61 | type MockIdentity struct{}
62 |
63 | // RegDeviceList mocks listing registered devices
64 | func (m *MockIdentity) RegDeviceList(orgID string) web.DevicesResponse {
65 | if orgID == "invalid" {
66 | return web.DevicesResponse{
67 | StandardResponse: web.StandardResponse{Code: "RegDeviceAuth", Message: "MOCK error devices"},
68 | Devices: nil,
69 | }
70 | }
71 | return web.DevicesResponse{
72 | StandardResponse: web.StandardResponse{},
73 | Devices: []domain.Enrollment{},
74 | }
75 | }
76 |
77 | // RegisterDevice mocks registering a device
78 | func (m *MockIdentity) RegisterDevice(body []byte) web.RegisterResponse {
79 | return web.RegisterResponse{
80 | StandardResponse: web.StandardResponse{},
81 | ID: "d444",
82 | }
83 | }
84 |
85 | // RegDeviceGet mocks fetching a registered device
86 | func (m *MockIdentity) RegDeviceGet(orgID, deviceID string) web.EnrollResponse {
87 | if deviceID == "invalid" {
88 | return web.EnrollResponse{
89 | StandardResponse: web.StandardResponse{Code: "RegDeviceAuth", Message: "MOCK error get"},
90 | Enrollment: domain.Enrollment{},
91 | }
92 | }
93 | return web.EnrollResponse{
94 | StandardResponse: web.StandardResponse{},
95 | Enrollment: domain.Enrollment{},
96 | }
97 | }
98 |
99 | // RegDeviceUpdate mocks updating a registered device
100 | func (m *MockIdentity) RegDeviceUpdate(orgID, deviceID string, body []byte) web.StandardResponse {
101 | if deviceID == "invalid" {
102 | return web.StandardResponse{Code: "RegDeviceUpdate", Message: "MOCK error update"}
103 | }
104 | return web.StandardResponse{}
105 | }
106 |
107 | // RegOrganizationList mocks listing registered organizations
108 | func (m *MockIdentity) RegOrganizationList() web.OrganizationsResponse {
109 | return web.OrganizationsResponse{
110 | StandardResponse: web.StandardResponse{},
111 | Organizations: []domain.Organization{},
112 | }
113 | }
114 |
115 | // RegisterOrganization mocks registering an organization
116 | func (m *MockIdentity) RegisterOrganization(body []byte) web.RegisterResponse {
117 | return web.RegisterResponse{
118 | StandardResponse: web.StandardResponse{},
119 | ID: "def",
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/k8s-management.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: management
5 | spec:
6 | selector:
7 | matchLabels:
8 | app: management
9 | tier: frontend
10 | track: stable
11 | replicas: 1
12 | template:
13 | metadata:
14 | labels:
15 | app: management
16 | tier: frontend
17 | track: stable
18 | spec:
19 | containers:
20 | - name: management
21 | image: sonicblue/iot-management
22 | env:
23 | - name: DRIVER
24 | value: "postgres"
25 | - name: DATASOURCE
26 | valueFrom:
27 | configMapKeyRef:
28 | name: postgres-config
29 | key: DATASOURCE
30 | - name: HOST
31 | value: "management:8010"
32 | - name: SCHEME
33 | value: "http"
34 | - name: IDENTITYAPI
35 | value: "http://identity:8030/v1/"
36 | - name: DEVICETWINAPI
37 | value: "http://devicetwin:8040/v1/"
38 | - name: STOREURL
39 | value: "https://api.snapcraft.io/api/v1/"
40 | ports:
41 | - containerPort: 8010
42 | ---
43 | apiVersion: v1
44 | kind: Service
45 | metadata:
46 | name: management
47 | spec:
48 | selector:
49 | app: management
50 | tier: frontend
51 | ports:
52 | - port: 8010
53 | protocol: TCP
54 |
--------------------------------------------------------------------------------
/k8s-postgres.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: postgres-config
5 | labels:
6 | app: postgres
7 | data:
8 | POSTGRES_DB: management
9 | POSTGRES_USER: manager
10 | POSTGRES_PASSWORD: m8v8l7IbOws81Kq
11 | DATASOURCE: "dbname=management host=postgres user=manager password=m8v8l7IbOws81Kq sslmode=disable"
12 | ---
13 | kind: PersistentVolume
14 | apiVersion: v1
15 | metadata:
16 | name: postgres-pv-volume
17 | labels:
18 | type: local
19 | app: postgres
20 | spec:
21 | storageClassName: manual
22 | capacity:
23 | storage: 1Gi
24 | accessModes:
25 | - ReadWriteMany
26 | hostPath:
27 | path: "/mnt/data"
28 | ---
29 | kind: PersistentVolumeClaim
30 | apiVersion: v1
31 | metadata:
32 | name: postgres-pv-claim
33 | labels:
34 | app: postgres
35 | spec:
36 | #storageClassName: manual
37 | accessModes:
38 | - ReadWriteMany
39 | resources:
40 | requests:
41 | storage: 1Gi
42 | ---
43 | apiVersion: apps/v1
44 | kind: Deployment
45 | metadata:
46 | name: postgres
47 | spec:
48 | selector:
49 | matchLabels:
50 | app: postgres
51 | replicas: 1
52 | template:
53 | metadata:
54 | labels:
55 | app: postgres
56 | spec:
57 | containers:
58 | - name: postgres
59 | image: postgres:10.4
60 | imagePullPolicy: "IfNotPresent"
61 | ports:
62 | - containerPort: 5432
63 | envFrom:
64 | - configMapRef:
65 | name: postgres-config
66 | volumeMounts:
67 | - mountPath: /var/lib/postgresql/data
68 | name: postgredb
69 | volumes:
70 | - name: postgredb
71 | persistentVolumeClaim:
72 | claimName: postgres-pv-claim
73 | ---
74 | apiVersion: v1
75 | kind: Service
76 | metadata:
77 | name: postgres
78 | labels:
79 | app: postgres
80 | spec:
81 | type: NodePort
82 | ports:
83 | - port: 5432
84 | selector:
85 | app: postgres
86 |
--------------------------------------------------------------------------------
/service/factory/factory.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package factory
21 |
22 | import (
23 | "fmt"
24 |
25 | "github.com/canonical/iot-management/config"
26 | "github.com/canonical/iot-management/datastore"
27 | "github.com/canonical/iot-management/datastore/memory"
28 | "github.com/canonical/iot-management/datastore/postgres"
29 | )
30 |
31 | // CreateDataStore is the factory method to create a data store
32 | func CreateDataStore(settings *config.Settings) (datastore.DataStore, error) {
33 | var db datastore.DataStore
34 | switch settings.Driver {
35 | case "memory":
36 | db = memory.NewStore()
37 | case "postgres":
38 | db = postgres.OpenStore(settings.Driver, settings.DataSource)
39 | default:
40 | return nil, fmt.Errorf("unknown data store driver: %v", settings.Driver)
41 | }
42 |
43 | return db, nil
44 | }
45 |
--------------------------------------------------------------------------------
/service/factory/factory_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package factory
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/canonical/iot-management/config"
26 | )
27 |
28 | func TestCreateDataStore(t *testing.T) {
29 | tests := []struct {
30 | name string
31 | wantErr bool
32 | }{
33 | {"valid", false},
34 | }
35 | for _, tt := range tests {
36 | t.Run(tt.name, func(t *testing.T) {
37 | settings, err := config.Config("../../testing/memory.yaml")
38 | if (err != nil) != tt.wantErr {
39 | t.Errorf("CreateDataStore() settings = %v, wantErr %v", err, tt.wantErr)
40 | return
41 | }
42 |
43 | _, err = CreateDataStore(settings)
44 | if (err != nil) != tt.wantErr {
45 | t.Errorf("CreateDataStore() error = %v, wantErr %v", err, tt.wantErr)
46 | return
47 | }
48 | })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/service/manage/action.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import "github.com/canonical/iot-devicetwin/web"
23 |
24 | // ActionList gets the actions for a device
25 | func (srv *Management) ActionList(orgID, username string, role int, deviceID string) web.ActionsResponse {
26 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
27 | if !hasAccess {
28 | return web.ActionsResponse{
29 | StandardResponse: web.StandardResponse{
30 | Code: "DeviceAuth",
31 | Message: "the user does not have permissions for the organization",
32 | },
33 | }
34 | }
35 |
36 | return srv.TwinAPI.ActionList(orgID, deviceID)
37 | }
38 |
--------------------------------------------------------------------------------
/service/manage/device.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import (
23 | "github.com/canonical/iot-devicetwin/web"
24 | )
25 |
26 | // DeviceList gets the devices a user can access for an organization
27 | func (srv *Management) DeviceList(orgID, username string, role int) web.DevicesResponse {
28 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
29 | if !hasAccess {
30 | return web.DevicesResponse{
31 | StandardResponse: web.StandardResponse{
32 | Code: "DevicesAuth",
33 | Message: "the user does not have permissions for the organization",
34 | },
35 | }
36 | }
37 |
38 | return srv.TwinAPI.DeviceList(orgID)
39 | }
40 |
41 | // DeviceGet gets the device for an organization
42 | func (srv *Management) DeviceGet(orgID, username string, role int, deviceID string) web.DeviceResponse {
43 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
44 | if !hasAccess {
45 | return web.DeviceResponse{
46 | StandardResponse: web.StandardResponse{
47 | Code: "DeviceAuth",
48 | Message: "the user does not have permissions for the organization",
49 | },
50 | }
51 | }
52 |
53 | return srv.TwinAPI.DeviceGet(orgID, deviceID)
54 | }
55 |
--------------------------------------------------------------------------------
/service/manage/device_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import (
23 | "testing"
24 |
25 | "github.com/canonical/iot-management/config"
26 | "github.com/canonical/iot-management/datastore/memory"
27 | "github.com/canonical/iot-management/identityapi"
28 |
29 | "github.com/canonical/iot-management/twinapi"
30 | )
31 |
32 | var settings *config.Settings
33 |
34 | func getSettings() *config.Settings {
35 | if settings == nil {
36 | settings, _ = config.Config("../../testing/memory.yaml")
37 | }
38 | return settings
39 | }
40 |
41 | func TestManagement_DeviceList(t *testing.T) {
42 | type args struct {
43 | orgID string
44 | username string
45 | role int
46 | }
47 | tests := []struct {
48 | name string
49 | args args
50 | want int
51 | wantErr string
52 | }{
53 | {"valid", args{"abc", "jamesj", 300}, 3, ""},
54 | {"invalid-user", args{"abc", "invalid", 200}, 0, "DevicesAuth"},
55 | }
56 | for _, tt := range tests {
57 | t.Run(tt.name, func(t *testing.T) {
58 | srv := NewManagement(getSettings(), memory.NewStore(), twinapi.NewMockClient(""), &identityapi.MockIdentity{})
59 | got := srv.DeviceList(tt.args.orgID, tt.args.username, tt.args.role)
60 | if got.Code != tt.wantErr {
61 | t.Errorf("Management.DeviceList() = %v, want %v", got.Code, tt.wantErr)
62 | }
63 | if len(got.Devices) != tt.want {
64 | t.Errorf("Management.DeviceList() = %v, want %v", len(got.Devices), tt.want)
65 | }
66 | })
67 | }
68 | }
69 |
70 | func TestManagement_DeviceGet(t *testing.T) {
71 | type args struct {
72 | orgID string
73 | username string
74 | role int
75 | deviceID string
76 | }
77 | tests := []struct {
78 | name string
79 | args args
80 | wantSerial string
81 | wantErr string
82 | }{
83 | {"valid", args{"abc", "jamesj", 200, "b222"}, "DR1000B222", ""},
84 | {"invalid-user", args{"abc", "invalid", 200, "b222"}, "", "DeviceAuth"},
85 | }
86 | for _, tt := range tests {
87 | t.Run(tt.name, func(t *testing.T) {
88 | srv := &Management{
89 | Settings: getSettings(),
90 | DB: memory.NewStore(),
91 | TwinAPI: twinapi.NewMockClient(""),
92 | }
93 | got := srv.DeviceGet(tt.args.orgID, tt.args.username, tt.args.role, tt.args.deviceID)
94 | if got.Code != tt.wantErr {
95 | t.Errorf("Management.DeviceGet() = %v, want %v", got.Code, tt.wantErr)
96 | }
97 | if got.Device.SerialNumber != tt.wantSerial {
98 | t.Errorf("Management.DeviceGet() = %v, want %v", got.Device.SerialNumber, tt.wantSerial)
99 | }
100 | })
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/service/manage/group.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import "github.com/canonical/iot-devicetwin/web"
23 |
24 | // GroupList lists the device groups
25 | func (srv *Management) GroupList(orgID, username string, role int) web.GroupsResponse {
26 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
27 | if !hasAccess {
28 | return web.GroupsResponse{
29 | StandardResponse: web.StandardResponse{
30 | Code: "GroupAuth",
31 | Message: "the user does not have permissions for the organization",
32 | },
33 | }
34 | }
35 |
36 | return srv.TwinAPI.GroupList(orgID)
37 | }
38 |
39 | // GroupCreate creates a device group
40 | func (srv *Management) GroupCreate(orgID, username string, role int, body []byte) web.StandardResponse {
41 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
42 | if !hasAccess {
43 | return web.StandardResponse{
44 | Code: "GroupAuth",
45 | Message: "the user does not have permissions for the organization",
46 | }
47 | }
48 |
49 | return srv.TwinAPI.GroupCreate(orgID, body)
50 | }
51 |
52 | // GroupDevices lists the devices for a groups
53 | func (srv *Management) GroupDevices(orgID, username string, role int, name string) web.DevicesResponse {
54 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
55 | if !hasAccess {
56 | return web.DevicesResponse{
57 | StandardResponse: web.StandardResponse{
58 | Code: "GroupAuth",
59 | Message: "the user does not have permissions for the organization",
60 | },
61 | }
62 | }
63 |
64 | return srv.TwinAPI.GroupDevices(orgID, name)
65 | }
66 |
67 | // GroupExcludedDevices lists the devices for a groups
68 | func (srv *Management) GroupExcludedDevices(orgID, username string, role int, name string) web.DevicesResponse {
69 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
70 | if !hasAccess {
71 | return web.DevicesResponse{
72 | StandardResponse: web.StandardResponse{
73 | Code: "GroupAuth",
74 | Message: "the user does not have permissions for the organization",
75 | },
76 | }
77 | }
78 |
79 | return srv.TwinAPI.GroupExcludedDevices(orgID, name)
80 | }
81 |
82 | // GroupDeviceLink links a device to a group
83 | func (srv *Management) GroupDeviceLink(orgID, username string, role int, name, deviceID string) web.StandardResponse {
84 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
85 | if !hasAccess {
86 | return web.StandardResponse{
87 | Code: "GroupAuth",
88 | Message: "the user does not have permissions for the organization",
89 | }
90 | }
91 |
92 | return srv.TwinAPI.GroupDeviceLink(orgID, name, deviceID)
93 | }
94 |
95 | // GroupDeviceUnlink unlinks a device from a group
96 | func (srv *Management) GroupDeviceUnlink(orgID, username string, role int, name, deviceID string) web.StandardResponse {
97 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
98 | if !hasAccess {
99 | return web.StandardResponse{
100 | Code: "GroupAuth",
101 | Message: "the user does not have permissions for the organization",
102 | }
103 | }
104 |
105 | return srv.TwinAPI.GroupDeviceUnlink(orgID, name, deviceID)
106 | }
107 |
--------------------------------------------------------------------------------
/service/manage/manage.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import (
23 | "github.com/canonical/iot-devicetwin/web"
24 | idweb "github.com/canonical/iot-identity/web"
25 | "github.com/canonical/iot-management/config"
26 | "github.com/canonical/iot-management/datastore"
27 | "github.com/canonical/iot-management/domain"
28 | "github.com/canonical/iot-management/identityapi"
29 | "github.com/canonical/iot-management/twinapi"
30 | "github.com/juju/usso/openid"
31 | )
32 |
33 | // Manage interface for the service
34 | type Manage interface {
35 | OpenIDNonceStore() openid.NonceStore
36 | CreateUser(user domain.User) error
37 | GetUser(username string) (domain.User, error)
38 | UserList() ([]domain.User, error)
39 | UserUpdate(user domain.User) error
40 | UserDelete(username string) error
41 |
42 | RegDeviceList(orgID, username string, role int) idweb.DevicesResponse
43 | RegisterDevice(orgID, username string, role int, body []byte) idweb.RegisterResponse
44 | RegDeviceGet(orgID, username string, role int, deviceID string) idweb.EnrollResponse
45 | RegDeviceUpdate(orgID, username string, role int, deviceID string, body []byte) idweb.StandardResponse
46 |
47 | DeviceList(orgID, username string, role int) web.DevicesResponse
48 | DeviceGet(orgID, username string, role int, deviceID string) web.DeviceResponse
49 | ActionList(orgID, username string, role int, deviceID string) web.ActionsResponse
50 |
51 | SnapList(orgID, username string, role int, deviceID string) web.SnapsResponse
52 | SnapListOnDevice(orgID, username string, role int, deviceID string) web.StandardResponse
53 | SnapInstall(orgID, username string, role int, deviceID, snap string) web.StandardResponse
54 | SnapRemove(orgID, username string, role int, deviceID, snap string) web.StandardResponse
55 | SnapUpdate(orgID, username string, role int, deviceID, snap, action string) web.StandardResponse
56 | SnapConfigSet(orgID, username string, role int, deviceID, snap string, config []byte) web.StandardResponse
57 |
58 | GroupList(orgID, username string, role int) web.GroupsResponse
59 | GroupCreate(orgID, username string, role int, body []byte) web.StandardResponse
60 | GroupDevices(orgID, username string, role int, name string) web.DevicesResponse
61 | GroupExcludedDevices(orgID, username string, role int, name string) web.DevicesResponse
62 | GroupDeviceLink(orgID, username string, role int, name, deviceID string) web.StandardResponse
63 | GroupDeviceUnlink(orgID, username string, role int, name, deviceID string) web.StandardResponse
64 |
65 | OrganizationsForUser(username string) ([]domain.Organization, error)
66 | OrganizationForUserToggle(orgID, username string) error
67 | OrganizationGet(orgID string) (domain.Organization, error)
68 | OrganizationCreate(org domain.OrganizationCreate) error
69 | OrganizationUpdate(org domain.Organization) error
70 | }
71 |
72 | // Management implementation of the management service use cases
73 | type Management struct {
74 | Settings *config.Settings
75 | DB datastore.DataStore
76 | TwinAPI twinapi.Client
77 | IdentityAPI identityapi.Client
78 | }
79 |
80 | // NewManagement creates an implementation of the management use cases
81 | func NewManagement(settings *config.Settings, db datastore.DataStore, api twinapi.Client, id identityapi.Client) *Management {
82 | return &Management{
83 | Settings: settings,
84 | DB: db,
85 | TwinAPI: api,
86 | IdentityAPI: id,
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/service/manage/organization.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import (
23 | "encoding/json"
24 | "fmt"
25 |
26 | "github.com/canonical/iot-management/datastore"
27 | "github.com/canonical/iot-management/domain"
28 | )
29 |
30 | // OrganizationsForUser fetches the organizations for a user
31 | func (srv *Management) OrganizationsForUser(username string) ([]domain.Organization, error) {
32 | orgs, err := srv.DB.OrganizationsForUser(username)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | oo := []domain.Organization{}
38 | for _, o := range orgs {
39 | oo = append(oo, domain.Organization{
40 | OrganizationID: o.OrganizationID,
41 | Name: o.Name,
42 | })
43 | }
44 | return oo, nil
45 | }
46 |
47 | // OrganizationForUserToggle toggles organization access for a user
48 | func (srv *Management) OrganizationForUserToggle(orgID, username string) error {
49 | return srv.DB.OrganizationForUserToggle(orgID, username)
50 | }
51 |
52 | // OrganizationGet fetches an organization
53 | func (srv *Management) OrganizationGet(orgID string) (domain.Organization, error) {
54 | org, err := srv.DB.OrganizationGet(orgID)
55 | if err != nil {
56 | return domain.Organization{}, err
57 | }
58 | return domain.Organization{
59 | OrganizationID: org.OrganizationID,
60 | Name: org.Name,
61 | }, nil
62 | }
63 |
64 | // OrganizationCreate creates a new organization
65 | func (srv *Management) OrganizationCreate(org domain.OrganizationCreate) error {
66 | // Serialize the request
67 | b, err := json.Marshal(org)
68 | if err != nil {
69 | return err
70 | }
71 |
72 | // Register the organization with the identity service
73 | resp := srv.IdentityAPI.RegisterOrganization(b)
74 | if len(resp.Message) > 0 {
75 | return fmt.Errorf("error registering organization: %v", resp.Message)
76 | }
77 |
78 | // Create the organization in the local database with the generated ID
79 | o := datastore.Organization{
80 | OrganizationID: resp.ID,
81 | Name: org.Name,
82 | }
83 | return srv.DB.OrganizationCreate(o)
84 | }
85 |
86 | // OrganizationUpdate updates an organization
87 | func (srv *Management) OrganizationUpdate(org domain.Organization) error {
88 | o := datastore.Organization{
89 | OrganizationID: org.OrganizationID,
90 | Name: org.Name,
91 | }
92 |
93 | return srv.DB.OrganizationUpdate(o)
94 | }
95 |
--------------------------------------------------------------------------------
/service/manage/registry.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import (
23 | "github.com/canonical/iot-identity/web"
24 | idweb "github.com/canonical/iot-identity/web"
25 | )
26 |
27 | // RegDeviceList gets the registered devices a user can access for an organization
28 | func (srv *Management) RegDeviceList(orgID, username string, role int) web.DevicesResponse {
29 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
30 | if !hasAccess {
31 | return web.DevicesResponse{
32 | StandardResponse: web.StandardResponse{
33 | Code: "RegDevicesAuth",
34 | Message: "the user does not have permissions for the organization",
35 | },
36 | }
37 | }
38 |
39 | return srv.IdentityAPI.RegDeviceList(orgID)
40 | }
41 |
42 | // RegisterDevice registers a new device
43 | func (srv *Management) RegisterDevice(orgID, username string, role int, body []byte) web.RegisterResponse {
44 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
45 | if !hasAccess {
46 | return web.RegisterResponse{
47 | StandardResponse: web.StandardResponse{
48 | Code: "RegDeviceAuth",
49 | Message: "the user does not have permissions for the organization",
50 | },
51 | }
52 | }
53 |
54 | return srv.IdentityAPI.RegisterDevice(body)
55 | }
56 |
57 | // RegDeviceGet fetches a device registration
58 | func (srv *Management) RegDeviceGet(orgID, username string, role int, deviceID string) web.EnrollResponse {
59 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
60 | if !hasAccess {
61 | return web.EnrollResponse{
62 | StandardResponse: web.StandardResponse{
63 | Code: "RegDeviceAuth",
64 | Message: "the user does not have permissions for the organization",
65 | },
66 | }
67 | }
68 |
69 | return srv.IdentityAPI.RegDeviceGet(orgID, deviceID)
70 | }
71 |
72 | // RegDeviceUpdate updates a device registration
73 | func (srv *Management) RegDeviceUpdate(orgID, username string, role int, deviceID string, body []byte) idweb.StandardResponse {
74 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
75 | if !hasAccess {
76 | return idweb.StandardResponse{
77 | Code: "RegDeviceAuth",
78 | Message: "the user does not have permissions for the organization",
79 | }
80 | }
81 | return srv.IdentityAPI.RegDeviceUpdate(orgID, deviceID, body)
82 | }
83 |
--------------------------------------------------------------------------------
/service/manage/snap.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import (
23 | "github.com/canonical/iot-devicetwin/web"
24 | )
25 |
26 | // SnapList lists the snaps for a device
27 | func (srv *Management) SnapList(orgID, username string, role int, deviceID string) web.SnapsResponse {
28 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
29 | if !hasAccess {
30 | return web.SnapsResponse{
31 | StandardResponse: web.StandardResponse{
32 | Code: "SnapsAuth",
33 | Message: "the user does not have permissions for the organization",
34 | },
35 | }
36 | }
37 |
38 | return srv.TwinAPI.SnapList(orgID, deviceID)
39 | }
40 |
41 | // SnapListOnDevice lists snaps on a device
42 | func (srv *Management) SnapListOnDevice(orgID, username string, role int, deviceID string) web.StandardResponse {
43 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
44 | if !hasAccess {
45 | return web.StandardResponse{
46 | Code: "SnapAuth",
47 | Message: "the user does not have permissions for the organization",
48 | }
49 | }
50 |
51 | return srv.TwinAPI.SnapListOnDevice(orgID, deviceID)
52 | }
53 |
54 | // SnapInstall installs a snap on a device
55 | func (srv *Management) SnapInstall(orgID, username string, role int, deviceID, snap string) web.StandardResponse {
56 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
57 | if !hasAccess {
58 | return web.StandardResponse{
59 | Code: "SnapAuth",
60 | Message: "the user does not have permissions for the organization",
61 | }
62 | }
63 |
64 | return srv.TwinAPI.SnapInstall(orgID, deviceID, snap)
65 | }
66 |
67 | // SnapRemove uninstalls a snap on a device
68 | func (srv *Management) SnapRemove(orgID, username string, role int, deviceID, snap string) web.StandardResponse {
69 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
70 | if !hasAccess {
71 | return web.StandardResponse{
72 | Code: "SnapAuth",
73 | Message: "the user does not have permissions for the organization",
74 | }
75 | }
76 |
77 | return srv.TwinAPI.SnapRemove(orgID, deviceID, snap)
78 | }
79 |
80 | // SnapUpdate enables/disables/refreshes a snap on a device
81 | func (srv *Management) SnapUpdate(orgID, username string, role int, deviceID, snap, action string) web.StandardResponse {
82 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
83 | if !hasAccess {
84 | return web.StandardResponse{
85 | Code: "SnapAuth",
86 | Message: "the user does not have permissions for the organization",
87 | }
88 | }
89 |
90 | return srv.TwinAPI.SnapUpdate(orgID, deviceID, snap, action)
91 | }
92 |
93 | // SnapConfigSet updates a snap config on a device
94 | func (srv *Management) SnapConfigSet(orgID, username string, role int, deviceID, snap string, config []byte) web.StandardResponse {
95 | hasAccess := srv.DB.OrgUserAccess(orgID, username, role)
96 | if !hasAccess {
97 | return web.StandardResponse{
98 | Code: "SnapAuth",
99 | Message: "the user does not have permissions for the organization",
100 | }
101 | }
102 |
103 | return srv.TwinAPI.SnapConfigSet(orgID, deviceID, snap, config)
104 | }
105 |
--------------------------------------------------------------------------------
/service/manage/user.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package manage
21 |
22 | import (
23 | "github.com/canonical/iot-management/datastore"
24 | "github.com/canonical/iot-management/domain"
25 | "github.com/juju/usso/openid"
26 | )
27 |
28 | // OpenIDNonceStore fetches the OpenID nonce store
29 | func (srv *Management) OpenIDNonceStore() openid.NonceStore {
30 | return srv.DB.OpenIDNonceStore()
31 | }
32 |
33 | // GetUser fetches a user from the database
34 | func (srv *Management) GetUser(username string) (domain.User, error) {
35 | u, err := srv.DB.GetUser(username)
36 | if err != nil {
37 | return domain.User{}, err
38 | }
39 |
40 | return domain.User{
41 | ID: u.ID,
42 | Name: u.Name,
43 | Username: u.Username,
44 | Email: u.Email,
45 | Role: u.Role,
46 | }, nil
47 | }
48 |
49 | // UserList fetches the existing users
50 | func (srv *Management) UserList() ([]domain.User, error) {
51 | users, err := srv.DB.UserList()
52 | if err != nil {
53 | return nil, err
54 | }
55 |
56 | uu := []domain.User{}
57 |
58 | for _, u := range users {
59 | uu = append(uu, domain.User{
60 | ID: u.ID,
61 | Name: u.Name,
62 | Username: u.Username,
63 | Email: u.Email,
64 | Role: u.Role,
65 | })
66 | }
67 | return uu, nil
68 | }
69 |
70 | // CreateUser creates a new user
71 | func (srv *Management) CreateUser(user domain.User) error {
72 | u := datastore.User{
73 | Username: user.Username,
74 | Name: user.Name,
75 | Email: user.Email,
76 | Role: user.Role,
77 | }
78 |
79 | _, err := srv.DB.CreateUser(u)
80 | return err
81 | }
82 |
83 | // UserUpdate updates a new user
84 | func (srv *Management) UserUpdate(user domain.User) error {
85 | u := datastore.User{
86 | ID: user.ID,
87 | Username: user.Username,
88 | Name: user.Name,
89 | Email: user.Email,
90 | Role: user.Role,
91 | }
92 |
93 | return srv.DB.UserUpdate(u)
94 | }
95 |
96 | // UserDelete removes a user
97 | func (srv *Management) UserDelete(username string) error {
98 | return srv.DB.UserDelete(username)
99 | }
100 |
--------------------------------------------------------------------------------
/static/app.html:
--------------------------------------------------------------------------------
1 |
{{.Title}}
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-brands-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-brands-400.eot
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-brands-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-brands-400.woff
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-regular-400.eot
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-regular-400.woff
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/static/font-awesome/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/font-awesome/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/static/images/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/images/ajax-loader.gif
--------------------------------------------------------------------------------
/static/images/checkbox_checked_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/images/checkbox_checked_16.png
--------------------------------------------------------------------------------
/static/images/checkbox_unchecked_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/images/checkbox_unchecked_16.png
--------------------------------------------------------------------------------
/static/images/chevron-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/images/chevron-down.png
--------------------------------------------------------------------------------
/static/images/chevron-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/images/chevron-up.png
--------------------------------------------------------------------------------
/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/static/images/favicon.ico
--------------------------------------------------------------------------------
/static/images/navigation-menu-plain.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | image/svg+xml
4 |
--------------------------------------------------------------------------------
/static/js/2.3f69199a.chunk.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | * Determine if an object is a Buffer
9 | *
10 | * @author Feross Aboukhadijeh
11 | * @license MIT
12 | */
13 |
14 | /** @license React v0.16.2
15 | * scheduler.production.min.js
16 | *
17 | * Copyright (c) Facebook, Inc. and its affiliates.
18 | *
19 | * This source code is licensed under the MIT license found in the
20 | * LICENSE file in the root directory of this source tree.
21 | */
22 |
23 | /** @license React v16.10.2
24 | * react-dom.production.min.js
25 | *
26 | * Copyright (c) Facebook, Inc. and its affiliates.
27 | *
28 | * This source code is licensed under the MIT license found in the
29 | * LICENSE file in the root directory of this source tree.
30 | */
31 |
32 | /** @license React v16.10.2
33 | * react.production.min.js
34 | *
35 | * Copyright (c) Facebook, Inc. and its affiliates.
36 | *
37 | * This source code is licensed under the MIT license found in the
38 | * LICENSE file in the root directory of this source tree.
39 | */
40 |
--------------------------------------------------------------------------------
/static/js/runtime-main.00ed32f5.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(r){for(var n,p,l=r[0],a=r[1],f=r[2],c=0,s=[];c .
18 | */
19 |
20 | package twinapi
21 |
22 | import (
23 | "encoding/json"
24 | "path"
25 |
26 | "github.com/canonical/iot-devicetwin/web"
27 | )
28 |
29 | // ActionList fetches actions for a device
30 | func (a *ClientAdapter) ActionList(orgID, deviceID string) web.ActionsResponse {
31 | r := web.ActionsResponse{}
32 | p := path.Join("device", orgID, deviceID, "actions")
33 |
34 | resp, err := get(a.urlPath(p))
35 | if err != nil {
36 | r.StandardResponse.Message = err.Error()
37 | return r
38 | }
39 |
40 | // Parse the response
41 | err = json.NewDecoder(resp.Body).Decode(&r)
42 | if err != nil {
43 | r.StandardResponse.Message = err.Error()
44 | }
45 | return r
46 | }
47 |
--------------------------------------------------------------------------------
/twinapi/action_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package twinapi
21 |
22 | import (
23 | "testing"
24 | )
25 |
26 | func TestClientAdapter_ActionList(t *testing.T) {
27 | b1 := `{"actions": [{"deviceId":"a111"}]}`
28 | type fields struct {
29 | URL string
30 | }
31 | type args struct {
32 | orgID string
33 | deviceID string
34 | body string
35 | }
36 | tests := []struct {
37 | name string
38 | fields fields
39 | args args
40 | want int
41 | wantErr string
42 | }{
43 | {"valid", fields{""}, args{"abc", "a111", b1}, 1, ""},
44 | {"invalid-org", fields{""}, args{"invalid", "a111", b1}, 0, "MOCK error get"},
45 | {"invalid-body", fields{""}, args{"abc", "a111", ""}, 0, "EOF"},
46 | }
47 | for _, tt := range tests {
48 | t.Run(tt.name, func(t *testing.T) {
49 | mockHTTP(tt.args.body)
50 | a := &ClientAdapter{
51 | URL: tt.fields.URL,
52 | }
53 | got := a.ActionList(tt.args.orgID, tt.args.deviceID)
54 | if got.Message != tt.wantErr {
55 | t.Errorf("ClientAdapter.DeviceGet() = %v, want %v", got.Message, tt.wantErr)
56 | }
57 | if len(got.Actions) != tt.want {
58 | t.Errorf("ClientAdapter.DeviceGet() = %v, want %v", len(got.Actions), tt.want)
59 | }
60 | })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/twinapi/device.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package twinapi
21 |
22 | import (
23 | "encoding/json"
24 | "path"
25 |
26 | "github.com/canonical/iot-devicetwin/web"
27 | )
28 |
29 | // DeviceList lists the devices for an account
30 | func (a *ClientAdapter) DeviceList(orgID string) web.DevicesResponse {
31 | r := web.DevicesResponse{}
32 | p := path.Join("device", orgID)
33 |
34 | resp, err := get(a.urlPath(p))
35 | if err != nil {
36 | r.StandardResponse.Message = err.Error()
37 | return r
38 | }
39 |
40 | // Parse the response
41 | err = json.NewDecoder(resp.Body).Decode(&r)
42 | if err != nil {
43 | r.StandardResponse.Message = err.Error()
44 | }
45 | return r
46 | }
47 |
48 | // DeviceGet fetches a device for an account
49 | func (a *ClientAdapter) DeviceGet(orgID, deviceID string) web.DeviceResponse {
50 | r := web.DeviceResponse{}
51 | p := path.Join("device", orgID, deviceID)
52 |
53 | resp, err := get(a.urlPath(p))
54 | if err != nil {
55 | r.StandardResponse.Message = err.Error()
56 | return r
57 | }
58 |
59 | // Parse the response
60 | err = json.NewDecoder(resp.Body).Decode(&r)
61 | if err != nil {
62 | r.StandardResponse.Message = err.Error()
63 | }
64 | return r
65 | }
66 |
--------------------------------------------------------------------------------
/twinapi/device_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package twinapi
21 |
22 | import (
23 | "testing"
24 | )
25 |
26 | func TestClientAdapter_DeviceList(t *testing.T) {
27 | b1 := `{"devices": [{"deviceId":"a111"}]}`
28 | type fields struct {
29 | URL string
30 | }
31 | type args struct {
32 | orgID string
33 | body string
34 | }
35 | tests := []struct {
36 | name string
37 | fields fields
38 | args args
39 | want int
40 | wantErr string
41 | }{
42 | {"valid", fields{""}, args{"abc", b1}, 1, ""},
43 | {"invalid-org", fields{""}, args{"invalid", b1}, 0, "MOCK error get"},
44 | {"invalid-body", fields{""}, args{"abc", ""}, 0, "EOF"},
45 | }
46 | for _, tt := range tests {
47 | t.Run(tt.name, func(t *testing.T) {
48 | mockHTTP(tt.args.body)
49 | a := &ClientAdapter{
50 | URL: tt.fields.URL,
51 | }
52 | got := a.DeviceList(tt.args.orgID)
53 | if got.Message != tt.wantErr {
54 | t.Errorf("ClientAdapter.DeviceList() = %v, want %v", got.Message, tt.wantErr)
55 | }
56 | if len(got.Devices) != tt.want {
57 | t.Errorf("ClientAdapter.DeviceList() = %v, want %v", len(got.Devices), tt.want)
58 | }
59 | })
60 | }
61 | }
62 |
63 | func TestClientAdapter_DeviceGet(t *testing.T) {
64 | b1 := `{"device": {"deviceId":"a111"}}`
65 | type fields struct {
66 | URL string
67 | }
68 | type args struct {
69 | orgID string
70 | deviceID string
71 | body string
72 | }
73 | tests := []struct {
74 | name string
75 | fields fields
76 | args args
77 | want string
78 | wantErr string
79 | }{
80 | {"valid", fields{""}, args{"abc", "a111", b1}, "a111", ""},
81 | {"invalid-org", fields{""}, args{"invalid", "a111", b1}, "", "MOCK error get"},
82 | {"invalid-body", fields{""}, args{"abc", "a111", ""}, "", "EOF"},
83 | }
84 | for _, tt := range tests {
85 | t.Run(tt.name, func(t *testing.T) {
86 | mockHTTP(tt.args.body)
87 | a := &ClientAdapter{
88 | URL: tt.fields.URL,
89 | }
90 | got := a.DeviceGet(tt.args.orgID, tt.args.deviceID)
91 | if got.Message != tt.wantErr {
92 | t.Errorf("ClientAdapter.DeviceGet() = %v, want %v", got.Message, tt.wantErr)
93 | }
94 | if got.Device.DeviceID != tt.want {
95 | t.Errorf("ClientAdapter.DeviceGet() = %v, want %v", got.Device.DeviceID, tt.want)
96 | }
97 | })
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/twinapi/group.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package twinapi
21 |
22 | import (
23 | "encoding/json"
24 | "path"
25 |
26 | "github.com/canonical/iot-devicetwin/web"
27 | )
28 |
29 | // GroupList lists the device groups
30 | func (a *ClientAdapter) GroupList(orgID string) web.GroupsResponse {
31 | r := web.GroupsResponse{}
32 | p := path.Join("group", orgID)
33 |
34 | resp, err := get(a.urlPath(p))
35 | if err != nil {
36 | r.StandardResponse.Message = err.Error()
37 | return r
38 | }
39 |
40 | // Parse the response
41 | err = json.NewDecoder(resp.Body).Decode(&r)
42 | if err != nil {
43 | r.StandardResponse.Message = err.Error()
44 | }
45 | return r
46 | }
47 |
48 | // GroupCreate creates a device group
49 | func (a *ClientAdapter) GroupCreate(orgID string, body []byte) web.StandardResponse {
50 | r := web.StandardResponse{}
51 | p := path.Join("group", orgID)
52 |
53 | resp, err := post(a.urlPath(p), body)
54 | if err != nil {
55 | r.Message = err.Error()
56 | return r
57 | }
58 |
59 | // Parse the response
60 | err = json.NewDecoder(resp.Body).Decode(&r)
61 | if err != nil {
62 | r.Message = err.Error()
63 | }
64 | return r
65 | }
66 |
67 | // GroupDevices lists the devices for a group
68 | func (a *ClientAdapter) GroupDevices(orgID, name string) web.DevicesResponse {
69 | r := web.DevicesResponse{}
70 | p := path.Join("group", orgID, name, "devices")
71 |
72 | resp, err := get(a.urlPath(p))
73 | if err != nil {
74 | r.StandardResponse.Message = err.Error()
75 | return r
76 | }
77 |
78 | // Parse the response
79 | err = json.NewDecoder(resp.Body).Decode(&r)
80 | if err != nil {
81 | r.StandardResponse.Message = err.Error()
82 | }
83 | return r
84 | }
85 |
86 | // GroupExcludedDevices lists the devices for a group
87 | func (a *ClientAdapter) GroupExcludedDevices(orgID, name string) web.DevicesResponse {
88 | r := web.DevicesResponse{}
89 | p := path.Join("group", orgID, name, "devices", "excluded")
90 |
91 | resp, err := get(a.urlPath(p))
92 | if err != nil {
93 | r.StandardResponse.Message = err.Error()
94 | return r
95 | }
96 |
97 | // Parse the response
98 | err = json.NewDecoder(resp.Body).Decode(&r)
99 | if err != nil {
100 | r.StandardResponse.Message = err.Error()
101 | }
102 | return r
103 | }
104 |
105 | // GroupDeviceLink links a device with a group
106 | func (a *ClientAdapter) GroupDeviceLink(orgID, name, deviceID string) web.StandardResponse {
107 | r := web.StandardResponse{}
108 | p := path.Join("group", orgID, name, deviceID)
109 |
110 | resp, err := post(a.urlPath(p), []byte(""))
111 | if err != nil {
112 | r.Message = err.Error()
113 | return r
114 | }
115 |
116 | // Parse the response
117 | err = json.NewDecoder(resp.Body).Decode(&r)
118 | if err != nil {
119 | r.Message = err.Error()
120 | }
121 | return r
122 | }
123 |
124 | // GroupDeviceUnlink unlinks a device from a group
125 | func (a *ClientAdapter) GroupDeviceUnlink(orgID, name, deviceID string) web.StandardResponse {
126 | r := web.StandardResponse{}
127 | p := path.Join("group", orgID, name, deviceID)
128 |
129 | resp, err := delete(a.urlPath(p))
130 | if err != nil {
131 | r.Message = err.Error()
132 | return r
133 | }
134 |
135 | // Parse the response
136 | err = json.NewDecoder(resp.Body).Decode(&r)
137 | if err != nil {
138 | r.Message = err.Error()
139 | }
140 | return r
141 | }
142 |
--------------------------------------------------------------------------------
/twinapi/snap.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package twinapi
21 |
22 | import (
23 | "encoding/json"
24 | "path"
25 |
26 | "github.com/canonical/iot-devicetwin/web"
27 | )
28 |
29 | // SnapList lists the snaps for a device
30 | func (a *ClientAdapter) SnapList(orgID, deviceID string) web.SnapsResponse {
31 | r := web.SnapsResponse{}
32 | p := path.Join("device", orgID, deviceID, "snaps")
33 |
34 | resp, err := get(a.urlPath(p))
35 | if err != nil {
36 | r.StandardResponse.Message = err.Error()
37 | return r
38 | }
39 |
40 | // Parse the response
41 | err = json.NewDecoder(resp.Body).Decode(&r)
42 | if err != nil {
43 | r.StandardResponse.Message = err.Error()
44 | }
45 | return r
46 | }
47 |
48 | // SnapListOnDevice triggers snap list on a device
49 | func (a *ClientAdapter) SnapListOnDevice(orgID, deviceID string) web.StandardResponse {
50 | r := web.StandardResponse{}
51 | p := path.Join("device", orgID, deviceID, "snaps", "list")
52 |
53 | resp, err := post(a.urlPath(p), nil)
54 | if err != nil {
55 | r.Code = "SnapList"
56 | r.Message = err.Error()
57 | return r
58 | }
59 |
60 | // Parse the response
61 | err = json.NewDecoder(resp.Body).Decode(&r)
62 | if err != nil {
63 | r.Code = "SnapList"
64 | r.Message = err.Error()
65 | }
66 | return r
67 | }
68 |
69 | // SnapInstall installs a snap on a device
70 | func (a *ClientAdapter) SnapInstall(orgID, deviceID, snap string) web.StandardResponse {
71 | r := web.StandardResponse{}
72 | p := path.Join("device", orgID, deviceID, "snaps", snap)
73 |
74 | resp, err := post(a.urlPath(p), nil)
75 | if err != nil {
76 | r.Code = "SnapInstall"
77 | r.Message = err.Error()
78 | return r
79 | }
80 |
81 | // Parse the response
82 | err = json.NewDecoder(resp.Body).Decode(&r)
83 | if err != nil {
84 | r.Code = "SnapInstall"
85 | r.Message = err.Error()
86 | }
87 | return r
88 | }
89 |
90 | // SnapRemove uninstalls a snap on a device
91 | func (a *ClientAdapter) SnapRemove(orgID, deviceID, snap string) web.StandardResponse {
92 | r := web.StandardResponse{}
93 | p := path.Join("device", orgID, deviceID, "snaps", snap)
94 |
95 | resp, err := delete(a.urlPath(p))
96 | if err != nil {
97 | r.Code = "SnapRemove"
98 | r.Message = err.Error()
99 | return r
100 | }
101 |
102 | // Parse the response
103 | err = json.NewDecoder(resp.Body).Decode(&r)
104 | if err != nil {
105 | r.Code = "SnapRemove"
106 | r.Message = err.Error()
107 | }
108 | return r
109 | }
110 |
111 | // SnapUpdate enables/disables/refreshes a snap on a device
112 | func (a *ClientAdapter) SnapUpdate(orgID, deviceID, snap, action string) web.StandardResponse {
113 | r := web.StandardResponse{}
114 | p := path.Join("device", orgID, deviceID, "snaps", snap, action)
115 |
116 | resp, err := put(a.urlPath(p), nil)
117 | if err != nil {
118 | r.Code = "SnapUpdate"
119 | r.Message = err.Error()
120 | return r
121 | }
122 |
123 | // Parse the response
124 | err = json.NewDecoder(resp.Body).Decode(&r)
125 | if err != nil {
126 | r.Code = "SnapUpdate"
127 | r.Message = err.Error()
128 | }
129 | return r
130 | }
131 |
132 | // SnapConfigSet sets a snap config on a device
133 | func (a *ClientAdapter) SnapConfigSet(orgID, deviceID, snap string, config []byte) web.StandardResponse {
134 | r := web.StandardResponse{}
135 | p := path.Join("device", orgID, deviceID, "snaps", snap, "settings")
136 |
137 | resp, err := put(a.urlPath(p), config)
138 | if err != nil {
139 | r.Code = "SnapUpdate"
140 | r.Message = err.Error()
141 | return r
142 | }
143 |
144 | // Parse the response
145 | err = json.NewDecoder(resp.Body).Decode(&r)
146 | if err != nil {
147 | r.Code = "SnapUpdate"
148 | r.Message = err.Error()
149 | }
150 | return r
151 | }
152 |
--------------------------------------------------------------------------------
/twinapi/twinapi.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package twinapi
21 |
22 | import (
23 | "bytes"
24 | "net/http"
25 | "net/url"
26 | "path"
27 |
28 | "github.com/canonical/iot-devicetwin/web"
29 | )
30 |
31 | // Client is a client for the device twin API
32 | type Client interface {
33 | DeviceList(orgID string) web.DevicesResponse
34 | DeviceGet(orgID, deviceID string) web.DeviceResponse
35 | ActionList(orgID, deviceID string) web.ActionsResponse
36 | SnapList(orgID, deviceID string) web.SnapsResponse
37 |
38 | SnapListOnDevice(orgID, deviceID string) web.StandardResponse
39 | SnapInstall(orgID, deviceID, snap string) web.StandardResponse
40 | SnapRemove(orgID, deviceID, snap string) web.StandardResponse
41 | SnapUpdate(orgID, deviceID, snap, action string) web.StandardResponse
42 | SnapConfigSet(orgID, deviceID, snap string, config []byte) web.StandardResponse
43 |
44 | GroupList(orgID string) web.GroupsResponse
45 | GroupCreate(orgID string, body []byte) web.StandardResponse
46 | GroupDevices(orgID, name string) web.DevicesResponse
47 | GroupExcludedDevices(orgID, name string) web.DevicesResponse
48 | GroupDeviceLink(orgID, name, deviceID string) web.StandardResponse
49 | GroupDeviceUnlink(orgID, name, deviceID string) web.StandardResponse
50 | }
51 |
52 | // ClientAdapter adapts our expectations to device twin API
53 | type ClientAdapter struct {
54 | URL string
55 | }
56 |
57 | var adapter *ClientAdapter
58 |
59 | // NewClientAdapter creates an adapter to access the device twin service
60 | func NewClientAdapter(u string) (*ClientAdapter, error) {
61 | if adapter == nil {
62 | adapter = &ClientAdapter{URL: u}
63 | }
64 | return adapter, nil
65 | }
66 |
67 | func (a *ClientAdapter) urlPath(p string) string {
68 | u, _ := url.Parse(a.URL)
69 | u.Path = path.Join(u.Path, p)
70 | return u.String()
71 | }
72 |
73 | var get = func(p string) (*http.Response, error) {
74 | return http.Get(p)
75 | }
76 |
77 | var post = func(p string, data []byte) (*http.Response, error) {
78 | return http.Post(p, "application/json", bytes.NewReader(data))
79 | }
80 |
81 | var put = func(p string, data []byte) (*http.Response, error) {
82 | client := &http.Client{}
83 | req, err := http.NewRequest(http.MethodPut, p, bytes.NewReader(data))
84 | if err != nil {
85 | return nil, err
86 | }
87 | return client.Do(req)
88 | }
89 |
90 | var delete = func(p string) (*http.Response, error) {
91 | client := &http.Client{}
92 | req, err := http.NewRequest(http.MethodDelete, p, nil)
93 | if err != nil {
94 | return nil, err
95 | }
96 | return client.Do(req)
97 | }
98 |
--------------------------------------------------------------------------------
/web/auth.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "errors"
24 | "net/http"
25 |
26 | "github.com/canonical/iot-management/datastore"
27 | "github.com/canonical/iot-management/web/usso"
28 |
29 | "github.com/dgrijalva/jwt-go"
30 | )
31 |
32 | func (wb Service) checkIsStandardAndGetUserFromJWT(w http.ResponseWriter, r *http.Request) (datastore.User, error) {
33 | return wb.checkPermissionsAndGetUserFromJWT(w, r, datastore.Standard)
34 | }
35 |
36 | func (wb Service) checkIsAdminAndGetUserFromJWT(w http.ResponseWriter, r *http.Request) (datastore.User, error) {
37 | return wb.checkPermissionsAndGetUserFromJWT(w, r, datastore.Admin)
38 | }
39 |
40 | func (wb Service) checkIsSuperuserAndGetUserFromJWT(w http.ResponseWriter, r *http.Request) (datastore.User, error) {
41 | return wb.checkPermissionsAndGetUserFromJWT(w, r, datastore.Superuser)
42 | }
43 |
44 | func (wb Service) checkPermissionsAndGetUserFromJWT(w http.ResponseWriter, r *http.Request, minimumAuthorizedRole int) (datastore.User, error) {
45 | user, err := wb.getUserFromJWT(w, r)
46 | if err != nil {
47 | return user, err
48 | }
49 | err = checkUserPermissions(user, minimumAuthorizedRole)
50 | if err != nil {
51 | return user, err
52 | }
53 | return user, nil
54 | }
55 |
56 | func (wb Service) getUserFromJWT(w http.ResponseWriter, r *http.Request) (datastore.User, error) {
57 | token, err := wb.JWTCheck(w, r)
58 | if err != nil {
59 | return datastore.User{}, err
60 | }
61 |
62 | // Null token is invalid
63 | if token == nil {
64 | return datastore.User{}, errors.New("No JWT provided")
65 | }
66 |
67 | claims := token.Claims.(jwt.MapClaims)
68 | username := claims[usso.ClaimsUsername].(string)
69 | role := int(claims[usso.ClaimsRole].(float64))
70 |
71 | return datastore.User{
72 | Username: username,
73 | Role: role,
74 | }, nil
75 | }
76 |
77 | func checkUserPermissions(user datastore.User, minimumAuthorizedRole int) error {
78 | if user.Role < minimumAuthorizedRole {
79 | return errors.New("The user is not authorized")
80 | }
81 | return nil
82 | }
83 |
--------------------------------------------------------------------------------
/web/handlers_app.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "encoding/json"
24 | "log"
25 | "net/http"
26 | "strings"
27 | "text/template"
28 |
29 | "github.com/canonical/iot-management/config"
30 |
31 | "github.com/gorilla/csrf"
32 | )
33 |
34 | var indexTemplate = "/static/app.html"
35 |
36 | // Page is the page details for the web application
37 | type Page struct {
38 | Title string
39 | Logo string
40 | }
41 |
42 | // VersionResponse is the JSON response from the API Version method
43 | type VersionResponse struct {
44 | Version string `json:"version"`
45 | }
46 |
47 | // IndexHandler is the front page of the web application
48 | func (wb Service) IndexHandler(w http.ResponseWriter, r *http.Request) {
49 | page := Page{Title: "IoT Management Service", Logo: ""}
50 |
51 | path := []string{".", indexTemplate}
52 | t, err := template.ParseFiles(strings.Join(path, ""))
53 | if err != nil {
54 | log.Printf("Error loading the application template: %v\n", err)
55 | http.Error(w, err.Error(), http.StatusInternalServerError)
56 | return
57 | }
58 | err = t.Execute(w, page)
59 | if err != nil {
60 | http.Error(w, err.Error(), http.StatusInternalServerError)
61 | }
62 | }
63 |
64 | // VersionHandler is the API method to return the version of the web
65 | func (wb Service) VersionHandler(w http.ResponseWriter, r *http.Request) {
66 | w.Header().Set("Content-Type", JSONHeader)
67 | w.WriteHeader(http.StatusOK)
68 |
69 | response := VersionResponse{Version: config.Version}
70 |
71 | // Encode the response as JSON
72 | if err := json.NewEncoder(w).Encode(response); err != nil {
73 | log.Printf("Error encoding the version response: %v\n", err)
74 | }
75 | }
76 |
77 | // TokenHandler returns CSRF protection new token in a X-CSRF-Token response header
78 | // This method is also used by the /authtoken endpoint to return the JWT. The method
79 | // indicates to the UI whether OpenID user auth is enabled
80 | func (wb Service) TokenHandler(w http.ResponseWriter, r *http.Request) {
81 | w.Header().Set("Content-Type", JSONHeader)
82 | w.Header().Set("X-CSRF-Token", csrf.Token(r))
83 |
84 | // Check the JWT and return it in the authorization header, if valid
85 | wb.JWTCheck(w, r)
86 |
87 | response := VersionResponse{Version: config.Version}
88 |
89 | // Encode the response as JSON
90 | if err := json.NewEncoder(w).Encode(response); err != nil {
91 | log.Printf("Error encoding the token response: %v", err)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/web/handlers_app_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "net/http"
24 | "testing"
25 |
26 | "github.com/canonical/iot-management/datastore/memory"
27 | "github.com/canonical/iot-management/service/manage"
28 | )
29 |
30 | func TestService_IndexHandler(t *testing.T) {
31 | tests := []struct {
32 | name string
33 | template string
34 | want int
35 | }{
36 | {"valid", "/../static/app.html", http.StatusOK},
37 | {"invalid-template", "/does-not-exist.html", http.StatusInternalServerError},
38 | }
39 | for _, tt := range tests {
40 | t.Run(tt.name, func(t *testing.T) {
41 | indexTemplate = tt.template
42 | db := memory.NewStore()
43 | wb := NewService(getSettings(), manage.NewMockManagement(db))
44 | w := sendRequest("GET", "/", nil, wb, "jamesj", wb.Settings.JwtSecret, 100)
45 | if w.Code != tt.want {
46 | t.Errorf("Expected HTTP status '%d', got: %v", tt.want, w.Code)
47 | }
48 | })
49 | }
50 | }
51 |
52 | func TestService_VersionTokenHandler(t *testing.T) {
53 | tests := []struct {
54 | name string
55 | }{
56 | {"valid"},
57 | }
58 | for _, tt := range tests {
59 | t.Run(tt.name, func(t *testing.T) {
60 | db := memory.NewStore()
61 | wb := NewService(getSettings(), manage.NewMockManagement(db))
62 | w := sendRequest("GET", "/v1/version", nil, wb, "jamesj", wb.Settings.JwtSecret, 100)
63 | if w.Code != http.StatusOK {
64 | t.Errorf("Expected HTTP status '%d', got: %v", http.StatusOK, w.Code)
65 | }
66 |
67 | w = sendRequest("GET", "/v1/token", nil, wb, "jamesj", wb.Settings.JwtSecret, 100)
68 | if w.Code != http.StatusOK {
69 | t.Errorf("Expected HTTP status '%d', got: %v", http.StatusOK, w.Code)
70 | }
71 | })
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/web/handlers_devices.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "net/http"
24 |
25 | dtwin "github.com/canonical/iot-devicetwin/web"
26 | "github.com/gorilla/mux"
27 | )
28 |
29 | func formatStandardResponse(errorCode, message string, w http.ResponseWriter) {
30 | response := dtwin.StandardResponse{Code: errorCode, Message: message}
31 | if len(errorCode) > 0 {
32 | w.WriteHeader(http.StatusBadRequest)
33 | }
34 |
35 | _ = encodeResponse(response, w)
36 | }
37 |
38 | // DevicesListHandler is the API method to list the registered devices
39 | func (wb Service) DevicesListHandler(w http.ResponseWriter, r *http.Request) {
40 | w.Header().Set("Content-Type", JSONHeader)
41 | user, err := wb.checkIsStandardAndGetUserFromJWT(w, r)
42 | if err != nil {
43 | formatStandardResponse("UserAuth", "", w)
44 | return
45 | }
46 |
47 | vars := mux.Vars(r)
48 |
49 | // Get the devices
50 | response := wb.Manage.DeviceList(vars["orgid"], user.Username, user.Role)
51 | _ = encodeResponse(response, w)
52 | }
53 |
54 | // DeviceGetHandler is the API method to get a registered device
55 | func (wb Service) DeviceGetHandler(w http.ResponseWriter, r *http.Request) {
56 | w.Header().Set("Content-Type", JSONHeader)
57 | user, err := wb.checkIsAdminAndGetUserFromJWT(w, r)
58 | if err != nil {
59 | formatStandardResponse("UserAuth", "", w)
60 | return
61 | }
62 |
63 | vars := mux.Vars(r)
64 |
65 | // Get the device
66 | response := wb.Manage.DeviceGet(vars["orgid"], user.Username, user.Role, vars["deviceid"])
67 | _ = encodeResponse(response, w)
68 | }
69 |
70 | // ActionListHandler is the API method to get actions for a device
71 | func (wb Service) ActionListHandler(w http.ResponseWriter, r *http.Request) {
72 | w.Header().Set("Content-Type", JSONHeader)
73 | user, err := wb.checkIsAdminAndGetUserFromJWT(w, r)
74 | if err != nil {
75 | formatStandardResponse("UserAuth", "", w)
76 | return
77 | }
78 |
79 | vars := mux.Vars(r)
80 |
81 | // Get the device
82 | response := wb.Manage.ActionList(vars["orgid"], user.Username, user.Role, vars["deviceid"])
83 | _ = encodeResponse(response, w)
84 | }
85 |
--------------------------------------------------------------------------------
/web/handlers_devices_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "net/http"
24 | "testing"
25 |
26 | "github.com/canonical/iot-management/datastore/memory"
27 | "github.com/canonical/iot-management/service/manage"
28 | )
29 |
30 | func TestService_DeviceHandlers(t *testing.T) {
31 | tests := []struct {
32 | name string
33 | url string
34 | permissions int
35 | want int
36 | wantErr string
37 | }{
38 | {"valid", "/v1/abc/devices", 300, http.StatusOK, ""},
39 | {"invalid-permissions", "/v1/abc/devices", 0, http.StatusBadRequest, "UserAuth"},
40 |
41 | {"valid", "/v1/abc/devices/a111", 300, http.StatusOK, ""},
42 | {"invalid-permissions", "/v1/abc/devices/a111", 0, http.StatusBadRequest, "UserAuth"},
43 | }
44 | for _, tt := range tests {
45 | t.Run(tt.name, func(t *testing.T) {
46 | db := memory.NewStore()
47 | wb := NewService(getSettings(), manage.NewMockManagement(db))
48 | w := sendRequest("GET", tt.url, nil, wb, "jamesj", wb.Settings.JwtSecret, tt.permissions)
49 | if w.Code != tt.want {
50 | t.Errorf("Expected HTTP status '%d', got: %v", tt.want, w.Code)
51 | }
52 |
53 | resp, err := parseStandardResponse(w.Body)
54 | if err != nil {
55 | t.Errorf("Error parsing response: %v", err)
56 | }
57 | if resp.Code != tt.wantErr {
58 | t.Errorf("Web.DeviceHandlers() got = %v, want %v", resp.Code, tt.wantErr)
59 | }
60 | })
61 | }
62 | }
63 |
64 | func TestService_ActionListHandler(t *testing.T) {
65 | tests := []struct {
66 | name string
67 | url string
68 | permissions int
69 | want int
70 | wantErr string
71 | }{
72 | {"valid", "/v1/abc/devices/a111/actions", 300, http.StatusOK, ""},
73 | {"invalid-permissions", "/v1/abc/devices/a111/actions", 0, http.StatusBadRequest, "UserAuth"},
74 | }
75 | for _, tt := range tests {
76 | t.Run(tt.name, func(t *testing.T) {
77 | db := memory.NewStore()
78 | wb := NewService(getSettings(), manage.NewMockManagement(db))
79 | w := sendRequest("GET", tt.url, nil, wb, "jamesj", wb.Settings.JwtSecret, tt.permissions)
80 | if w.Code != tt.want {
81 | t.Errorf("Expected HTTP status '%d', got: %v", tt.want, w.Code)
82 | }
83 |
84 | resp, err := parseStandardResponse(w.Body)
85 | if err != nil {
86 | t.Errorf("Error parsing response: %v", err)
87 | }
88 | if resp.Code != tt.wantErr {
89 | t.Errorf("Web.ActionListHandler() got = %v, want %v", resp.Code, tt.wantErr)
90 | }
91 | })
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/web/handlers_store.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "fmt"
24 | "github.com/gorilla/mux"
25 | "io/ioutil"
26 | "net/http"
27 | )
28 |
29 | // StoreSearchHandler fetches the available snaps from the store
30 | func (wb Service) StoreSearchHandler(w http.ResponseWriter, r *http.Request) {
31 | w.Header().Set("Content-Type", JSONHeader)
32 |
33 | vars := mux.Vars(r)
34 |
35 | resp, err := get(wb.Settings.StoreURL + "snaps/search?q=" + vars["snapName"])
36 | if err != nil {
37 | fmt.Fprint(w, "{}")
38 | return
39 | }
40 | defer resp.Body.Close()
41 |
42 | body, err := ioutil.ReadAll(resp.Body)
43 | if err != nil {
44 | fmt.Fprint(w, "{}")
45 | return
46 | }
47 |
48 | fmt.Fprint(w, string(body))
49 | }
50 |
51 | var get = func(u string) (*http.Response, error) {
52 | return http.Get(u)
53 | }
54 |
--------------------------------------------------------------------------------
/web/handlers_store_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "fmt"
24 | "io/ioutil"
25 | "net/http"
26 | "strings"
27 | "testing"
28 |
29 | "github.com/canonical/iot-management/datastore/memory"
30 | "github.com/canonical/iot-management/service/manage"
31 | )
32 |
33 | func TestService_StoreSearchHandler(t *testing.T) {
34 | tests := []struct {
35 | name string
36 | url string
37 | permissions int
38 | want int
39 | }{
40 | {"valid", "/v1/store/snaps/helloworld", 300, http.StatusOK},
41 | {"invalid-response", "/v1/store/snaps/invalid", 300, http.StatusOK},
42 | }
43 | for _, tt := range tests {
44 | t.Run(tt.name, func(t *testing.T) {
45 | mockGET(`[{}]`)
46 | db := memory.NewStore()
47 | wb := NewService(getSettings(), manage.NewMockManagement(db))
48 | w := sendRequest("GET", tt.url, nil, wb, "jamesj", wb.Settings.JwtSecret, tt.permissions)
49 | if w.Code != tt.want {
50 | t.Errorf("Expected HTTP status '%d', got: %v", tt.want, w.Code)
51 | }
52 | })
53 | }
54 | }
55 |
56 | func mockGET(body string) {
57 | // Mock the HTTP methods
58 | get = func(p string) (*http.Response, error) {
59 | if strings.Contains(p, "invalid") {
60 | return nil, fmt.Errorf("MOCK error get")
61 | }
62 | return &http.Response{
63 | Body: ioutil.NopCloser(strings.NewReader(body)),
64 | }, nil
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/web/login.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "html/template"
24 | "log"
25 | "net/http"
26 |
27 | "github.com/canonical/iot-management/datastore"
28 | "github.com/canonical/iot-management/web/usso"
29 | )
30 |
31 | // LoginHandler processes the login for Ubuntu SSO
32 | func (wb Service) LoginHandler(w http.ResponseWriter, r *http.Request) {
33 | // Get the openid nonce store
34 | nonce := wb.Manage.OpenIDNonceStore()
35 |
36 | // Call the openid login handler
37 | resp, req, username, err := usso.LoginHandler(wb.Settings, nonce, w, r)
38 | if err != nil {
39 | log.Printf("Error verifying the OpenID response: %v\n", err)
40 | replyHTTPError(w, http.StatusBadRequest, err)
41 | return
42 | }
43 | if req != nil {
44 | // Redirect is handled by the SSO handler
45 | return
46 | }
47 |
48 | // Check that the user is registered
49 | user, err := wb.Manage.GetUser(username)
50 | if err != nil {
51 | // Cannot find the user, so redirect to the login page
52 | log.Printf("Error retrieving user from datastore: %v\n", err)
53 | http.Redirect(w, r, "/notfound", http.StatusTemporaryRedirect)
54 | return
55 | }
56 |
57 | // Verify role value is valid
58 | if user.Role != datastore.Standard && user.Role != datastore.Admin && user.Role != datastore.Superuser {
59 | log.Printf("Role obtained from database for user %v has not a valid value: %v\n", username, user.Role)
60 | http.Redirect(w, r, "/notfound", http.StatusTemporaryRedirect)
61 | return
62 | }
63 |
64 | // Build the JWT
65 | jwtToken, err := usso.NewJWTToken(wb.Settings.JwtSecret, resp, user.Role)
66 | if err != nil {
67 | // Unexpected that this should occur, so leave the detailed response
68 | log.Printf("Error creating the JWT: %v", err)
69 | replyHTTPError(w, http.StatusBadRequest, err)
70 | return
71 | }
72 |
73 | // Set a cookie with the JWT
74 | usso.AddJWTCookie(jwtToken, w)
75 |
76 | // Redirect to the homepage with the JWT
77 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
78 | }
79 |
80 | // LogoutHandler logs the user out by removing the cookie and the JWT authorization header
81 | func (wb Service) LogoutHandler(w http.ResponseWriter, r *http.Request) {
82 | usso.LogoutHandler(w, r)
83 | }
84 |
85 | func replyHTTPError(w http.ResponseWriter, returnCode int, err error) {
86 | w.Header().Set("ContentType", "text/html")
87 | w.WriteHeader(returnCode)
88 | errorTemplate.Execute(w, err)
89 | }
90 |
91 | var errorTemplate = template.Must(template.New("failure").Parse(`
92 | Login Error
93 | {{.}}
94 |
95 | `))
96 |
--------------------------------------------------------------------------------
/web/login_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "fmt"
24 | "net/http"
25 | "net/url"
26 | "testing"
27 |
28 | "github.com/canonical/iot-management/config"
29 | "github.com/canonical/iot-management/datastore/memory"
30 | "github.com/canonical/iot-management/service/manage"
31 | "github.com/juju/usso"
32 | )
33 |
34 | func TestLoginHandlerUSSORedirect(t *testing.T) {
35 | // Mock the services
36 | settings, _ := config.Config("../testing/memory.yaml")
37 | db := memory.NewStore()
38 | m := manage.NewMockManagement(db)
39 | wb := NewService(settings, m)
40 |
41 | w := sendRequest("GET", "/login", nil, wb, "jamesj", wb.Settings.JwtSecret, 100)
42 |
43 | if w.Code != http.StatusFound {
44 | t.Errorf("Expected HTTP status '302', got: %v", w.Code)
45 | }
46 |
47 | u, err := url.Parse(w.Header().Get("Location"))
48 | if err != nil {
49 | t.Errorf("Error Parsing the redirect URL: %v", u)
50 | return
51 | }
52 |
53 | // Check that the redirect is to the Ubuntu SSO service
54 | ssoURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
55 | if ssoURL != usso.ProductionUbuntuSSOServer.LoginURL() {
56 | t.Errorf("Unexpected redirect URL: %v", ssoURL)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/web/middleware.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "errors"
24 | "log"
25 | "net/http"
26 | "time"
27 |
28 | "github.com/canonical/iot-management/web/usso"
29 | "github.com/dgrijalva/jwt-go"
30 | )
31 |
32 | // Logger Handle logging for the web web
33 | func Logger(start time.Time, r *http.Request) {
34 | log.Printf(
35 | "%s\t%s\t%s",
36 | r.Method,
37 | r.RequestURI,
38 | time.Since(start),
39 | )
40 | }
41 |
42 | // Middleware to pre-process web web requests
43 | func Middleware(inner http.Handler) http.Handler {
44 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45 | start := time.Now()
46 |
47 | // Log the request
48 | Logger(start, r)
49 |
50 | inner.ServeHTTP(w, r)
51 | })
52 | }
53 |
54 | // JWTCheck extracts the JWT from the request, validates it and returns the token
55 | func (wb Service) JWTCheck(w http.ResponseWriter, r *http.Request) (*jwt.Token, error) {
56 |
57 | // Get the JWT from the header or cookie
58 | jwtToken, err := usso.JWTExtractor(r)
59 | if err != nil {
60 | log.Println("Error in JWT extraction:", err.Error())
61 | return nil, errors.New("error in retrieving the authentication token")
62 | }
63 |
64 | // Verify the JWT string
65 | token, err := usso.VerifyJWT(wb.Settings.JwtSecret, jwtToken)
66 | if err != nil {
67 | log.Printf("JWT fails verification: %v", err.Error())
68 | return nil, errors.New("the authentication token is invalid")
69 | }
70 |
71 | if !token.Valid {
72 | log.Println("Invalid JWT")
73 | return nil, errors.New("the authentication token is invalid")
74 | }
75 |
76 | // Set up the bearer token in the header
77 | w.Header().Set("Authorization", "Bearer "+jwtToken)
78 |
79 | return token, nil
80 | }
81 |
--------------------------------------------------------------------------------
/web/response.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "encoding/json"
24 | "log"
25 | "net/http"
26 | )
27 |
28 | func encodeResponse(response interface{}, w http.ResponseWriter) error {
29 | // Encode the response as JSON
30 | if err := json.NewEncoder(w).Encode(response); err != nil {
31 | log.Println("Error forming the accounts response.")
32 | return err
33 | }
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/web/usso/constants.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package usso
21 |
22 | // ClaimsKey is the context key for the JWT claims
23 | var ClaimsKey struct{}
24 |
25 | // UserClaims holds the JWT custom claims for a user
26 | const (
27 | ClaimsIdentity = "identity"
28 | ClaimsUsername = "username"
29 | ClaimsEmail = "email"
30 | ClaimsName = "name"
31 | ClaimsRole = "role"
32 | ClaimsAccountFilter = "accountFilter"
33 | StandardClaimExpiresAt = "exp"
34 | )
35 |
36 | // JWTCookie is the name of the cookie used to store the JWT
37 | const JWTCookie = "X-Auth-Token"
38 |
--------------------------------------------------------------------------------
/web/usso/jwt.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package usso
21 |
22 | import (
23 | "errors"
24 | "log"
25 | "strings"
26 | "time"
27 |
28 | "net/http"
29 |
30 | "github.com/dgrijalva/jwt-go"
31 | "github.com/juju/usso/openid"
32 | )
33 |
34 | func createJWT(jwtSecret, username, name, email, identity string, role int, expires int64) (string, error) {
35 | token := jwt.New(jwt.SigningMethodHS256)
36 |
37 | claims := token.Claims.(jwt.MapClaims)
38 | claims[ClaimsUsername] = username
39 | claims[ClaimsName] = name
40 | claims[ClaimsEmail] = email
41 | claims[ClaimsIdentity] = identity
42 | claims[ClaimsRole] = role
43 | //claims[ClaimsAccountFilter] = datastore.Environ.Config.AccountFilter
44 | claims[StandardClaimExpiresAt] = expires
45 |
46 | if len(jwtSecret) == 0 {
47 | return "", errors.New("JWT secret empty value. Please configure it properly")
48 | }
49 |
50 | tokenString, err := token.SignedString([]byte(jwtSecret))
51 | if err != nil {
52 | log.Printf("Error signing the JWT: %v", err.Error())
53 | }
54 | return tokenString, err
55 | }
56 |
57 | // NewJWTToken creates a new JWT from the verified OpenID response
58 | func NewJWTToken(jwtSecret string, resp *openid.Response, role int) (string, error) {
59 |
60 | return createJWT(jwtSecret, resp.SReg["nickname"], resp.SReg["fullname"], resp.SReg["email"], resp.ID, role, time.Now().Add(time.Hour*24).Unix())
61 | }
62 |
63 | // VerifyJWT checks that we have a valid token
64 | func VerifyJWT(jwtSecret, jwtToken string) (*jwt.Token, error) {
65 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (i interface{}, e error) {
66 | if len(jwtSecret) == 0 {
67 | return []byte{}, errors.New("JWT secret empty value. Please configure it properly")
68 | }
69 | return []byte(jwtSecret), nil
70 | })
71 |
72 | return token, err
73 | }
74 |
75 | // AddJWTCookie sets the JWT as a cookie
76 | func AddJWTCookie(jwtToken string, w http.ResponseWriter) {
77 | // Set the JWT as a bearer token
78 | // (In practice, the cookie will be used more as clicking on a page link will not send the auth header)
79 | w.Header().Set("Authorization", "Bearer "+jwtToken)
80 |
81 | expireCookie := time.Now().Add(time.Hour * 1)
82 | cookie := http.Cookie{Name: JWTCookie, Value: jwtToken, Expires: expireCookie, HttpOnly: true}
83 | http.SetCookie(w, &cookie)
84 | }
85 |
86 | // JWTExtractor extracts the JWT from a request and returns the token string.
87 | // The token is not verified.
88 | func JWTExtractor(r *http.Request) (string, error) {
89 | // Get the JWT from the header
90 | jwtToken := r.Header.Get("Authorization")
91 | splitToken := strings.Split(jwtToken, " ")
92 | if len(splitToken) != 2 {
93 | jwtToken = ""
94 | } else {
95 | jwtToken = splitToken[1]
96 | }
97 |
98 | // Check the cookie if we don't have a bearer token in the header
99 | if jwtToken == "" {
100 | cookie, err := r.Cookie(JWTCookie)
101 | if err != nil {
102 | log.Println("Cannot find the JWT")
103 | return "", errors.New("Cannot find the JWT")
104 | }
105 | jwtToken = cookie.Value
106 | }
107 |
108 | return jwtToken, nil
109 | }
110 |
--------------------------------------------------------------------------------
/web/usso/jwt_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package usso
21 |
22 | import (
23 | "github.com/dgrijalva/jwt-go"
24 | "net/http"
25 | "net/http/httptest"
26 | "testing"
27 |
28 | "github.com/juju/usso/openid"
29 | )
30 |
31 | type testJWT struct {
32 | resp openid.Response
33 | expected []string
34 | role int
35 | }
36 |
37 | const jwtSecret = "jwt_secret"
38 |
39 | func TestNewJWTToken(t *testing.T) {
40 | test1 := testJWT{
41 | resp: openid.Response{ID: "id", Teams: []string{"teamone", "team2"}},
42 | expected: []string{"", "", ""},
43 | role: 100,
44 | }
45 | test2 := testJWT{
46 | resp: openid.Response{ID: "id", Teams: []string{"teamone", "team2"}, SReg: map[string]string{"nickname": "jwt"}},
47 | expected: []string{"jwt", "", ""},
48 | role: 200,
49 | }
50 | test3 := testJWT{
51 | resp: openid.Response{
52 | ID: "id",
53 | Teams: []string{"teamone", "team2"},
54 | SReg: map[string]string{"nickname": "jwt", "email": "jwt@example.com", "fullname": "John W Thompson"},
55 | },
56 | expected: []string{"jwt", "jwt@example.com", "John W Thompson"},
57 | role: 300,
58 | }
59 |
60 | for _, r := range []testJWT{test1, test2, test3} {
61 |
62 | // adding arbitrary role value for second parameter
63 | jwtToken, err := NewJWTToken(jwtSecret, &r.resp, r.role)
64 | if err != nil {
65 | t.Errorf("Error creating JWT: %v", err)
66 | }
67 |
68 | expectedToken(t, jwtToken, &r.resp, r.expected[0], r.expected[1], r.expected[2], r.role)
69 |
70 | }
71 | }
72 |
73 | func expectedToken(t *testing.T, jwtToken string, resp *openid.Response, username, email, name string, role int) {
74 | token, err := VerifyJWT(jwtSecret, jwtToken)
75 | if err != nil {
76 | t.Errorf("Error validating JWT: %v", err)
77 | }
78 |
79 | claims := token.Claims.(jwt.MapClaims)
80 | if claims[ClaimsIdentity] != resp.ID {
81 | t.Errorf("JWT ID does not match: %v", claims[ClaimsIdentity])
82 | }
83 | if claims[ClaimsUsername] != username {
84 | t.Errorf("JWT username does not match: %v", claims[ClaimsUsername])
85 | }
86 | if claims[ClaimsEmail] != email {
87 | t.Errorf("JWT email does not match: %v", claims[ClaimsEmail])
88 | }
89 | if claims[ClaimsName] != name {
90 | t.Errorf("JWT name does not match: %v", claims[ClaimsName])
91 | }
92 | if int(claims[ClaimsRole].(float64)) != role {
93 | t.Errorf("JWT role does not match: expected %v but got %v", role, claims[ClaimsRole])
94 | }
95 | }
96 |
97 | func testHandler(w http.ResponseWriter, r *http.Request) {
98 |
99 | }
100 |
101 | func TestAddJWTCookie(t *testing.T) {
102 | w := httptest.NewRecorder()
103 | AddJWTCookie("ThisShouldBeAJWT", w)
104 |
105 | // Copy the Cookie over to a new Request
106 | request := &http.Request{Header: http.Header{"Cookie": w.HeaderMap["Set-Cookie"]}}
107 |
108 | // Extract the cookie from the request
109 | jwtToken, err := JWTExtractor(request)
110 | if err != nil {
111 | t.Errorf("Error getting the JWT cookie: %v", err)
112 | }
113 | if jwtToken != "ThisShouldBeAJWT" {
114 | t.Errorf("Expected 'ThisShouldBeAJWT', got '%v'", jwtToken)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/web/usso/openid.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package usso
21 |
22 | import (
23 | "fmt"
24 | "log"
25 | "net/http"
26 | "strings"
27 | "time"
28 |
29 | "github.com/canonical/iot-management/config"
30 |
31 | "github.com/juju/usso"
32 | "github.com/juju/usso/openid"
33 | )
34 |
35 | var (
36 | teams = "" // e.g. ce-web-logs,canonical
37 | required = "email,fullname,nickname"
38 | optional = ""
39 | )
40 |
41 | var client *openid.Client
42 |
43 | // verify is used to perform the OpenID verification of the login
44 | // response. This is declared as a variable so it can be overridden for
45 | // testing.
46 | var verify func(string) (*openid.Response, error)
47 |
48 | func getClient(nonce openid.NonceStore) *openid.Client {
49 | if client != nil {
50 | return client
51 | }
52 | client = openid.NewClient(usso.ProductionUbuntuSSOServer, nonce, nil)
53 | verify = client.Verify
54 | return client
55 | }
56 |
57 | // LoginHandler processes the login for Ubuntu SSO
58 | func LoginHandler(settings *config.Settings, nonce openid.NonceStore, w http.ResponseWriter, r *http.Request) (*openid.Response, *openid.Request, string, error) {
59 | getClient(nonce)
60 | r.ParseForm()
61 |
62 | url := *r.URL
63 |
64 | // Set the return URL: from the OpenID login with the full domain name
65 | url.Scheme = settings.URLScheme
66 | url.Host = settings.URLHost
67 |
68 | if r.Form.Get("openid.ns") == "" {
69 | req := openid.Request{
70 | ReturnTo: url.String(),
71 | Teams: strings.FieldsFunc(teams, isComma),
72 | SRegRequired: strings.FieldsFunc(required, isComma),
73 | SRegOptional: strings.FieldsFunc(optional, isComma),
74 | }
75 | url := client.RedirectURL(&req)
76 | http.Redirect(w, r, url, http.StatusFound)
77 | return nil, &req, "", nil
78 | }
79 |
80 | resp, err := verify(url.String())
81 | if err != nil {
82 | // A mangled OpenID response is suspicious, so leave a nasty response
83 | return nil, nil, "", fmt.Errorf("error verifying the OpenID response: %v", err)
84 | }
85 |
86 | return resp, nil, r.Form.Get("openid.sreg.nickname"), nil
87 | }
88 |
89 | func isComma(c rune) bool {
90 | return c == ','
91 | }
92 |
93 | // LogoutHandler logs the user out by removing the cookie and the JWT authorization header
94 | func LogoutHandler(w http.ResponseWriter, r *http.Request) {
95 | // Remove the authorization header with contains the bearer token
96 | w.Header().Del("Authorization")
97 |
98 | // Create a new invalid token with an unauthorized user
99 | jwtToken, err := createJWT("INVALID", "INVALID", "Not Logged-In", "", "", 0, 0)
100 | if err != nil {
101 | log.Println("Error logging out:", err.Error())
102 | }
103 |
104 | // Update the cookie with the invalid token and expired date
105 | c, err := r.Cookie(JWTCookie)
106 | if err != nil {
107 | log.Println("Error logging out:", err.Error())
108 | }
109 | c.Value = jwtToken
110 | c.Expires = time.Now().AddDate(0, 0, -1)
111 |
112 | // Set the bearer token and the cookie
113 | http.SetCookie(w, c)
114 |
115 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
116 | }
117 |
--------------------------------------------------------------------------------
/web/web.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "fmt"
24 | "net/http"
25 |
26 | "github.com/canonical/iot-management/config"
27 | "github.com/canonical/iot-management/service/manage"
28 | )
29 |
30 | // JSONHeader is the content-type header for JSON responses
31 | const JSONHeader = "application/json; charset=UTF-8"
32 |
33 | // Service is the implementation of the web API
34 | type Service struct {
35 | Settings *config.Settings
36 | Manage manage.Manage
37 | }
38 |
39 | // NewService returns a new web controller
40 | func NewService(settings *config.Settings, srv manage.Manage) *Service {
41 | return &Service{
42 | Settings: settings,
43 | Manage: srv,
44 | }
45 | }
46 |
47 | // Run starts the web service
48 | func (wb Service) Run() error {
49 | fmt.Printf("Starting service on port :%s\n", wb.Settings.LocalPort)
50 | return http.ListenAndServe(":"+wb.Settings.LocalPort, wb.Router())
51 | }
52 |
--------------------------------------------------------------------------------
/web/web_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * This file is part of the IoT Management Service
5 | * Copyright 2019 Canonical Ltd.
6 | *
7 | * This program is free software: you can redistribute it and/or modify it
8 | * under the terms of the GNU Affero General Public License version 3, as
9 | * published by the Free Software Foundation.
10 | *
11 | * This program is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
13 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU Affero General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Affero General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package web
21 |
22 | import (
23 | "encoding/json"
24 | "fmt"
25 | "io"
26 | "log"
27 | "net/http"
28 | "net/http/httptest"
29 |
30 | "github.com/canonical/iot-devicetwin/web"
31 | "github.com/canonical/iot-management/config"
32 | "github.com/canonical/iot-management/web/usso"
33 | "github.com/juju/usso/openid"
34 | )
35 |
36 | var settings *config.Settings
37 |
38 | func getSettings() *config.Settings {
39 | if settings == nil {
40 | settings, _ = config.Config("../testing/memory.yaml")
41 | }
42 | return settings
43 | }
44 |
45 | func sendRequest(method, url string, data io.Reader, srv *Service, username, jwtSecret string, permissions int) *httptest.ResponseRecorder {
46 | w := httptest.NewRecorder()
47 | r, _ := http.NewRequest(method, url, data)
48 |
49 | if err := createJWTWithRole(username, jwtSecret, r, permissions); err != nil {
50 | log.Fatalf("Error creating JWT: %v", err)
51 | }
52 |
53 | srv.Router().ServeHTTP(w, r)
54 |
55 | return w
56 | }
57 |
58 | func createJWTWithRole(username, jwtSecret string, r *http.Request, role int) error {
59 | sreg := map[string]string{"nickname": username, "fullname": "JJ", "email": "jj@example.com"}
60 | resp := openid.Response{ID: "identity", Teams: []string{}, SReg: sreg}
61 | jwtToken, err := usso.NewJWTToken(jwtSecret, &resp, role)
62 | if err != nil {
63 | return fmt.Errorf("error creating a JWT: %v", err)
64 | }
65 | r.Header.Set("Authorization", "Bearer "+jwtToken)
66 | return nil
67 | }
68 |
69 | func parseStandardResponse(r io.Reader) (web.StandardResponse, error) {
70 | // Parse the response
71 | result := web.StandardResponse{}
72 | err := json.NewDecoder(r).Decode(&result)
73 | return result, err
74 | }
75 |
--------------------------------------------------------------------------------
/webapp/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 |
--------------------------------------------------------------------------------
/webapp/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | *.css
24 |
--------------------------------------------------------------------------------
/webapp/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Build the project
4 | npm run build
5 |
6 | # Create the static directory
7 | rm -rf ../static/css
8 | rm -rf ../static/js
9 | cp -R build/static/css ../static/
10 | cp -R build/static/js ../static/
11 | cp build/index.html ../static/app.html
12 |
13 | # cleanup
14 | rm -rf ./build
15 |
--------------------------------------------------------------------------------
/webapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webapp",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "0.19.0",
7 | "history": "^4.7.2",
8 | "jwt-decode": "^2.2.0",
9 | "moment": "^2.22.2",
10 | "react": "^16.6.3",
11 | "react-dom": "^16.6.3",
12 | "react-scripts": "^3.4.3"
13 | },
14 | "scripts": {
15 | "build-css": "sass --load-path=. src:src",
16 | "start": "npm run build-css && react-scripts start",
17 | "buildonly": "react-scripts build",
18 | "build": "npm run build-css&& react-scripts build",
19 | "test": "npm run build-css && react-scripts test --env=jsdom",
20 | "eject": "react-scripts eject"
21 | },
22 | "devDependencies": {
23 | "filesize": "5.0.3",
24 | "sass": "^1.32.11",
25 | "vanilla-framework": "2.4.0"
26 | },
27 | "browserslist": [
28 | ">0.2%",
29 | "not dead",
30 | "not ie <= 11",
31 | "not op_mini all"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/webapp/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/iot-management/88bcfc78850f3977477d8ea1a47e270f94d9c1f2/webapp/public/favicon.ico
--------------------------------------------------------------------------------
/webapp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{.Title}}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/webapp/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/webapp/src/App.test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import React from 'react';
19 | import ReactDOM from 'react-dom';
20 | import App from './App';
21 |
22 | it('renders without crashing', () => {
23 | const div = document.createElement('div');
24 | ReactDOM.render( , div);
25 | });
26 |
--------------------------------------------------------------------------------
/webapp/src/components/Accounts.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 |
19 | import React, {Component} from 'react';
20 | import AlertBox from './AlertBox';
21 | import {T, isUserAdmin, isUserSuperuser} from './Utils';
22 |
23 | class Accounts extends Component {
24 |
25 | constructor(props) {
26 | super(props);
27 | this.state = {
28 | message: null,
29 | accounts: [],
30 | };
31 | }
32 |
33 | renderTable(items) {
34 |
35 | if (items.length > 0) {
36 | return (
37 |
38 |
39 |
40 |
41 | {T('code')} {T('name')}
42 |
43 |
44 |
45 | {this.renderRows(items)}
46 |
47 |
48 |
49 | );
50 | } else {
51 | return (
52 | {T('no-accounts')}
53 | );
54 | }
55 | }
56 |
57 | renderRows(items) {
58 | return items.map((l) => {
59 | let isSuperuser = isUserSuperuser(this.props.token);
60 | return (
61 |
62 | {isSuperuser ? {l.orgid} : l.orgid}
63 | {l.name}
64 |
65 | );
66 | });
67 | }
68 |
69 | render () {
70 |
71 | if (!isUserAdmin(this.props.token)) {
72 | return (
73 |
76 | )
77 | }
78 | let isSuperuser = isUserSuperuser(this.props.token);
79 |
80 | return (
81 |
82 |
83 |
84 |
{T('accounts')}
85 | {isSuperuser ?
86 |
91 | : ''
92 | }
93 |
94 |
95 |
98 |
99 |
100 |
101 | {this.renderTable(this.props.accounts)}
102 |
103 |
104 | )
105 | }
106 |
107 | }
108 |
109 | export default Accounts;
110 |
--------------------------------------------------------------------------------
/webapp/src/components/Actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 |
19 | import React, {Component} from 'react';
20 | import AlertBox from './AlertBox';
21 | import {T} from './Utils';
22 |
23 | class Actions extends Component {
24 | constructor(props) {
25 | super(props)
26 | this.state = {
27 | name: null,
28 | groups: [],
29 | actions: [],
30 | }
31 | }
32 |
33 | refresh(orgid, name) {
34 | }
35 |
36 | handleGroupClick = (e) => {
37 | e.preventDefault();
38 |
39 | let selected = e.target.getAttribute('data-key')
40 | if (this.state.name === selected) {
41 | // Deselect the group
42 | this.setState({name: null, devices: [], devicesExcluded: []})
43 | } else {
44 | // Select the group
45 | this.setState({name: selected})
46 | this.refresh(this.props.account.orgid, selected)
47 | }
48 | }
49 |
50 | renderRowsGroups(items) {
51 | return items.map((l) => {
52 | let selected = (l.name===this.state.name) ? 'p-button--brand' : 'p-button--neutral'
53 | return (
54 |
55 |
56 | {l.name}
57 |
58 |
59 | );
60 | });
61 | }
62 |
63 | renderTableGroups(items) {
64 | if (!items) {
65 | return
66 | }
67 | if (items.length > 0) {
68 | return (
69 |
70 |
71 |
72 |
73 | {T('groups')}
74 |
75 |
76 |
77 | {this.renderRowsGroups(items)}
78 |
79 |
80 |
81 | );
82 | } else {
83 | return (
84 | {T('no-groups')}
85 | );
86 | }
87 | }
88 |
89 | render () {
90 | return (
91 |
92 |
93 | {T('groups')}
94 |
97 |
98 |
99 |
100 | {this.renderTableGroups(this.props.groups)}
101 |
102 |
103 | )
104 | }
105 | }
106 |
107 | export default Actions
108 |
--------------------------------------------------------------------------------
/webapp/src/components/AlertBox.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 |
19 | import React, {Component} from 'react';
20 |
21 |
22 | class AlertBox extends Component {
23 | render() {
24 | if (this.props.message) {
25 | var c = 'p-notification--';
26 | if (this.props.type) {
27 | c = c + this.props.type;
28 | } else {
29 | c = c + 'negative';
30 | }
31 |
32 | return (
33 |
34 |
{this.props.message}
35 |
36 | );
37 | } else {
38 | return ;
39 | }
40 | }
41 | }
42 |
43 | export default AlertBox;
44 |
--------------------------------------------------------------------------------
/webapp/src/components/Constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 |
19 | export const Role = {
20 | Standard: 100,
21 | Admin: 200,
22 | Superuser: 300,
23 | }
24 |
25 | export const LoadingImage = '/static/images/ajax-loader.gif'
26 |
27 | export const Status = {
28 | 1: 'waiting',
29 | 2: 'enrolled',
30 | 3: 'disabled',
31 | }
32 |
--------------------------------------------------------------------------------
/webapp/src/components/Devices.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 |
19 | import React, {Component} from 'react';
20 | import moment from 'moment';
21 | import AlertBox from './AlertBox';
22 | import {T} from './Utils';
23 |
24 |
25 | class Devices extends Component {
26 |
27 | getAge(m) {
28 | var start = moment(m);
29 | var end = moment()
30 | var dur = moment.duration(end.diff(start));
31 | var d = dur.asMinutes()
32 | if (d < 2) {
33 | return
34 | } else if (d < 5) {
35 | return
36 | } else {
37 | return
38 | }
39 | }
40 |
41 | renderTable(items) {
42 | if (items.length > 0) {
43 | return (
44 |
45 |
46 |
47 |
48 | {T('brand')} {T('model')} {T('serial')} {T('reg-date')} {T('last-update')}
49 |
50 |
51 |
52 | {this.renderRows(items)}
53 |
54 |
55 |
56 | );
57 | } else {
58 | return (
59 | {T('no-devices-connected')}
60 | );
61 | }
62 | }
63 |
64 | renderRows(items) {
65 | return items.map((l) => {
66 | return (
67 |
68 | {l.brand}
69 | {l.model}
70 | {l.serial}
71 | {moment(l.created).format('lll')}
72 |
73 | {moment(l.lastRefresh).format('lll')}
74 |
75 | {this.getAge(l.lastRefresh)}
76 |
77 |
78 | );
79 | });
80 | }
81 |
82 | render () {
83 | return (
84 |
85 |
86 | {T('devices-connected')}
87 |
90 |
91 |
92 |
93 | {this.renderTable(this.props.devices)}
94 |
95 |
96 | )
97 | }
98 |
99 | }
100 |
101 | export default Devices;
--------------------------------------------------------------------------------
/webapp/src/components/DialogBox.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 |
19 | import React, {Component} from 'react';
20 | import {T} from './Utils';
21 |
22 | class DialogBox extends Component {
23 |
24 | render() {
25 |
26 | if (this.props.message) {
27 | return (
28 |
29 |
{this.props.message}
30 |
31 |
32 | {T('cancel')}
33 |
34 |
35 | {T('ok')}
36 |
37 |
38 |
39 | );
40 | } else {
41 | return ;
42 | }
43 | }
44 | }
45 |
46 | export default DialogBox;
47 |
--------------------------------------------------------------------------------
/webapp/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import React, {Component} from 'react';
19 | import api from '../models/api';
20 | import {T} from './Utils';
21 |
22 |
23 | class Footer extends Component {
24 |
25 | constructor(props) {
26 | super(props)
27 | this.state = {version: ''};
28 |
29 | this.getVersion();
30 | }
31 |
32 | getVersion() {
33 | api.version().then(response => {
34 | this.setState({version: response.data.version})
35 | })
36 | }
37 |
38 | render() {
39 | return (
40 |
45 | );
46 | }
47 | }
48 |
49 | export default Footer;
50 |
--------------------------------------------------------------------------------
/webapp/src/components/Header.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import React, {Component} from 'react';
19 | import Navigation from './Navigation';
20 | import NavigationUser from './NavigationUser';
21 |
22 | class Header extends Component {
23 |
24 | render() {
25 | return (
26 |
52 | )
53 | }
54 | }
55 |
56 | export default Header;
57 |
--------------------------------------------------------------------------------
/webapp/src/components/If.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import React from 'react'
19 |
20 | export default function If({ cond, children }) {
21 | if (!cond) return null
22 | return (
23 | Array.isArray(children)
24 | ? ( {children}
)
25 | : children
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/webapp/src/components/Index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 |
19 | import React, { Component } from 'react';
20 | import {T, isLoggedIn} from './Utils'
21 | import AlertBox from './AlertBox'
22 |
23 |
24 | class Index extends Component {
25 |
26 | constructor(props) {
27 |
28 | super(props)
29 | this.state = {
30 | token: props.token || {},
31 | }
32 | }
33 |
34 | handleLogin = (e) => {
35 | window.location.pathname = '/login'
36 | }
37 |
38 | renderUser() {
39 | if (isLoggedIn(this.props.token)) {
40 | return
41 | } else {
42 | return (
43 |
44 | {T('login')}
45 |
46 | )
47 | }
48 | }
49 |
50 | renderError() {
51 | if (this.props.error) {
52 | return (
53 |
54 | )
55 | }
56 | }
57 |
58 | render() {
59 | return (
60 |
61 |
62 |
63 | {T('title')}
64 | {this.props.account.name}
65 | {this.renderError()}
66 |
67 |
68 | {T('site-description')}
69 |
70 |
71 |
72 |
73 |
74 | {this.renderUser()}
75 |
76 |
77 | );
78 | }
79 | }
80 |
81 | export default Index;
--------------------------------------------------------------------------------
/webapp/src/components/Navigation.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import React, {Component} from 'react';
19 | import {Role} from './Constants'
20 | import {T} from './Utils';
21 |
22 | const linksStandard = ['devices', 'groups'];
23 | const linksAdmin = ['devices', 'groups', 'accounts'];
24 | const linksSuperuser = ['devices', 'groups', 'accounts', 'users'];
25 |
26 | class Navigation extends Component {
27 |
28 | link(l) {
29 | if (this.props.sectionId) {
30 | // This is the secondary menu
31 | return '/' + this.props.section + '/' + this.props.sectionId + '/' + l;
32 | } else {
33 | return '/' + l;
34 | }
35 | }
36 |
37 | render() {
38 |
39 | var token = this.props.token
40 | var links;
41 |
42 | if (this.props.links) {
43 | links = this.props.links;
44 | } else {
45 | switch(token.role) {
46 | case Role.Admin:
47 | links = linksAdmin;
48 | break;
49 | case Role.Superuser:
50 | links = linksSuperuser;
51 | break;
52 | case Role.Standard:
53 | links = linksStandard
54 | break
55 | default:
56 | links = []
57 | }
58 | }
59 |
60 | return (
61 |
62 | {links.map((l) => {
63 | var active = '';
64 | if ((this.props.section === l) || (this.props.subsection === l)) {
65 | active = ' active'
66 | }
67 | return (
68 | {T(l)}
69 | )
70 | })}
71 |
72 | );
73 | }
74 | }
75 |
76 | export default Navigation;
77 |
--------------------------------------------------------------------------------
/webapp/src/components/NavigationUser.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import React, {Component} from 'react';
19 | import {T, isLoggedIn} from './Utils'
20 |
21 |
22 | class NavigationUser extends Component {
23 | handleAccountChange = (e) => {
24 | e.preventDefault()
25 |
26 | // Get the account
27 | var accountId = e.target.value;
28 | var account = this.props.accounts.filter(a => {
29 | return a.orgid === accountId
30 | })[0]
31 |
32 | this.props.onAccountChange(account)
33 | }
34 |
35 | renderAccounts(token) {
36 | if (!isLoggedIn(token)) {
37 | return
38 | }
39 |
40 | if (this.props.accounts.length === 0) {
41 | return
42 | }
43 |
44 | return (
45 |
46 |
53 |
54 | )
55 | }
56 |
57 | renderUser(token) {
58 | if (isLoggedIn(token)) {
59 | // The name is undefined if user authentication is off
60 | if (token.name) {
61 | return (
62 | {token.name}
63 | )
64 | }
65 | } else {
66 | return (
67 | {T('login')}
68 | )
69 | }
70 | }
71 |
72 | renderUserLogout(token) {
73 | if (isLoggedIn(token)) {
74 | return (
75 | {T('logout')}
76 | )
77 | }
78 | }
79 |
80 | render() {
81 |
82 | var token = this.props.token
83 |
84 | return (
85 |
86 | {this.renderAccounts(token)}
87 | {this.renderUser(token)}
88 | {this.renderUserLogout(token)}
89 |
90 | );
91 | }
92 | }
93 |
94 | export default NavigationUser;
95 |
--------------------------------------------------------------------------------
/webapp/src/components/Pagination.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import React, { Component } from 'react'
19 |
20 | class Pagination extends Component {
21 |
22 | constructor(props) {
23 | super(props)
24 |
25 | this.state = {
26 | page: 1,
27 | query: null,
28 | maxRecords: props.pageSize | 5,
29 | }
30 | }
31 |
32 | pageUp = () => {
33 | var pages = this.calculatePages();
34 | var page = this.state.page + 1;
35 | if (page > pages) {
36 | page = pages;
37 | }
38 | this.setState({page: page});
39 | this.signalPageChange(page);
40 | }
41 |
42 | pageDown = () => {
43 | var page = this.state.page - 1;
44 | if (page <= 0) {
45 | page = 1;
46 | }
47 | this.setState({page: page});
48 | this.signalPageChange(page);
49 | }
50 |
51 | signalPageChange(page) {
52 | // Signal the rows that the owner should display
53 | var startRow = ((page - 1) * this.state.maxRecords);
54 |
55 | this.props.pageChange(startRow, startRow + this.state.maxRecords);
56 | }
57 |
58 | calculatePages() {
59 | // Use the filtered row count when we a query has been entered
60 | var length = this.props.displayRows.length;
61 |
62 | var pages = parseInt(length / this.state.maxRecords, 10);
63 | if (length % this.state.maxRecords > 0) {
64 | pages += 1;
65 | }
66 |
67 | return pages;
68 | }
69 |
70 | renderPaging() {
71 | var pages = this.calculatePages();
72 | if (pages > 1) {
73 | return (
74 |
75 | «
76 | {this.state.page} of {pages}
77 | »
78 |
79 | );
80 | } else {
81 | return
;
82 | }
83 | }
84 |
85 | render() {
86 | return (
87 |
88 | {this.renderPaging()}
89 |
90 | );
91 | }
92 | }
93 |
94 | export default Pagination
95 |
--------------------------------------------------------------------------------
/webapp/src/components/Utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import Messages from './Messages'
19 | import {Role} from './Constants'
20 | import jwtDecode from 'jwt-decode'
21 | import api from '../models/api';
22 |
23 |
24 |
25 | const subLinks = {
26 | register: ['register', 'devices'],
27 | devices: ['info', 'snaps'],
28 | groups: ['groups', 'actions'],
29 | actions: ['groups', 'actions'],
30 | }
31 |
32 | export function T(message) {
33 | const msg = Messages[message] || message;
34 | return msg
35 | }
36 |
37 | // URL is in the form:
38 | // /section
39 | // /section/sectionId
40 | // /section/sectionId/subsection
41 | export function parseRoute() {
42 | const parts = window.location.pathname.split('/')
43 |
44 | switch (parts.length) {
45 | case 2:
46 | return {section: parts[1]}
47 | case 3:
48 | return {section: parts[1], sectionId: parts[2]}
49 | case 4:
50 | return {section: parts[1], sectionId: parts[2], subsection: parts[3]}
51 | default:
52 | return {}
53 | }
54 | }
55 |
56 | export function sectionNavLinks(section, sectionId) {
57 | if (section === '') {
58 | return;
59 | }
60 | if ((section === 'devices') && (!sectionId)) {
61 | return subLinks['register']
62 | }
63 | return subLinks[section];
64 | }
65 |
66 | export function isLoggedIn(token) {
67 | return isUserStandard(token)
68 | }
69 |
70 | export function isUserStandard(token) {
71 | return isUser(Role.Standard, token)
72 | }
73 |
74 | export function canUserAdministrate(token) {
75 | return isUser(Role.Admin, token) || isUser(Role.Superuser, token)
76 | }
77 |
78 | export function isUserAdmin(token) {
79 | return isUser(Role.Admin, token)
80 | }
81 |
82 | export function isUserSuperuser(token) {
83 | return isUser(Role.Superuser, token)
84 | }
85 |
86 | export function roleAsString(role) {
87 | var str
88 | switch (role) {
89 | case Role.Standard:
90 | str = "Standard"
91 | break;
92 | case Role.Admin:
93 | str = "Admin"
94 | break;
95 | case Role.Superuser:
96 | str = "Superuser"
97 | break
98 | default:
99 | str= "invalid role"
100 | break;
101 | }
102 | return str
103 | }
104 |
105 | function isUser(role, token) {
106 | if (!token) return false
107 | if (!token.role) return false
108 |
109 | return (token.role >= role)
110 | }
111 |
112 | export function getAuthToken(callback) {
113 | // Get a fresh token and return it to the callback
114 | // The token will be passed to the views
115 | api.getAuthToken().then((resp) => {
116 |
117 | var jwt = resp.headers.authorization
118 |
119 | if (!jwt) {
120 | callback({})
121 | return
122 | }
123 | var token = jwtDecode(jwt)
124 |
125 | if (!token) {
126 | callback({})
127 | return
128 | }
129 | callback(token)
130 | })
131 | }
132 |
133 | export function formatError(data) {
134 | var message = T(data.code);
135 | if (data.message) {
136 | message += ': ' + data.message;
137 | }
138 | return message;
139 | }
140 |
141 | export function saveAccount(account) {
142 | sessionStorage.setItem('accountCode', account.orgid);
143 | sessionStorage.setItem('accountName', account.name);
144 | }
145 |
146 | export function getAccount() {
147 | return {
148 | orgid: sessionStorage.getItem('accountCode'),
149 | name: sessionStorage.getItem('accountName'),
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/webapp/src/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 | import React from 'react';
19 | import ReactDOM from 'react-dom';
20 | import App from './App';
21 | import {getAuthToken} from './components/Utils'
22 |
23 | getAuthToken( (token) => {
24 | ReactDOM.render( , document.getElementById('root'));
25 | })
26 | //registerServiceWorker();
27 |
--------------------------------------------------------------------------------
/webapp/src/models/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the IoT Management Service
3 | * Copyright 2019 Canonical Ltd.
4 | *
5 | * This program is free software: you can redistribute it and/or modify it
6 | * under the terms of the GNU Affero General Public License version 3, as
7 | * published by the Free Software Foundation.
8 | *
9 | * This program is distributed in the hope that it will be useful, but WITHOUT
10 | * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11 | * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 |
18 |
19 | const API_PREFIX = '/v1/'
20 |
21 | function getBaseURL() {
22 | return window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + API_PREFIX;
23 | }
24 |
25 | var Constants = {
26 | baseUrl: getBaseURL()
27 | }
28 |
29 | export default Constants
30 |
--------------------------------------------------------------------------------