cw_controllers/
admin.rs

1use schemars::JsonSchema;
2use std::fmt;
3use thiserror::Error;
4
5use cosmwasm_schema::cw_serde;
6use cosmwasm_std::{
7    attr, Addr, CustomQuery, Deps, DepsMut, MessageInfo, Response, StdError, StdResult,
8};
9use cw_storage_plus::{Item, Namespace};
10
11// TODO: should the return values end up in utils, so eg. cw4 can import them as well as this module?
12/// Returned from Admin.query_admin()
13#[cw_serde]
14pub struct AdminResponse {
15    pub admin: Option<String>,
16}
17
18/// Errors returned from Admin
19#[derive(Error, Debug, PartialEq)]
20pub enum AdminError {
21    #[error("{0}")]
22    Std(#[from] StdError),
23
24    #[error("Caller is not admin")]
25    NotAdmin {},
26}
27
28// state/logic
29pub struct Admin(Item<Option<Addr>>);
30
31// this is the core business logic we expose
32impl Admin {
33    pub const fn new(namespace: &'static str) -> Self {
34        Admin(Item::new(namespace))
35    }
36
37    pub fn new_dyn(storage_key: impl Into<Namespace>) -> Self {
38        Admin(Item::new_dyn(storage_key))
39    }
40
41    pub fn set<Q: CustomQuery>(&self, deps: DepsMut<Q>, admin: Option<Addr>) -> StdResult<()> {
42        self.0.save(deps.storage, &admin)
43    }
44
45    pub fn get<Q: CustomQuery>(&self, deps: Deps<Q>) -> StdResult<Option<Addr>> {
46        self.0.load(deps.storage)
47    }
48
49    /// Returns Ok(true) if this is an admin, Ok(false) if not and an Error if
50    /// we hit an error with Api or Storage usage
51    pub fn is_admin<Q: CustomQuery>(&self, deps: Deps<Q>, caller: &Addr) -> StdResult<bool> {
52        match self.0.load(deps.storage)? {
53            Some(owner) => Ok(caller == owner),
54            None => Ok(false),
55        }
56    }
57
58    /// Like is_admin but returns AdminError::NotAdmin if not admin.
59    /// Helper for a nice one-line auth check.
60    pub fn assert_admin<Q: CustomQuery>(
61        &self,
62        deps: Deps<Q>,
63        caller: &Addr,
64    ) -> Result<(), AdminError> {
65        if !self.is_admin(deps, caller)? {
66            Err(AdminError::NotAdmin {})
67        } else {
68            Ok(())
69        }
70    }
71
72    pub fn execute_update_admin<C, Q: CustomQuery>(
73        &self,
74        deps: DepsMut<Q>,
75        info: MessageInfo,
76        new_admin: Option<Addr>,
77    ) -> Result<Response<C>, AdminError>
78    where
79        C: Clone + fmt::Debug + PartialEq + JsonSchema,
80    {
81        self.assert_admin(deps.as_ref(), &info.sender)?;
82
83        let admin_str = match new_admin.as_ref() {
84            Some(admin) => admin.to_string(),
85            None => "None".to_string(),
86        };
87        let attributes = vec![
88            attr("action", "update_admin"),
89            attr("admin", admin_str),
90            attr("sender", info.sender),
91        ];
92
93        self.set(deps, new_admin)?;
94
95        Ok(Response::new().add_attributes(attributes))
96    }
97
98    pub fn query_admin<Q: CustomQuery>(&self, deps: Deps<Q>) -> StdResult<AdminResponse> {
99        let admin = self.get(deps)?.map(String::from);
100        Ok(AdminResponse { admin })
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    use cosmwasm_std::testing::{mock_dependencies, mock_info};
109    use cosmwasm_std::Empty;
110
111    #[test]
112    fn set_and_get_admin() {
113        let mut deps = mock_dependencies();
114        let control = Admin::new("foo");
115
116        // initialize and check
117        let admin = Some(Addr::unchecked("admin"));
118        control.set(deps.as_mut(), admin.clone()).unwrap();
119        let got = control.get(deps.as_ref()).unwrap();
120        assert_eq!(admin, got);
121
122        // clear it and check
123        control.set(deps.as_mut(), None).unwrap();
124        let got = control.get(deps.as_ref()).unwrap();
125        assert_eq!(None, got);
126    }
127
128    #[test]
129    fn admin_checks() {
130        let mut deps = mock_dependencies();
131
132        let control = Admin::new("foo");
133        let owner = Addr::unchecked("big boss");
134        let imposter = Addr::unchecked("imposter");
135
136        // ensure checks proper with owner set
137        control.set(deps.as_mut(), Some(owner.clone())).unwrap();
138        assert!(control.is_admin(deps.as_ref(), &owner).unwrap());
139        assert!(!(control.is_admin(deps.as_ref(), &imposter).unwrap()));
140        control.assert_admin(deps.as_ref(), &owner).unwrap();
141        let err = control.assert_admin(deps.as_ref(), &imposter).unwrap_err();
142        assert_eq!(AdminError::NotAdmin {}, err);
143
144        // ensure checks proper with owner None
145        control.set(deps.as_mut(), None).unwrap();
146        assert!(!(control.is_admin(deps.as_ref(), &owner).unwrap()));
147        assert!(!(control.is_admin(deps.as_ref(), &imposter).unwrap()));
148        let err = control.assert_admin(deps.as_ref(), &owner).unwrap_err();
149        assert_eq!(AdminError::NotAdmin {}, err);
150        let err = control.assert_admin(deps.as_ref(), &imposter).unwrap_err();
151        assert_eq!(AdminError::NotAdmin {}, err);
152    }
153
154    #[test]
155    fn test_execute_query() {
156        let mut deps = mock_dependencies();
157
158        // initial setup
159        let control = Admin::new("foo");
160        let owner = Addr::unchecked("big boss");
161        let imposter = Addr::unchecked("imposter");
162        let friend = Addr::unchecked("buddy");
163        control.set(deps.as_mut(), Some(owner.clone())).unwrap();
164
165        // query shows results
166        let res = control.query_admin(deps.as_ref()).unwrap();
167        assert_eq!(Some(owner.to_string()), res.admin);
168
169        // imposter cannot update
170        let info = mock_info(imposter.as_ref(), &[]);
171        let new_admin = Some(friend.clone());
172        let err = control
173            .execute_update_admin::<Empty, Empty>(deps.as_mut(), info, new_admin.clone())
174            .unwrap_err();
175        assert_eq!(AdminError::NotAdmin {}, err);
176
177        // owner can update
178        let info = mock_info(owner.as_ref(), &[]);
179        let res = control
180            .execute_update_admin::<Empty, Empty>(deps.as_mut(), info, new_admin)
181            .unwrap();
182        assert_eq!(0, res.messages.len());
183
184        // query shows results
185        let res = control.query_admin(deps.as_ref()).unwrap();
186        assert_eq!(Some(friend.to_string()), res.admin);
187    }
188}