Skip to content

Commit

Permalink
Merge pull request #1322 from ansible-semaphore/roles
Browse files Browse the repository at this point in the history
Add Project Team Roles
  • Loading branch information
fiftin committed Jul 9, 2023
2 parents 4fef07b + 076bb19 commit 9a35aab
Show file tree
Hide file tree
Showing 26 changed files with 374 additions and 152 deletions.
2 changes: 1 addition & 1 deletion .dredd/hooks/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func addUserProjectRelation(pid int, user int) {
_, err := store.CreateProjectUser(db.ProjectUser{
ProjectID: pid,
UserID: user,
Admin: true,
Role: db.ProjectOwner,
})
if err != nil {
panic(err)
Expand Down
2 changes: 1 addition & 1 deletion .dredd/hooks/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func main() {
dbConnect()
defer store.Close("")
deleteUserProjectRelation(userProject.ID, userPathTestUser.ID)
transaction.Request.Body = "{ \"user_id\": " + strconv.Itoa(userPathTestUser.ID) + ",\"admin\": true}"
transaction.Request.Body = "{ \"user_id\": " + strconv.Itoa(userPathTestUser.ID) + ",\"role\": \"owner\"}"
})

h.Before("project > /api/project/{project_id}/keys/{key_id} > Updates access key > 204 > application/json", capabilityWrapper("access_key"))
Expand Down
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: semaphore
open_collective: # semaphore
ko_fi: fiftin
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
push:
branches:
- develop
- go19
- roles

jobs:
build-local:
Expand Down
44 changes: 31 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Pull Requests
## Pull Requests

When creating a pull-request you should:

Expand All @@ -8,7 +8,7 @@ When creating a pull-request you should:
- __Update api documentation:__ If your pull-request adding/modifying an API request, make sure you update the swagger documentation (`api-docs.yml`)
- __Run Api Tests:__ If your pull request modifies the API make sure you run the integration tests using dredd.

# Installation in a development environment
## Installation in a development environment

- Check out the `develop` branch
- [Install Go](https://golang.org/doc/install). Go must be >= v1.10 for all the tools we use to work
Expand Down Expand Up @@ -59,14 +59,32 @@ Dredd is used for API integration tests, if you alter the API in any way you mus
matches the responses.

As Dredd and the application database config may differ it expects it's own config.json in the .dredd folder.
The most basic configuration for this using a local docker container to run the database would be
```json
{
"mysql": {
"host": "0.0.0.0:3306",
"user": "semaphore",
"pass": "semaphore",
"name": "semaphore"
}
}
```

### How to run Dredd tests locally

1) Build Dredd hooks:
````bash
task compile:api:hooks
```
2) Install Dredd globally
```bash
npm install -g dredd
```
3) Create `./dredd/config.json` for Dredd. It must contain database connection same as used in Semaphore server.
You can use any supported database dialect for tests. For example BoltDB.
```json
{
"bolt": {
"host": "/tmp/database.boltdb"
},
"dialect": "bolt"
}
```
4) Start Semaphore server (add `--config` option if required):
````bash
./bin/semaphore server
```
5) Start Dredd tests
```
dredd --config ./.dredd/dredd.local.yml
```
32 changes: 5 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,18 @@
# Ansible Semaphore

