├── .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 | ![IoT Management Solution Overview](docs/IoTManagement.svg) 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 | 42 | 43 | 44 | 45 | {this.renderRows(items)} 46 | 47 |
{T('code')}{T('name')}
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 |
74 | 75 |
76 | ) 77 | } 78 | let isSuperuser = isUserSuperuser(this.props.token); 79 | 80 | return ( 81 |
82 |
83 |
84 |

{T('accounts')} 85 | {isSuperuser ? 86 |
87 | 88 |