[![Twitter](https://img.shields.io/twitter/follow/semaphoreui?style=social&logo=twitter)](https://twitter.com/semaphoreui)
[![semaphore](https://snapcraft.io/semaphore/badge.svg)](https://snapcraft.io/semaphore)
[![StackShare](https://img.shields.io/badge/tech-stack-008ff9)](https://stackshare.io/ansible-semaphore)
[![Join the chat at https://gitter.im/AnsibleSemaphore/semaphore](https://img.shields.io/gitter/room/AnsibleSemaphore/semaphore?logo=gitter)](https://gitter.im/AnsibleSemaphore/semaphore?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

<!-- [![Release](https://img.shields.io/github/v/release/ansible-semaphore/semaphore.svg)](https://stackshare.io/ansible-semaphore) -->
<!-- [![Godoc Reference](https://pkg.go.dev/badge/github.com/ansible-semaphore/semaphore?utm_source=godoc)](https://godoc.org/github.com/ansible-semaphore/semaphore) -->
<!-- [![Codacy Badge](https://api.codacy.com/project/badge/Grade/89e0129c6ba64fe2b1ebe983f72a4eff)](https://www.codacy.com/app/ansible-semaphore/semaphore?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=ansible-semaphore/semaphore&amp;utm_campaign=Badge_Grade)
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/89e0129c6ba64fe2b1ebe983f72a4eff)](https://www.codacy.com/app/ansible-semaphore/semaphore?utm_source=github.com&utm_medium=referral&utm_content=ansible-semaphore/semaphore&utm_campaign=Badge_Coverage) -->
[![Twitter](https://img.shields.io/twitter/follow/semaphoreui?style=social&logo=twitter)](https://twitter.com/semaphoreui)

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/fiftin)

Ansible Semaphore is a modern UI for Ansible. It lets you easily run Ansible playbooks, get notifications about fails, control access to deployment system.

If your project has grown and deploying from the terminal is no longer for you then Ansible Semaphore is what you need.

![responsive-ui-phone1](https://user-images.githubusercontent.com/914224/134777345-8789d9e4-ff0d-439c-b80e-ddc56b74fcee.png)

<!--
![image](https://user-images.githubusercontent.com/914224/134411082-48235676-06d2-4d4b-b674-4ffe1e8d0d0d.png)
![semaphore](https://user-images.githubusercontent.com/914224/125253358-c214ed80-e312-11eb-952e-d96a1eba93f6.png)
-->


<!--
- [Releases](https://github.com/ansible-semaphore/semaphore/releases)
- [Installation](https://docs.ansible-semaphore.com/administration-guide/installation)
- [Docker Hub](https://hub.docker.com/r/semaphoreui/semaphore/)
- [Contribution](https://github.com/ansible-semaphore/semaphore/blob/develop/CONTRIBUTING.md)
- [Troubleshooting](https://github.com/ansible-semaphore/semaphore/wiki/Troubleshooting)
- [Roadmap](https://github.com/ansible-semaphore/semaphore/projects)
- [UI Walkthrough](https://blog.strangeman.info/ansible/2017/08/05/semaphore-ui-guide.html) (external blog)
-->

## Installation

### Full documentation
Expand All @@ -48,6 +28,8 @@ sudo semaphore user add --admin --name "Your Name" --login your_login --email yo

### Docker

https://hub.docker.com/r/semaphoreui/semaphore

`docker-compose.yml` for minimal configuration:

```yaml
Expand All @@ -66,7 +48,6 @@ services:
- /path/to/data/home:/etc/semaphore # config.json location
- /path/to/data/lib:/var/lib/semaphore # database.boltdb location (Not required if using mysql or postgres)
```
https://hub.docker.com/r/semaphoreui/semaphore
## Demo
Expand All @@ -93,9 +74,6 @@ All releases after 2.5.1 are signed with the gpg public key

If you like Ansible Semaphore, you can support the project development on [Ko-fi](https://ko-fi.com/fiftin).

[<img src="https://user-images.githubusercontent.com/914224/203517453-4febf7f6-debb-4be9-b6a2-a3b19f5d9f9a.png">](https://ko-fi.com/fiftin)


## License

MIT License
Expand Down
36 changes: 17 additions & 19 deletions api-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ paths:
in: query
required: true
type: string
enum: [name, username, email, admin]
enum: [name, username, email, role]
description: sorting name
x-example: email
- name: order
Expand Down Expand Up @@ -987,8 +987,9 @@ paths:
user_id:
type: integer
minimum: 2
admin:
type: boolean
role:
type: string
example: owner
responses:
204:
description: User added
Expand All @@ -1003,24 +1004,21 @@ paths:
responses:
204:
description: User removed
/project/{project_id}/users/{user_id}/admin:
parameters:
- $ref: "#/parameters/project_id"
- $ref: "#/parameters/user_id"
post:
tags:
- project
summary: Makes user admin
responses:
204:
description: User made administrator
delete:
tags:
- project
summary: Revoke admin privileges
put:
parameters:
- name: Project User
in: body
required: true
schema:
type: object
properties:
role:
type: string
example: owner
summary: Update user role
responses:
204:
description: User admin privileges revoked
description: User updated

# project access keys
/project/{project_id}/keys:
Expand Down
60 changes: 33 additions & 27 deletions api/projects/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package projects
import (
"github.com/ansible-semaphore/semaphore/api/helpers"
"github.com/ansible-semaphore/semaphore/db"
"github.com/gorilla/mux"
"net/http"

"github.com/gorilla/context"
Expand All @@ -22,7 +23,7 @@ func ProjectMiddleware(next http.Handler) http.Handler {
return
}

// check if user it project's team
// check if user in project's team
_, err = helpers.Store(r).GetProjectUser(projectID, user.ID)

if err != nil {
Expand All @@ -42,34 +43,39 @@ func ProjectMiddleware(next http.Handler) http.Handler {
})
}

// MustBeAdmin ensures that the user has administrator rights
func MustBeAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project)
user := context.Get(r, "user").(*db.User)

projectUser, err := helpers.Store(r).GetProjectUser(project.ID, user.ID)

if err == db.ErrNotFound {
w.WriteHeader(http.StatusForbidden)
return
}

if err != nil {
helpers.WriteError(w, err)
return
}

if !projectUser.Admin {
w.WriteHeader(http.StatusForbidden)
return
}

next.ServeHTTP(w, r)
})
// GetMustCanMiddlewareFor ensures that the user has administrator rights
func GetMustCanMiddlewareFor(permissions db.ProjectUserPermission) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project)
user := context.Get(r, "user").(*db.User)

if !user.Admin {
// check if user in project's team
projectUser, err := helpers.Store(r).GetProjectUser(project.ID, user.ID)

if err == db.ErrNotFound {
w.WriteHeader(http.StatusForbidden)
return
}

if err != nil {
helpers.WriteError(w, err)
return
}

if r.Method != "GET" && r.Method != "HEAD" && !projectUser.Can(permissions) {
w.WriteHeader(http.StatusForbidden)
return
}
}

next.ServeHTTP(w, r)
})
}
}

//GetProject returns a project details
// GetProject returns a project details
func GetProject(w http.ResponseWriter, r *http.Request) {
helpers.WriteJSON(w, http.StatusOK, context.Get(r, "project"))
}
Expand Down
2 changes: 1 addition & 1 deletion api/projects/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func AddProject(w http.ResponseWriter, r *http.Request) {
return
}

_, err = helpers.Store(r).CreateProjectUser(db.ProjectUser{ProjectID: body.ID, UserID: user.ID, Admin: true})
_, err = helpers.Store(r).CreateProjectUser(db.ProjectUser{ProjectID: body.ID, UserID: user.ID, Role: db.ProjectOwner})
if err != nil {
helpers.WriteError(w, err)
return
Expand Down
41 changes: 30 additions & 11 deletions api/projects/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,24 @@ func GetUsers(w http.ResponseWriter, r *http.Request) {
func AddUser(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project)
var projectUser struct {
UserID int `json:"user_id" binding:"required"`
Admin bool `json:"admin"`
UserID int `json:"user_id" binding:"required"`
Role db.ProjectUserRole `json:"role"`
}

if !helpers.Bind(w, r, &projectUser) {
return
}

_, err := helpers.Store(r).CreateProjectUser(db.ProjectUser{ProjectID: project.ID, UserID: projectUser.UserID, Admin: projectUser.Admin})
if !projectUser.Role.IsValid() {
w.WriteHeader(http.StatusBadRequest)
return
}

_, err := helpers.Store(r).CreateProjectUser(db.ProjectUser{
ProjectID: project.ID,
UserID: projectUser.UserID,
Role: projectUser.Role,
})

if err != nil {
w.WriteHeader(http.StatusConflict)
Expand All @@ -82,7 +91,7 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
desc := "User ID " + strconv.Itoa(projectUser.UserID) + " added to team"

_, err = helpers.Store(r).CreateEvent(db.Event{
UserID: &user.ID,
UserID: &user.ID,
ProjectID: &project.ID,
ObjectType: &objType,
ObjectID: &projectUser.UserID,
Expand Down Expand Up @@ -127,18 +136,28 @@ func RemoveUser(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}

// MakeUserAdmin writes the admin flag to the users account
func MakeUserAdmin(w http.ResponseWriter, r *http.Request) {
func UpdateUser(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project)
user := context.Get(r, "projectUser").(db.User)
admin := true

if r.Method == "DELETE" {
// strip admin
admin = false
var projectUser struct {
Role db.ProjectUserRole `json:"role"`
}

if !helpers.Bind(w, r, &projectUser) {
return
}

err := helpers.Store(r).UpdateProjectUser(db.ProjectUser{UserID: user.ID, ProjectID: project.ID, Admin: admin})
if !projectUser.Role.IsValid() {
w.WriteHeader(http.StatusBadRequest)
return
}

err := helpers.Store(r).UpdateProjectUser(db.ProjectUser{
UserID: user.ID,
ProjectID: project.ID,
Role: projectUser.Role,
})

if err != nil {
helpers.WriteError(w, err)
Expand Down
Loading

0 comments on commit 9a35aab

Please sign in to comment.